0

Repository — a class that mediates between domain objects and data storage

Beginner5 min read·eng-16-012
interviewsolidsql

Concept

Repository — a class that mediates between domain objects (models) and the data storage layer. It provides a collection-like interface for accessing domain objects, hiding the query complexity.

What the Repository pattern provides:

  • A named, centralized place for all queries related to an entity.
  • Isolation between domain logic and persistence concerns.
  • Swappable storage: replace SQL with Elasticsearch or in-memory without changing callers.
  • Better testability: inject an InMemoryRepository in tests, EloquentRepository in production.

What a repository contains:

  • find(int $id): ?Entity
  • findAll(): Collection
  • findBy(array $criteria): Collection
  • save(Entity $entity): void
  • delete(Entity $entity): void
  • Domain-specific queries: findActiveUsersWithOrdersInMonth(Carbon $month): Collection

Repository vs service:

  • Repository: Data access ONLY. No business logic. Returns domain objects.
  • Service: Business logic that orchestrates multiple operations. May use repositories.

In Laravel: The Repository pattern is optional — many developers put queries directly in controllers or models (Eloquent makes this easy). But for large codebases or when you need testability or swappability, repositories are valuable.

Criticism: The "abstract repository with interface" pattern adds overhead. For simple CRUD apps, querying Eloquent directly in controllers is fine. Use the Repository pattern when you have complex query logic or need to swap storage backends.

Code Example

php
<?php
// Repository interface — defines the contract
interface OrderRepositoryInterface
{
    public function find(int $id): ?Order;
    public function findForUser(int $userId): \Illuminate\Support\Collection;
    public function findPendingOlderThan(\Carbon\Carbon $date): \Illuminate\Support\Collection;
    public function save(Order $order): void;
    public function delete(Order $order): void;
}

// Eloquent implementation — production
class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function find(int $id): ?Order
    {
        return Order::with(['items', 'user'])->find($id);
    }

    public function findForUser(int $userId): \Illuminate\Support\Collection
    {
        return Order::where('user_id', $userId)
                    ->with('items')
                    ->orderByDesc('created_at')
                    ->get();
    }

    public function findPendingOlderThan(\Carbon\Carbon $date): \Illuminate\Support\Collection
    {
        return Order::where('status', 'pending')
                    ->where('created_at', '<', $date)
                    ->get();
    }

    public function save(Order $order): void  { $order->save(); }
    public function delete(Order $order): void { $order->delete(); }
}

// In-memory implementation — for tests
class InMemoryOrderRepository implements OrderRepositoryInterface
{
    private array $orders = [];
    private int   $nextId = 1;

    public function find(int $id): ?Order
    {
        return collect($this->orders)->firstWhere('id', $id);
    }

    public function findForUser(int $userId): \Illuminate\Support\Collection
    {
        return collect($this->orders)->where('user_id', $userId)->values();
    }

    public function findPendingOlderThan(\Carbon\Carbon $date): \Illuminate\Support\Collection
    {
        return collect($this->orders)
            ->where('status', 'pending')
            ->filter(fn($o) => $o->created_at < $date)
            ->values();
    }

    public function save(Order $order): void
    {
        $order->id          = $order->id ?? $this->nextId++;
        $this->orders[$order->id] = $order;
    }

    public function delete(Order $order): void { unset($this->orders[$order->id]); }
}

// Bind in service provider
$this->app->bind(OrderRepositoryInterface::class, EloquentOrderRepository::class);

// Service using the repository
class OrderService
{
    public function __construct(private readonly OrderRepositoryInterface $orders) {}

    public function cancelOldPendingOrders(): int
    {
        $stale = $this->orders->findPendingOlderThan(now()->subDays(7));
        foreach ($stale as $order) {
            $order->status = 'cancelled';
            $this->orders->save($order);
        }
        return $stale->count();
    }
}

// Test — no database
class OrderServiceTest extends \PHPUnit\Framework\TestCase
{
    public function test_cancels_old_orders(): void
    {
        $repo    = new InMemoryOrderRepository();
        $service = new OrderService($repo);
        // add test orders, run service, assert
    }
}