Repository Pattern Guide
Overview
The Repository Pattern provides data access abstraction that isolates domain logic from infrastructure concerns. Repositories act as in-memory collections of aggregates, hiding database implementation details and providing a clean API for domain operations.
Core Concepts
What is a Repository?
A repository mediates between domain and data layers:
php
// ❌ Without Repository: Domain coupled to Eloquent
class ProductService
{
public function activateProduct(int $id): void
{
$product = Product::findOrFail($id); // Coupled to Eloquent
$product->status = 'active';
$product->save();
}
}
// ✅ With Repository: Clean domain layer
class ProductService
{
public function __construct(
private readonly ProductRepositoryInterface $repository
) {}
public function activateProduct(int $id): void
{
$product = $this->repository->findOrFail($id);
$product->activate();
$this->repository->save($product);
}
}Benefits:
- Domain logic independent of infrastructure
- Easy to test (mock repository)
- Centralized query logic
- Can swap implementations (Eloquent, API, in-memory)
Repository Interface
php
<?php
namespace App\Modules\Operations\Domain\Contracts;
use App\Modules\Operations\Domain\Models\Product;
use Illuminate\Support\Collection;
interface ProductRepositoryInterface
{
public function find(int $id): ?Product;
public function findOrFail(int $id): Product;
public function findBySKU(string $sku): ?Product;
public function save(Product $product): void;
public function delete(Product $product): void;
public function all(): Collection;
public function paginate(int $perPage = 15): \Illuminate\Pagination\LengthAwarePaginator;
}Repository Implementation
Base Repository
php
<?php
namespace App\Shared\Infrastructure\Repositories;
use App\Shared\Domain\Models\BaseEntity;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
abstract class EloquentRepository
{
protected Model $model;
public function find(int $id): ?BaseEntity
{
return $this->model->find($id);
}
public function findOrFail(int $id): BaseEntity
{
return $this->model->findOrFail($id);
}
public function save(BaseEntity $entity): void
{
$entity->save();
// Dispatch domain events
foreach ($entity->releaseEvents() as $event) {
Event::dispatch($event);
}
}
public function delete(BaseEntity $entity): void
{
$entity->delete();
}
public function all(): Collection
{
return $this->model->all();
}
public function paginate(int $perPage = 15)
{
return $this->model->paginate($perPage);
}
}Product Repository
php
<?php
namespace App\Modules\Operations\Infrastructure\Repositories;
use App\Modules\Operations\Domain\Contracts\ProductRepositoryInterface;
use App\Modules\Operations\Domain\Models\Product;
use App\Shared\Infrastructure\Repositories\EloquentRepository;
use Illuminate\Support\Collection;
final class EloquentProductRepository extends EloquentRepository implements ProductRepositoryInterface
{
public function __construct(Product $model)
{
$this->model = $model;
}
public function findBySKU(string $sku): ?Product
{
return $this->model->where('sku', $sku)->first();
}
public function findActiveProducts(): Collection
{
return $this->model
->where('status', 'active')
->orderBy('name')
->get();
}
public function findLowStockProducts(int $warehouseId = null): Collection
{
$query = $this->model
->whereColumn('stock_quantity', '<=', 'reorder_point')
->where('stock_quantity', '>', 0);
if ($warehouseId) {
$query->where('warehouse_id', $warehouseId);
}
return $query->get();
}
public function search(string $term): Collection
{
return $this->model
->where('name', 'like', "%{$term}%")
->orWhere('sku', 'like', "%{$term}%")
->orWhere('description', 'like', "%{$term}%")
->get();
}
public function findByCategory(int $categoryId): Collection
{
return $this->model
->where('category_id', $categoryId)
->get();
}
public function existsBySKU(string $sku): bool
{
return $this->model->where('sku', $sku)->exists();
}
}Repository Registration
Service Provider Binding
php
<?php
namespace App\Modules\Operations\Providers;
use Illuminate\Support\ServiceProvider;
use App\Modules\Operations\Domain\Contracts\ProductRepositoryInterface;
use App\Modules\Operations\Infrastructure\Repositories\EloquentProductRepository;
final class OperationsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
ProductRepositoryInterface::class,
EloquentProductRepository::class
);
$this->app->bind(
PurchaseOrderRepositoryInterface::class,
EloquentPurchaseOrderRepository::class
);
// ... other repositories
}
}Using Repositories
In Command Handlers
php
<?php
namespace App\Modules\Operations\Application\Commands;
use App\Shared\Application\Commands\CommandHandler;
use App\Modules\Operations\Domain\Contracts\ProductRepositoryInterface;
final class ActivateProductCommandHandler implements CommandHandler
{
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
) {}
public function handle(Command $command): Product
{
\assert($command instanceof ActivateProductCommand);
// Find via repository
$product = $this->productRepository->findOrFail($command->productId);
// Domain method
$product->activate();
// Save via repository (dispatches events)
$this->productRepository->save($product);
return $product;
}
}In Query Handlers
php
<?php
namespace App\Modules\Operations\Application\Queries;
use App\Shared\Application\Queries\QueryHandler;
use App\Modules\Operations\Domain\Contracts\ProductRepositoryInterface;
final class GetProductByIdQueryHandler implements QueryHandler
{
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
) {}
public function handle(Query $query): Product
{
\assert($query instanceof GetProductByIdQuery);
return $this->productRepository->findOrFail($query->productId);
}
}Advanced Querying
Specification Pattern
php
<?php
namespace App\Modules\Operations\Domain\Specifications;
interface Specification
{
public function isSatisfiedBy($candidate): bool;
public function toQuery(): \Closure;
}
final class ActiveProductSpecification implements Specification
{
public function isSatisfiedBy($product): bool
{
return $product->status === ProductStatus::ACTIVE;
}
public function toQuery(): \Closure
{
return fn($query) => $query->where('status', 'active');
}
}
// Usage in repository
public function findBySpecification(Specification $spec): Collection
{
return $this->model->where($spec->toQuery())->get();
}Query Builder in Repository
php
public function findFiltered(ProductFilterDTO $filters): Collection
{
$query = $this->model->query();
if ($filters->status) {
$query->where('status', $filters->status);
}
if ($filters->categoryId) {
$query->where('category_id', $filters->categoryId);
}
if ($filters->minPrice) {
$query->where('selling_price', '>=', $filters->minPrice);
}
if ($filters->maxPrice) {
$query->where('selling_price', '<=', $filters->maxPrice);
}
if ($filters->searchTerm) {
$query->where(function ($q) use ($filters) {
$q->where('name', 'like', "%{$filters->searchTerm}%")
->orWhere('sku', 'like', "%{$filters->searchTerm}%");
});
}
return $query
->orderBy($filters->sortBy ?? 'name', $filters->sortDirection ?? 'asc')
->get();
}Event Dispatching
Automatic Event Dispatch
php
public function save(BaseEntity $entity): void
{
// Save to database
$entity->save();
// Dispatch all recorded events
foreach ($entity->releaseEvents() as $event) {
Event::dispatch($event);
}
}Flow:
- Domain method records event:
$product->recordEvent($event) - Repository saves entity:
$repository->save($product) - Repository dispatches events automatically
- Event listeners handle events
Testing Repositories
Unit Testing (Mock Repository)
php
use Mockery;
public function test_activates_product_via_repository(): void
{
$mockRepo = Mockery::mock(ProductRepositoryInterface::class);
$product = Product::factory()->make(['status' => ProductStatus::DRAFT]);
$mockRepo->shouldReceive('findOrFail')
->once()
->with(42)
->andReturn($product);
$mockRepo->shouldReceive('save')
->once()
->with($product);
$handler = new ActivateProductCommandHandler($mockRepo);
$handler->handle(new ActivateProductCommand(productId: 42));
$this->assertEquals(ProductStatus::ACTIVE, $product->status);
}Integration Testing (Real Repository)
php
public function test_finds_product_by_sku(): void
{
Product::factory()->create(['sku' => 'TEST-001']);
$repository = app(ProductRepositoryInterface::class);
$product = $repository->findBySKU('TEST-001');
$this->assertNotNull($product);
$this->assertEquals('TEST-001', $product->sku);
}
public function test_saves_product_and_dispatches_events(): void
{
Event::fake();
$repository = app(ProductRepositoryInterface::class);
$product = Product::create(['name' => 'Test Product']);
$product->activate();
$repository->save($product);
Event::assertDispatched(ProductActivated::class);
}Repository Best Practices
DO:
- ✅ Define repository interfaces in Domain layer
- ✅ Implement repositories in Infrastructure layer
- ✅ Use repositories in Application layer (handlers)
- ✅ Dispatch domain events on save
- ✅ Centralize complex queries in repository
- ✅ Return domain entities, not DTOs
DON'T:
- ❌ Use repositories in Domain layer (only interfaces)
- ❌ Expose Eloquent query builder publicly
- ❌ Put business logic in repositories
- ❌ Create generic repositories (one per aggregate)
- ❌ Skip event dispatching
Repository vs Query Builder
Use Repository When:
- Working with aggregates
- Need event dispatching
- Domain operations (save, delete)
- Want to hide persistence details
Use Query Builder When:
- Read-only queries in Query handlers
- Complex reporting
- Aggregations and statistics
- Performance-critical reads
Operations Module Repositories
app/Modules/Operations/Domain/Contracts/
├── ProductRepositoryInterface.php
├── InventoryRepositoryInterface.php
├── PurchaseOrderRepositoryInterface.php
└── StockMovementRepositoryInterface.php
app/Modules/Operations/Infrastructure/Repositories/
├── EloquentProductRepository.php
├── EloquentInventoryRepository.php
├── EloquentPurchaseOrderRepository.php
└── EloquentStockMovementRepository.phpRelated Patterns
- Aggregates - Repositories save aggregates
- Domain Events - Dispatched by repositories
- CQRS Pattern - Repositories in handlers
- Value Objects - VOs in repository methods
Further Reading
- Base Repository:
app/Shared/Infrastructure/Repositories/EloquentRepository.php - Product Repository:
app/Modules/Operations/Infrastructure/Repositories/EloquentProductRepository.php - Domain Contracts:
app/Modules/Operations/Domain/Contracts/