Skip to content

Repository Layer

The repository layer abstracts all database access behind contracts (interfaces). This decouples business logic from Eloquent, making services testable and the data layer swappable.

Service → RepositoryInterface → ConcreteRepository → Eloquent Model

Directory Structure

app/
├── Contracts/
│   └── Repositories/
│       ├── RepositoryInterface.php
│       ├── FilterRepositoryInterface.php
│       ├── StatusRepositoryInterface.php
│       ├── FilterableRepositoryInterface.php  # Composite
│       ├── BranchRepositoryInterface.php
│       └── UserRepositoryInterface.php
└── Repositories/
    ├── AbstractFilterableRepository.php       # Base class
    ├── PatientRepository.php
    ├── TransactionRepository.php
    ├── BranchRepository.php
    ├── MedicineRepository.php
    └── ... (46 repositories total)

Contracts

RepositoryInterface

The core CRUD contract:

php
/**
 * @template TModel of Model
 */
interface RepositoryInterface
{
    public function find(int $id): ?Model;
    public function create(array $data): Model;
    public function update(int $id, array $data): Model;
    public function delete(int $id): bool;
}

FilterableRepositoryInterface

Composite interface combining filtering, CRUD, and status toggling:

php
interface FilterableRepositoryInterface extends
    FilterRepositoryInterface,
    RepositoryInterface,
    StatusRepositoryInterface
{
}

This is the most commonly used interface — most services depend on it.

AbstractFilterableRepository

The base class all repositories extend. It provides:

  • Pagination with search and filtering
  • Sorting with validated field whitelist
  • CRUD operations
  • Status toggling
php
abstract class AbstractFilterableRepository implements FilterableRepositoryInterface
{
    abstract protected function getModel(): Model;
    abstract protected function getDefaultSortField(): string;
    abstract protected function getAllowedSortFields(): array;
    abstract protected function buildQuery(?string $search, ?array $filters): Builder;

    public function getPaginated(?string $search, ?array $filters, int $perPage = 15)
    {
        $query = $this->buildQuery($search, $filters);

        $sortField = $this->validateSortField($filters['sortField'] ?? null);
        $sortDirection = $this->validateSortDirection($filters['sortDirection'] ?? null);

        return $query->orderBy($sortField, $sortDirection)->paginate($perPage);
    }

    public function find(int $id): ?Model
    {
        return $this->getModel()::find($id);
    }

    public function create(array $data): Model
    {
        return $this->getModel()::create($data);
    }

    public function update(int $id, array $data): Model
    {
        $model = $this->getModel()::findOrFail($id);
        $model->update($data);
        return $model;
    }

    public function delete(int $id): bool
    {
        return $this->getModel()::findOrFail($id)->delete();
    }

    public function toggleStatus(int $id, string $status): Model
    {
        $model = $this->getModel()::findOrFail($id);
        $model->update(['status' => $status]);
        return $model;
    }

    protected function validateSortField(?string $field): string
    {
        return in_array($field, $this->getAllowedSortFields())
            ? $field
            : $this->getDefaultSortField();
    }

    protected function validateSortDirection(?string $direction): string
    {
        return in_array(strtolower($direction ?? ''), ['asc', 'desc'])
            ? strtolower($direction)
            : 'asc';
    }
}

Concrete Repository Examples

PatientRepository

Uses model query scopes for composable filtering:

php
class PatientRepository extends AbstractFilterableRepository
{
    protected function getModel(): Model
    {
        return new Patient();
    }

    protected function getDefaultSortField(): string
    {
        return 'last_name';
    }

    protected function getAllowedSortFields(): array
    {
        return ['id', 'first_name', 'last_name', 'middle_name', 'created_at', 'updated_at'];
    }

    protected function buildQuery(?string $search, ?array $filters): Builder
    {
        return Patient::query()
            ->search($search)
            ->filterByGender($filters['sex'] ?? null)
            ->filterByCivilStatus($filters['civil_status'] ?? null)
            ->filterByStatus($filters['status'] ?? null)
            ->filterByBirthDate($filters['birth_date_from'] ?? null, $filters['birth_date_to'] ?? null)
            ->filterByNationality($filters['nationality'] ?? null)
            ->filterByTelephone($filters['telephone'] ?? null);
    }
}

TransactionRepository

Uses manual query building with eager loading:

php
class TransactionRepository extends AbstractFilterableRepository
{
    protected function getDefaultSortField(): string
    {
        return 'created_at';
    }

    protected function getAllowedSortFields(): array
    {
        return ['id', 'transaction_number', 'patient_id', 'transaction_date',
                'total_amount', 'status', 'created_at', 'updated_at'];
    }

    protected function buildQuery(?string $search, ?array $filters): Builder
    {
        return Transaction::query()
            ->with(['items', 'patient', 'branch', 'paymentMethod'])
            ->when($search, fn ($q) => $q->where(function ($q) use ($search) {
                $q->where('transaction_number', 'like', "%{$search}%")
                  ->orWhereHas('patient', fn ($q) =>
                      $q->where('first_name', 'like', "%{$search}%")
                        ->orWhere('last_name', 'like', "%{$search}%")
                  );
            }))
            ->when($filters['start_date'] ?? null, fn ($q, $date) =>
                $q->whereDate('transaction_date', '>=', $date))
            ->when($filters['end_date'] ?? null, fn ($q, $date) =>
                $q->whereDate('transaction_date', '<=', $date))
            ->when($filters['status'] ?? null, fn ($q, $status) =>
                $q->where('status', $status))
            ->when($filters['branch_id'] ?? null, fn ($q, $branchId) =>
                $q->where('branch_id', $branchId));
    }
}

BranchRepository

Custom methods beyond the base class:

php
class BranchRepository extends AbstractFilterableRepository implements BranchRepositoryInterface
{
    protected function buildQuery(?string $search, ?array $filters): Builder
    {
        return Branch::query()
            ->withCount('users')
            ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"));
    }

    public function findWithRelations(int $id): ?Branch
    {
        return Branch::with(['users', 'branchServices'])->find($id);
    }

    public function hasUsers(int $id): bool
    {
        return Branch::find($id)?->users()->exists() ?? false;
    }

    public function codeExists(string $code): bool
    {
        return Branch::where('code', $code)->exists();
    }
}

Dependency Injection

Repositories are bound to interfaces in RepositoryServiceProvider:

php
// Direct interface bindings
$this->app->bind(BranchRepositoryInterface::class, BranchRepository::class);
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);

// Contextual bindings - Actions get specific repositories
$this->app->when([CreateBranchAction::class, UpdateBranchAction::class])
    ->needs(RepositoryInterface::class)
    ->give(BranchRepository::class);

// Services get FilterableRepositoryInterface
$this->app->when(MedicineService::class)
    ->needs(FilterableRepositoryInterface::class)
    ->give(MedicineRepository::class);

Creating a New Repository

  1. Create the repository in app/Repositories/:
php
class ExampleRepository extends AbstractFilterableRepository
{
    protected function getModel(): Model { return new Example(); }
    protected function getDefaultSortField(): string { return 'created_at'; }
    protected function getAllowedSortFields(): array { return ['id', 'name', 'created_at']; }

    protected function buildQuery(?string $search, ?array $filters): Builder
    {
        return Example::query()
            ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"));
    }
}
  1. Register bindings in RepositoryServiceProvider
  2. Inject into your service or action via constructor

CPR - Clinical Patient Records