Repository Pattern
debt(d7/e7/b7/t5)
Closest to 'only careful code review or runtime testing' (d7). phpstan/psalm cannot detect missing repository abstraction — scattered Eloquent/Doctrine calls in controllers are valid code that only architecture review catches.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix sounds simple (define interface, implement it), but retrofitting repositories means touching every controller/service that currently calls the ORM directly — cross-cutting across the codebase.
Closest to 'strong gravitational pull' (b7). applies_to spans web/cli/queue and the pattern shapes the domain/infrastructure boundary; once adopted, every new data access decision is shaped by it. Not b9 because it can coexist with direct ORM access in places.
Closest to 'notable trap most devs eventually learn' (t5). Per misconception, devs think a repository is just a DB wrapper and leak ORM methods (whereHas, paginate) into the interface — a well-documented gotcha but not catastrophic.
Also Known As
TL;DR
Explanation
The Repository pattern provides a collection-like interface for accessing domain objects, hiding the underlying data source (SQL, NoSQL, API, file system). Business logic works against the interface (UserRepositoryInterface) without knowing about SQL. This enables testing with in-memory or mock repositories without a database, swapping storage technologies, and enforcing that all data access goes through a defined contract. In PHP, repositories are typically injected via DI and type-hinted to their interface, not their concrete implementation.
Diagram
flowchart LR
subgraph Application Layer
UC[Use Case /<br/>Service]
end
subgraph Domain Layer
ENT[Domain Entity<br/>Order, User]
REPO_INT[Repository<br/>Interface]
end
subgraph Infrastructure Layer
REPO_IMPL[Repository<br/>Implementation]
ORM[ORM / PDO]
DB[(Database)]
end
UC --> REPO_INT
REPO_INT -.->|implemented by| REPO_IMPL
REPO_IMPL --> ORM --> DB
ENT -.->|returned by| REPO_INT
style ENT fill:#6e40c9,color:#fff
style REPO_INT fill:#1f6feb,color:#fff
style REPO_IMPL fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Leaking ORM-specific methods (whereHas, with, paginate) into the repository interface — the interface should speak domain language.
- Creating a generic repository base class that exposes every possible query — defeats the purpose of hiding persistence details.
- Putting business logic inside the repository — it should only handle reading and writing, not rules.
- Skipping repositories entirely in Laravel because Eloquent "is good enough" — fine for small apps, painful at scale.
Avoid When
- Simple CRUD applications with no domain logic — a repository over an ORM that already abstracts the DB is double abstraction.
- The repository becomes a query dumping ground with 50+ methods — use query objects or specifications instead.
- You only have one data source and no plans to change it — the abstraction adds complexity for no gain.
- Microservices that own a single table — an ORM directly in the service is often sufficient.
When To Use
- Domain-driven design where you want to keep persistence logic out of domain entities.
- Applications that need to switch or support multiple data sources (DB + cache + API).
- Code that requires thorough unit testing — repositories can be replaced with in-memory fakes.
- Complex query logic that should be centralised and named rather than scattered across service classes.
Code Examples
// Controller talks directly to Eloquent everywhere
class OrderController {
public function index() {
return Order::where('status', 'paid')
->with('user', 'items')
->orderByDesc('created_at')
->paginate(20);
}
}
// Controller uses repository interface
class OrderController {
public function __construct(private OrderRepositoryInterface $orders) {}
public function index() {
return $this->orders->findPaid(limit: 20);
}
}