persistence-patterns
Persistence, Mappers & Repositories in PHP
When to Use Rich Persistence vs. Simple ORM
Rich persistence (Mapper + Repository) is only worth the ceremony when the application has complex invariants or needs to swap storage. Use simple ORM for straightforward CRUD.
| Use Rich Persistence (Mapper + Repository) | Use Simple ORM (Entity + Query Builder) |
|---|---|
| Complex business invariants to protect | Single-table CRUD with few rules |
| Need to swap storage implementation | Fixed database schema |
| Tests must run without DB for domain logic | Schema changes hand-in-hand with feature |
| Long-lived system, evolving requirements | Prototype, MVP, throwaway code |
A library or package that simply exposes data objects should not include mappers or repository interfaces.
Data Source Spectrum & Gateway vs Repository
Persistence structure is a separate axis from domain-logic style. There is a progression of increasing separation between business logic and storage: Table Data Gateway → Row Data Gateway → Active Record → Data Mapper (see data-source-patterns.md for the spectrum, philosophy, and Gateway-vs-Repository distinction). The lightest end (Table Data Gateway) is one class per table that fetches/persists rows and nothing more; the heavy end (Data Mapper) fully isolates domain objects from the database. Active Record sits in the middle: one object per row that also carries its business behavior and knows how to persist itself. It is outstanding for CRUD-heavy apps with modest rules and small teams that value rapid iteration, but becomes a liability as the domain grows richer and more interconnected — the class drifts from representing the business to representing the ORM. Recognize when it has outgrown its limits and move persistence/mapping out into a Data Mapper. At the heavy end, Data Mapper fully isolates domain objects from the database (only the Mapper bridges the two); it is the preferred persistence model for complex enterprise software (DDD, Hexagonal/Onion/Clean, CQRS writes) because it keeps database concerns out of the domain. Its value is not easy database swaps — it is persistence ignorance for a rich domain model — and the extra indirection is only justified when a rich domain would otherwise be compromised by persistence concerns.