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 ModelDirectory 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
- 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}%"));
}
}- Register bindings in
RepositoryServiceProvider - Inject into your service or action via constructor