Skip to content

Repository Interfaces

Repository Interfaces define contracts for persisting and retrieving aggregate roots. Interfaces belong to the Domain layer; implementations live in Infrastructure.

Characteristics

  • Domain layer location: Interfaces are ports in Domain
  • Aggregate root focus: One repository per aggregate root
  • Collection semantics: Repositories act like in-memory collections
  • Technology agnostic: Interface knows nothing about persistence

Defining Interfaces

interface BookRepository
{
    public function findAll(): BooksCollection;
    public function findBy(SpecificationInterface $spec, ?array $orderBy = null, ?int $offset = null, ?int $limit = null): BooksCollection;
    public function findOneBy(SpecificationInterface $spec): ?Book;
    public function findById(IdInterface $id): ?Book;
    public function getById(IdInterface $id): Book;
    public function exists(IdInterface $id): bool;
    public function save(Book $book): void;
    public function delete(Book $book): int;
    public function deleteById(IdInterface $id): int;
}

Method Conventions

Method Return Behavior
findById($id) ?Entity Returns entity or null
getById($id) Entity Returns entity or throws
findAll() Collection Returns all entities
findBy($spec, ...) Collection Returns matching entities (supports orderBy, offset, limit)
findOneBy($spec) ?Entity Returns first matching entity or null
save($entity) void Insert or update
delete($entity) int Affected rows count
deleteById($id) int Affected rows count

Using Specifications

$specification = new UserIdSpecification($userId);
$loans = $loanRepository->findBy($specification);

// With pagination
$books = $bookRepository->findBy(
    specification: new AlwaysTrueSpecification(),
    orderBy: ['title' => 'ASC'],
    limit: 10
);

Container Configuration

BookRepository::class => match ($_ENV['database.type'] ?? 'Sqlite') {
    'InMemory' => DI\get(InMemoryBookRepository::class),
    'Sqlite' => DI\get(SqliteBookRepository::class),
    'Mysql' => DI\get(MysqlBookRepository::class),
    'Postgresql' => DI\get(PostgresqlBookRepository::class),
},

Best Practices

  • Interface in Domain, implementation in Infrastructure
  • One repository per aggregate root
  • Use Specifications for complex queries
  • Return typed collections, not raw arrays
  • Use InMemory for fast, isolated tests

See Also