The Problem
Ever seen controllers like this?
public class OrderController extends Controller
{
public function show($id)
{
$order = Order::with(['customer', 'items.product'])
->where('id', $id)
->first();
return response()->json($order);
}
public function getUserOrders($userId)
{
// Same query duplicated! π±
$orders = Order::with(['customer', 'items.product'])
->where('customer_id', $userId)
->get();
return response()->json($orders);
}
}
Problems:
π΄ Duplicated queries everywhere
π΄ Controllers tightly coupled to Eloquent
π΄ Impossible to test without database
π΄ Business logic mixed with data access
The Solution: Repository Pattern
Step 1: Create Interface
interface OrderRepositoryInterface
{
public function find(int $id): ?Order;
public function findWithRelations(int $id): ?Order;
public function findByCustomer(int $customerId): Collection;
}
Step 2: Implement Repository
class OrderRepository implements OrderRepositoryInterface
{
protected $model;
public function __construct(Order $model)
{
$this->model = $model;
}
public function findWithRelations(int $id): ?Order
{
return $this->model
->with(['customer', 'items.product'])
->find($id);
}
public function findByCustomer(int $customerId): Collection
{
return $this->model
->with(['customer', 'items.product'])
->where('customer_id', $customerId)
->orderBy('created_at', 'desc')
->get();
}
}
Step 3: Register in Service Provider
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
OrderRepositoryInterface::class,
OrderRepository::class
);
}
}
Step 4: Clean Controller
class OrderController extends Controller
{
protected $orderRepository;
public function __construct(OrderRepositoryInterface $orderRepository)
{
$this->orderRepository = $orderRepository;
}
public function show(int $id)
{
$order = $this->orderRepository->findWithRelations($id);
if (!$order) {
return response()->json(['message' => 'Not found'], 404);
}
return response()->json($order);
}
public function getUserOrders(int $userId)
{
$orders = $this->orderRepository->findByCustomer($userId);
return response()->json($orders);
}
}
Benefits
β
No Duplication - Query logic in one place
β
Easy Testing - Mock repositories instead of database
β
Flexibility - Switch data sources without touching business logic
β
Clean Code - Controllers focus on HTTP concerns
β
Reusability - Use same repository in controllers, jobs, commands
Advanced: Base Repository
abstract class BaseRepository
{
protected $model;
public function all(): Collection
{
return $this->model->all();
}
public function find(int $id): ?Model
{
return $this->model->find($id);
}
public function create(array $data): Model
{
return $this->model->create($data);
}
public function update(int $id, array $data): bool
{
return $this->model->find($id)?->update($data) ?? false;
}
public function delete(int $id): bool
{
return $this->model->find($id)?->delete() ?? false;
}
}
Now extend it:
class ProductRepository extends BaseRepository
{
public function __construct(Product $model)
{
parent::__construct($model);
}
public function getFeatured(int $limit = 10): Collection
{
return $this->model
->where('is_featured', true)
->where('stock', '>', 0)
->limit($limit)
->get();
}
public function searchAndFilter(array $filters)
{
$query = $this->model->query();
if (!empty($filters['search'])) {
$query->where('name', 'like', "%{$filters['search']}%");
}
if (!empty($filters['min_price'])) {
$query->where('price', '>=', $filters['min_price']);
}
return $query->paginate(15);
}
}
Testing Made Easy
Without Repository:
// Must set up entire database
$order = Order::factory()->hasItems(3)->create();
$response = $this->getJson("/api/orders/{$order->id}");
With Repository:
// Just mock the repository!
$orderRepo = Mockery::mock(OrderRepositoryInterface::class);
$orderRepo->shouldReceive('find')
->with(1)
->andReturn($mockOrder);
$this->app->instance(OrderRepositoryInterface::class, $orderRepo);
Common Pitfalls
β Don't Return Query Builder
// Bad
public function getActive()
{
return $this->model->where('active', true); // Query builder!
}
β Do Return Concrete Results
// Good
public function getActive(): Collection
{
return $this->model->where('active', true)->get();
}
β Do Keep Repository for Data Access Only
// Good - Repository only handles data
public function create(array $data): Order
{
return $this->model->create($data);
}
// Business logic in Service
class OrderService
{
public function placeOrder(array $data): Order
{
$order = $this->orderRepository->create($data);
Mail::to($order->customer)->send(new OrderCreated($order));
return $order;
}
}
Quick Checklist
Before implementing Repository Pattern, ask yourself:
- Is my controller doing database queries directly?
- Am I duplicating the same queries in multiple places?
- Is testing my code difficult without a database?
- Do I want to switch between Eloquent/Query Builder/Raw SQL easily?
-
Am I building more than a simple CRUD app?
If you answered yes to 2+ questions, Repository Pattern will help you!Conclusion
The Repository Pattern isn't always necessary for simple CRUD apps, but once your application grows, it becomes invaluable. It gives you:
Clean, testable code
Centralized data access logic
Flexibility to change data sources
Better separation of concerns
Start small - implement it for your most complex models first, then expand as needed.
Want the Full Deep Dive?
This is a condensed version! For the complete guide with:
β¨ More advanced examples (caching, service layer integration)
β¨ Real-world blog system implementation
β¨ Complete testing strategies
β¨ E-commerce order management example
Read the full article on Medium:
π Repository Pattern in Laravel: From Problem to Solution
Follow me for more Laravel tips:
π masteryoflaravel on Medium
What's your experience with Repository Pattern? Love it? Hate it? Let's discuss in the comments! π¬
Top comments (0)