What's New in DataMapper 2.0
DataMapper 2.0 adds query, collection, cache, casting, timestamp, soft-delete, and streaming helpers while keeping the classic DataMapper API available.
Overview
Backward Compatible
Existing DataMapper-style models and controllers can continue to use the classic API while you adopt 2.0 helpers gradually.
DataMapper 2.0 focuses on three key areas:
- Developer Experience - Chainable model syntax
- Performance - Eager loading and caching
- Productivity - Traits and collection methods
Major Features
Eager Loading with Constraints
Load relationships up front and avoid repeated relation queries in loops:
// Load users with their published posts
$users = (new User())
->with([
'post' => function($q) {
$q->where('published', 1)
->order_by('views', 'DESC')
->limit(5);
}
])
->get();
// Posts are already loaded for this result set.
foreach ($users as $user) {
foreach ($user->post as $post) {
echo $post->title;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
For a single relation, eager loading changes the query pattern from 1 + N to one parent query plus one relation query. Multiple relation paths add their own relation queries.
Collections
Work with query results using collection methods:
$users = (new User())
->where('active', 1)
->collect();
// Filter
$adults = $users->filter(fn($u) => $u->age >= 18);
// Map
$names = $users->map(fn($u) => $u->first_name . ' ' . $u->last_name);
// Pluck
$ids = $users->pluck('id');
$emails = $users->pluck('email');
// Aggregate
$totalCredits = $users->sum('credits');
$avgAge = $users->avg('age');
// First/Last
$first = $users->first();
$last = $users->last();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Migrating gradually
get() still returns the model instance (with $this->all populated) so legacy controllers keep working. When you're ready for the query builder API, swap get() for collect() or the other result helpers (pluck(), value(), first()) on a per-call basis.
Query Caching
Cache expensive queries automatically:
// Cache for 1 hour
$users = (new User())
->where('active', 1)
->cache(3600)
->get();
// Cache with custom key
$users = (new User())
->where('status', 'premium')
->cache(3600, 'premium_users')
->get();
// Clear cache
(new User())->clear_cache('premium_users');2
3
4
5
6
7
8
9
10
11
12
13
14
Soft Deletes
Never lose data with soft delete support:
use SoftDeletes;
class User extends DataMapper {
use SoftDeletes;
}
// Soft delete (sets deleted_at timestamp)
$user = (new User())->find(1);
$user->delete();
// Query without deleted records (automatic)
$users = (new User())->get();
// Include deleted records
$allUsers = (new User())->with_softdeleted()->get();
// Only deleted records
$deleted = (new User())->only_softdeleted()->get();
// Restore soft-deleted record
$user->restore();
// Permanently delete
$user->force_delete();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Automatic Timestamps
Never manually manage created_at and updated_at again:
use HasTimestamps;
class User extends DataMapper {
use HasTimestamps;
}
// Automatically sets created_at
$user = new User();
$user->username = 'john';
$user->save(); // created_at = now()
// Automatically updates updated_at
$user->email = 'john@example.com';
$user->save(); // updated_at = now()2
3
4
5
6
7
8
9
10
11
12
13
14
Attribute Casting
Automatically cast database values to proper PHP types:
class User extends DataMapper {
protected $casts = array(
'id' => 'int',
'active' => 'bool',
'credits' => 'float',
'settings' => 'json',
'last_login' => 'datetime'
);
}
$user = (new User())->find(1);
// Automatic type casting
var_dump($user->active); // bool(true) not string "1"
var_dump($user->credits); // float(99.99) not string "99.99"
var_dump($user->settings); // array(...) not string "{...}"
var_dump($user->last_login); // DateTime object2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Streaming Results
Process massive datasets efficiently with generators:
// Stream millions of records with minimal memory
(new User())->stream(function($user) {
// Process each user
echo $user->username . "\n";
// Update user
$user->last_processed = date('Y-m-d H:i:s');
$user->save();
});
// Chunk processing
(new User())->chunk(1000, function($users) {
foreach ($users as $user) {
// Process batch of 1000 users
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dirty Tracking
Know exactly which fields have been modified on a model:
$user = new User();
$user->get_by_id(1);
$user->email = 'new@example.com';
$user->is_dirty(); // TRUE
$user->is_dirty('email'); // TRUE
$user->is_dirty('name'); // FALSE
$user->get_dirty(); // array('email' => 'new@example.com')
$user->get_original('email'); // 'old@example.com'2
3
4
5
6
7
8
9
10
11
Model Events
Hook into the save/delete lifecycle with before and after events:
class User extends DataMapper {
protected function before_save()
{
$this->username = strtolower($this->username);
}
protected function before_delete()
{
if ($this->is_admin) {
return FALSE; // Cancel the delete
}
}
protected function after_create()
{
// Send welcome email
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Local Query Scopes
Encapsulate reusable query logic inside the model:
class Post extends DataMapper {
public function scope_published()
{
return $this->where('status', 'published');
}
public function scope_popular($min = 1000)
{
return $this->where('views >', $min);
}
}
// Use without the scope_ prefix — chainable!
$posts = new Post();
$posts->published()->popular(500)->order_by('views', 'desc')->get();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Serialization Control
Control which fields appear in to_array() and to_json() output:
class User extends DataMapper {
public $hidden = array('password', 'api_secret');
public $appends = array('full_name');
public function get_full_name_attribute()
{
return $this->first_name . ' ' . $this->last_name;
}
}
$user->to_array();
// password and api_secret excluded, full_name included automatically2
3
4
5
6
7
8
9
10
11
12
Model Utilities
Convenience methods for common model operations:
// Atomic counters
$post->increment('views');
$post->decrement('stock', 5);
// Model comparison
$user_a->is($user_b); // Same record?
// Replicate
$copy = $product->replicate();
$copy->name .= ' (Copy)';
$copy->save();
// Bulk delete by ID
User::destroy(array(1, 2, 3));
// Fresh copy from database
$fresh = $user->fresh();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Advanced Query Building
Build complex queries with ease:
// Subqueries
$users = (new User())
->where_in('id', function($subquery) {
$subquery->select('user_id')
->from('orders')
->where('total >', 1000);
})
->get();
// Complex joins
$users = (new User())
->select('users.*, COUNT(posts.id) as post_count')
->join('posts', 'posts.user_id = users.id', 'left')
->group_by('users.id')
->having('post_count >', 10)
->get();
// Conditional queries
$query = (new User())->where('active', 1);
if ($searchTerm) {
$query->where('username LIKE', "%{$searchTerm}%");
}
if ($minAge) {
$query->where('age >=', $minAge);
}
$users = $query->get();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Comparison Table
| Feature | DataMapper 1.x | DataMapper 2.0 |
|---|---|---|
| Syntax | Traditional | Modern query builder + Traditional |
| Eager Loading | Basic | With constraints |
| Related Columns | include_related() flattening | with() + accessors/attributes |
| Collections | No | Yes |
| Query Caching | No | Built-in |
| Soft Deletes | Manual | Trait |
| Timestamps | Manual | Trait |
| Type Casting | Manual | Automatic |
| Streaming | No | Yes |
| Dirty Tracking | No | Built-in |
| Model Events | No | Built-in |
| Query Scopes | No | Built-in |
| Serialization Control | No | $hidden / $visible / $appends |
| Atomic Counters | No | increment() / decrement() |
| Model Comparison | No | is() / is_not() |
| Debugging | check_last_query() | debug(), benchmark(), get_query_index() |
| PHP Version | 5.6 - 7.4 | 7.4 - 8.3+ |
| Performance | Good | Excellent |
Legacy API Quick Reference
| If you used this in 1.x… | Use this in 2.0 | Why it’s better |
|---|---|---|
$user->include_related('company') | (new User())->with('company') | Loads full related objects, supports constraints, fewer queries |
$user->include_related('company', 'name') | Access via accessor/attribute on eager-loaded relation ($user->company->name) | Keeps data normalized, no column collisions |
$config['auto_populate_has_one'] = TRUE | Keep auto-populate disabled and call with() only when needed | Prevents hidden N+1 queries, reduces memory usage |
Manual JSON decoding (json_decode($user->settings)) | Core $casts = array('settings' => 'json') | Automatic hydration + serialization |
Manual timestamp fields ($user->created_at = date(...)) | HasTimestamps trait | Ensures consistent timestamps |
Custom logger wrappers (DMZ_Logger::debug) | dmz_log_message('debug', ...) (delegates to CI log_message) | Single logging pipeline, respects CI thresholds |
These replacements are additive—you can adopt them gradually while legacy code continues to run.
Migration Path
You can adopt 2.0 features gradually:
Phase 1: Drop-in Replacement
// Just replace library files
// Everything works as before
$user = new User();
$user->get();2
3
4
Phase 2: Add Traits
use HasTimestamps, SoftDeletes;
class User extends DataMapper {
use HasTimestamps, SoftDeletes;
}2
3
4
5
Phase 3: Modern Query Builder Syntax
// Start using the chainable query builder
$users = (new User())->where('active', 1)->get();2
Phase 4: Eager Loading
// Optimize with eager loading
$users = (new User())->with('post')->get();2
Real-World Impact
Before DataMapper 2.0
// E-commerce: Get customers with recent orders
$customers = new Customer();
$customers->where('status', 'premium');
$customers->order_by('total_spent', 'DESC');
$customers->limit(50);
$customers->get();
// N+1 problem!
foreach ($customers as $customer) {
$customer->order->where('created_at >', date('Y-m-d', strtotime('-30 days')));
$customer->order->get(); // +1 query per customer!
foreach ($customer->order as $order) {
echo $order->total;
}
}
// Total: 51 queries (1 + 50)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
After DataMapper 2.0
// Same functionality, 96% fewer queries!
$customers = (new Customer())
->with([
'order' => function($q) {
$q->where('created_at >', date('Y-m-d', strtotime('-30 days')))
->order_by('created_at', 'DESC');
}
])
->where('status', 'premium')
->order_by('total_spent', 'DESC')
->limit(50)
->cache(1800) // Cache for 30 minutes
->get();
foreach ($customers as $customer) {
foreach ($customer->order as $order) {
echo $order->total;
}
}
// Total: 2 queries!2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Getting Started
Ready to upgrade? Follow our guide:
- Requirements - Check compatibility
- Upgrading - Step-by-step upgrade guide
- Query Builder - Learn modern syntax
Feature Deep Dives
Start Small
You don't need to adopt everything at once! Start with the modern query builder, then gradually add eager loading and other features as needed.
Questions?
Check our FAQ or GitHub Discussions