Data Mapper vs Active Record
debt(d7/e7/b7/t5)
Closest to 'only careful code review or runtime testing' (d7). While phpstan and deptrac are listed as detection tools, they cannot automatically detect the architectural mismatch of using Active Record for complex domains or Data Mapper for simple CRUD. This requires experienced code review to assess whether the chosen pattern fits the domain complexity. No automated tool can reliably flag 'you used the wrong ORM pattern for your use case.'
Closest to 'cross-cutting refactor across the codebase' (e7). Switching from Active Record (Eloquent) to Data Mapper (Doctrine) or vice versa touches virtually every model, repository, and service in the application. The quick_fix describes choosing the right pattern upfront, but correcting a wrong choice requires rewriting entity classes, query logic, and often service layer code across the entire codebase.
Closest to 'strong gravitational pull' (b7). The ORM pattern choice shapes how every model, repository, and query is written. Per applies_to, this affects web, cli, and queue-worker contexts — essentially the entire application. Every new feature must conform to the chosen pattern, and the accumulated code becomes increasingly expensive to change. Just shy of b9 because it doesn't completely define system shape — other architectural decisions remain independent.
Closest to 'notable trap' (t5). The misconception explicitly states developers believe 'Data Mapper is always better than Active Record' — a documented gotcha that experienced developers eventually learn to evaluate contextually. Common mistakes show the trap works both ways: over-engineering with Data Mapper for simple apps, or polluting Active Record models with complex domain logic. The trap is significant but learnable.
Also Known As
TL;DR
Explanation
Active Record (Laravel Eloquent, Rails): the model extends a base class and knows how to save itself (User::find(), $user->save()). Simple, fast to build, works well for CRUD. Data Mapper (Doctrine): the domain object is a plain PHP class with no knowledge of the database; a separate mapper/repository handles persistence. More complex, but allows true domain objects without ORM contamination. DDD practitioners almost always prefer Data Mapper; simple applications are often better served by Active Record.
Diagram
flowchart LR
subgraph Active Record
AR_MODEL["User extends Model<br/>$user->save()<br/>User::find(1)"]
AR_DB[(Database)]
AR_MODEL <-->|knows about DB| AR_DB
end
subgraph Data Mapper
DM_ENT[User<br/>plain PHP class<br/>no DB knowledge]
DM_REPO[UserRepository<br/>find, save, delete]
DM_DB[(Database)]
DM_ENT <--> DM_REPO <--> DM_DB
end
style AR_MODEL fill:#d29922,color:#fff
style DM_ENT fill:#238636,color:#fff
style DM_REPO fill:#1f6feb,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Using Data Mapper complexity for a simple CRUD app — enormous overhead for no benefit.
- Active Record models with business logic that calls other models directly — creates hidden dependencies.
- Treating Eloquent models as domain entities in complex domains — mix of concerns makes testing harder.
- Not using Eloquent's query scopes to encapsulate query logic — raw where() chains scattered everywhere.
Code Examples
// Active Record domain contamination:
class Order extends Model {
public function process(): void {
// Domain logic mixed with ORM calls:
$this->status = 'processing';
$this->save(); // Domain object knows about DB
$this->customer->notify(); // ORM relationship traversal in domain method
}
}
// Data Mapper — clean domain object:
class Order { // No ORM inheritance — pure domain
private OrderStatus $status = OrderStatus::Pending;
private array $events = [];
public function process(): void {
$this->status = OrderStatus::Processing;
$this->events[] = new OrderProcessed($this->id);
// No DB calls — persistence handled by OrderRepository
}
}