PHP 8: Modern PHP Development · Databases with PDO

Repositories and Data Mapping

Separate SQL from business logic with repositories that return domain objects.

A repository encapsulates data access for an aggregate behind an interface (OrderRepository::find(), save()), mapping rows to domain objects and back. Business code depends on the interface, so tests can use an in-memory fake and you can change storage without touching domain logic. Avoid N+1 queries by fetching related rows in bulk.

<?php
interface OrderRepository
{
    public function find(int $id): ?Order;
    public function save(Order $order): void;
}

final class PdoOrderRepository implements OrderRepository
{
    public function __construct(private PDO $pdo) {}

    public function find(int $id): ?Order
    {
        $stmt = $this->pdo->prepare('SELECT * FROM orders WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        return $row ? $this->hydrate($row) : null;
    }

    public function save(Order $order): void
    {
        $this->pdo->prepare('UPDATE orders SET status = :status, total_cents = :total WHERE id = :id')
            ->execute(['status' => $order->status->value, 'total' => $order->total->amount, 'id' => $order->id]);
    }

    private function hydrate(array $row): Order
    {
        return new Order((int) $row['id'], OrderStatus::from($row['status']), new Money((int) $row['total_cents'], $row['currency']));
    }
}

Common pitfalls

  • Leaking SQL or PDO types into controllers and domain code.
  • Generic repositories with dozens of query methods — add only what use cases need.
  • Lazy loading relations in loops (N+1).

Key points

  • Repositories hide persistence details
  • Return domain objects, not raw rows
  • Fakes make domain tests fast

Further reading: Martin Fowler: Repository pattern

Practice questions

What's the benefit of a repository interface in tests?

Answer

Domain/application services can be tested with an in-memory implementation, avoiding a database and making tests fast and deterministic.

How do you avoid N+1 queries when loading orders with their items?

Answer

Load all items for the fetched order IDs in one query (WHERE order_id IN (...)) and group them in PHP, or use a JOIN, instead of querying per order.

Open in the app to mark lessons complete, take notes and track your progress.