Join our FREE personalized newsletter for news, trends, and insights that matter to everyone in America

Newsletter
New

Repository Pattern In Laravel: Clean Up Your Messy Code

Card image cap

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! ????