Skip to content

Repository Implementations

Phexium provides four repository implementations for each aggregate, allowing database switching without changing application code.

Available Implementations

Implementation Use Case Persistence
InMemory Testing, prototyping In-memory array
Sqlite Development, small apps SQLite file
Mysql Production MySQL server
Postgresql Production PostgreSQL server

Usage

All implementations implement the same domain interface:

interface BookRepository
{
    public function findById(IdInterface $id): ?Book;
    public function save(Book $book): void;
    public function findAll(): BooksCollection;
}

InMemory Implementation

Fast testing without database dependencies:

final readonly class InMemoryBookRepository implements BookRepository
{
    public function findById(IdInterface $id): ?Book
    {
        $row = $this->driver->findById(self::TABLE, $id);
        return $row !== null ? $this->rowToBook($row) : null;
    }

    public function save(Book $book): void
    {
        $this->driver->save(self::TABLE, $this->bookToRow($book));
    }
}

Switching Implementations

Configure via DI container:

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

Database Initialization

task dev:database:init              # SQLite development
task tests:acceptance:mysql         # MySQL for tests
task tests:acceptance:postgresql    # PostgreSQL for tests

Mapping Traits

Repository implementations share data mapping logic through traits. Each entity has a dedicated mapping trait that converts between domain objects and database rows.

Structure

trait BookRepositoryMappingTrait
{
    protected function rowToBook(array $row): Book
    {
        return new Book(
            $this->idGenerator->from($row['id']),
            Title::fromString($row['title']),
            Author::fromString($row['author']),
            ISBN::fromString($row['isbn']),
            BookStatus::from($row['status'])
        );
    }

    protected function bookToRow(Book $book): array
    {
        return [
            'id' => $book->getId()->getValue(),
            'title' => $book->getTitle()->getValue(),
            'author' => $book->getAuthor()->getValue(),
            'isbn' => $book->getIsbn()->getValue(),
            'status' => $book->getStatus()->value,
        ];
    }
}

Usage in Repositories

All four implementations use the same trait:

final readonly class InMemoryBookRepository implements BookRepository
{
    use BookRepositoryMappingTrait;
    // ...
}

final readonly class SqliteBookRepository implements BookRepository
{
    use BookRepositoryMappingTrait;
    // ...
}

This approach ensures consistent entity reconstruction across all implementations and avoids code duplication.

Available Traits

  • BookRepositoryMappingTrait - Book entity mapping
  • UserRepositoryMappingTrait - User entity mapping
  • LoanRepositoryMappingTrait - Loan entity mapping

Source Files

  • app/demo/Library/Infrastructure/Trait/BookRepositoryMappingTrait.php
  • app/demo/User/Infrastructure/Trait/UserRepositoryMappingTrait.php
  • app/demo/Loan/Infrastructure/Trait/LoanRepositoryMappingTrait.php

Best Practices

  • Interface in Domain, implementation in Infrastructure
  • Use InMemory for unit tests (fast, no database)
  • Use real implementations for integration tests
  • Keep mapping logic in traits for reuse across implementations

See Also