Repository Pattern in Laravel: Clean Up Your Messy Code

Published: (December 28, 2025 at 06:55 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

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

Laravel Mastery

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 lives in one place
  • Easy Testing – Mock repositories instead of the database
  • Flexibility – Switch data sources without touching business logic
  • Clean Code – Controllers focus on HTTP concerns only
  • Reusability – Same repository can be used in controllers, jobs, commands, etc.

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;
    }
}

Extending the Base Repository

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();
}

✅ 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 the 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, the 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 the Repository Pattern? Love it? Hate it? Let’s discuss in the comments! 💬

Back to Blog

Related posts

Read more »

Laravel FAQs (Beginner to Advanced)

Why Laravel still matters and what this FAQ solves Laravel remains one of the fastest ways to ship secure, maintainable PHP applications — from simple sites to...