Repositories and Hydration

The documentation is constantly evolving and updated alongside the project's progress. Since it is developed in parallel with the code, some sections may temporarily refer to earlier versions of the system-these are gradually adapted to the current state of the application. If you notice any outdated information, please report it or open an Issue / Pull Request.

Documentation

# Repositories and Hydration

A repository keeps persistence operations close to the domain feature that owns the data. Inject DatabaseInterface; use AbstractRepository for a single-table base case, and add focused methods for domain-specific reads and writes.

# Basic repository

namespace App\Repository;

use Dbm\Database\Contracts\DatabaseInterface;
use Dbm\Database\Repository\AbstractRepository;

final class UserRepository extends AbstractRepository
{
    protected string $table = 'dbm_user';

    public function __construct(protected DatabaseInterface $database)
    {
        parent::__construct($database);
    }
}

The base class provides find($id), insert($data), update($data, $where, $params), and delete($where, $params). find() returns an associative array or null; write methods return a boolean. Conditions and values are separate so the repository can bind parameters.

# Custom reads

Use the SELECT builder for a focused lookup, then return an array or hydrate the row:

public function findByEmail(string $email): ?object
{
    $qb = $this->database->createQueryBuilder();
    $qb->select('id', 'email', 'created_at')
        ->from($this->table)
        ->where('email = :email')
        ->setParameter('email', $email)
        ->setMaxResults(1);

    $row = $this->database->fetch($qb->getSQL(), $qb->getParameters());

    return $row ? $this->database->hydrate($row) : null;
}

For a multi-table result, select explicit columns and aliases to avoid ambiguous fields. Complex joins or domain-specific ordering can use direct parameterized SQL; see Database Queries.

# Hydrating rows

hydrate($row) returns a framework DataObject. Pass a class name to create that class instead, and use hydrateAll() for a list:

$record = $this->database->hydrate($row, UserRecord::class);
$records = $this->database->hydrateAll($rows, UserRecord::class);

The current smart hydrator creates the object without constructor arguments, maps snake_case column names to camelCase public properties, and casts basic scalar types. A custom class therefore needs a no-required-argument constructor and public writable properties for the selected fields. It does not currently hydrate dates, enums, or private properties. Use a DTO mapper or construct the domain object explicitly when its invariants require more than this mapping.

# Platform example: hydrating a product record

DBM Platform's Shopping module uses ProductRepository with the typed ProductRecord class. The repository builds a joined SELECT, fetches a row, and asks the adapter to hydrate it into that class:

$row = $this->database->fetch($qb->getSQL(), $qb->getParameters());
$product = $this->database->hydrate($row, ProductRecord::class);

For a collection, the repository uses hydrateAll($rows, ProductRecord::class). The hydrator maps result keys such as tax_class_id to public properties such as taxClassId and casts supported scalar types. ProductRecord has no required constructor arguments, so it can be created by the hydrator. Its non-nullable typed properties must be included in the SELECT result; optional properties can retain their defaults. Runtime-only data such as the calculated pricing property is filled later by the relevant service, not loaded from the row.

Keep repositories responsible for persistence and result mapping. Put business decisions in application/domain services, and use transactions around multi-step writes that must be atomic.