Should I keep query business rules in Eloquent local scopes or in a repository layer?
Question
I'm building a Laravel app and query logic is starting to spread across my controllers; I'm repeating the same `where` blocks in several places and it bothers me. I'm torn between two options: should I use fluent Eloquent local scopes, or introduce a repository layer for testability? I keep hearing that repositories abstract the query logic and make it mockable, but I'm also worried that's overkill for this project.
Answer
Short answer
For most Laravel apps the right default is local scopes — they’re testable, composable and idiomatic.
Context
Reach for a repository only when you have a real reason (swapping the data source, or protecting a domain boundary), not reflexively for “testability”.
Why
- The real problem is query logic in controllers, and both fix that. Getting the
whereblocks out of the controller is 80% of the win. Scopes do it with far less ceremony than a repository layer. - Local scopes: idiomatic, composable, cheap.
Order::active()->forCustomer($id)->recent()reads like the domain. You test them easily against a real test DB (in-memory sqlite or a test transaction); you don’t need to mock Eloquent to test your query.class Order extends Model { public function scopeActive(Builder $query): void { $query->where('status', 'active'); } public function scopeForCustomer(Builder $query, int $customerId): void { $query->where('customer_id', $customerId); } } // Usage: Order::active()->forCustomer($id)->get(); - The “repository for testability” argument is weaker than it sounds in Laravel. Eloquent is an Active Record by design, tightly coupled to the DB. Mocking a repository interface tests that you called a method, not that your query is correct. The more valuable test is the one that hits a real database.
What to do
- When a repository does earn its place: when you genuinely need to swap the backing store (Eloquent → external API / search index), when you’re doing DDD with a domain model separate from the Eloquent model, or when you want to forbid query-building from leaking into the domain layer. Those are architectural decisions, not testing ones.
- Don’t write a repository that wraps Eloquent 1:1.
UserRepository::find()callingUser::find()adds a layer and buys you nothing but indirection. That’s the anti-pattern I see most. - A middle path: query classes / actions. For complex, multi-clause queries, a dedicated query class (or Spatie’s query builder) gives you a named, testable object without going all the way to a full repository abstraction.
Bottom line: personally I’d keep the query logic in scopes and test them against a real (sqlite/pgsql) test DB. I only introduce repositories when the domain genuinely needs to decouple from Eloquent — and when I do, they return domain objects, not Eloquent models. Adding a repository purely to enable mocking usually trades a real test for a fake one.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.