DEV Community

Cover image for Repository Pattern in Laravel: Clean Up Your Messy Code
Laravel Mastery
Laravel Mastery

Posted on

Repository Pattern in Laravel: Clean Up Your Messy Code

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Register in Service Provider

class RepositoryServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(
            OrderRepositoryInterface::class,
            OrderRepository::class
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Made Easy
Without Repository:

// Must set up entire database
$order = Order::factory()->hasItems(3)->create();
$response = $this->getJson("/api/orders/{$order->id}");
Enter fullscreen mode Exit fullscreen mode

With Repository:

// Just mock the repository!
$orderRepo = Mockery::mock(OrderRepositoryInterface::class);
$orderRepo->shouldReceive('find')
    ->with(1)
    ->andReturn($mockOrder);

$this->app->instance(OrderRepositoryInterface::class, $orderRepo);
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

❌ Don't Return Query Builder

// Bad
public function getActive()
{
    return $this->model->where('active', true); // Query builder!
}
Enter fullscreen mode Exit fullscreen mode

βœ… Do Return Concrete Results

// Good
public function getActive(): Collection
{
    return $this->model->where('active', true)->get();
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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)