Phase 4 · Databases & PersistenceModule 25~46 min read

Migrations, Repositories & Data Mappers

Version schemas and isolate persistence behind testable repositories and mapping boundaries.

What you'll learn

Version database structure and keep persistence choices behind a clean application contract. Migrations explain how schemas evolve; repositories and mappers translate between storage rows and domain objects.

By the end of this lesson, you'll be able to:

  • Write reversible, deployable migrations
  • Define a repository around domain needs
  • Map rows without leaking PDO into business logic

Core mental model

Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.

ConceptWhat it protectsDecision rule
MigrationRepeatable schema historyMake each change small, ordered, and safe for deployed data.
RepositoryCollection-like domain accessExpose use-case queries, not generic table operations.
Data mapperDomain/storage separationTranslate explicitly when shapes or lifecycles differ.

Professional workflow

Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.

  1. Describe the persistence boundary boundary: its inputs, outputs, invariants, and expected failures.
  2. Implement the smallest happy path behind an explicit contract.
  3. Add validation and translate low-level failures into language the caller understands.
  4. Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
  5. Refactor only after behavior is protected by a repeatable check.

Make the boundary visible

Name inputs, outputs, side effects, and failure cases before adding framework or infrastructure code. That habit keeps advanced PHP understandable as the application grows.

Guided code lab

Make a staged schema change

Add the nullable column first, backfill separately, then enforce NOT NULL in a later release.

2026_08_add_slug.sql
ALTER TABLE courses ADD COLUMN slug VARCHAR(190) NULL;
CREATE UNIQUE INDEX courses_slug_unique ON courses (slug);

-- Backfill in bounded batches before a later migration runs:
-- ALTER TABLE courses ALTER COLUMN slug SET NOT NULL;

Map persistence explicitly

The repository owns SQL and mapping; callers receive domain objects.

PdoCourseRepository.php
<?php
final readonly class PdoCourseRepository implements CourseRepository
{
    public function __construct(private PDO $db) {}
    public function get(int $id): Course
    {
        $statement = $this->db->prepare('SELECT id, title, status FROM courses WHERE id = :id');
        $statement->execute(['id' => $id]);
        $row = $statement->fetch();
        if (!$row) throw new CourseNotFound($id);
        return Course::restore((int) $row['id'], $row['title'], CourseStatus::from($row['status']));
    }
}

Production practice

Contract

Repositories speak domain language and never return half-initialized objects or raw database errors.

Verification

Run migrations from empty and from the previous production schema; contract-test repository implementations.

Operations

Plan large backfills separately, monitor locks, and make rollback implications explicit.

Common failure mode

Adding a required column with a computed default to a huge table can lock or rewrite it. Separate structural change, backfill, and constraint enforcement.

Independent workshop

Add publish scheduling to an existing course database without downtime and expose it through a repository.

Your finished workshop must include:

  • Multi-step forward migration plan
  • Mapper handling old and new rows during rollout
  • Repository contract and integration tests

Definition of done

Run the happy path and at least two failure paths, explain one design tradeoff in a short README, and leave the code formatted and ready for review.

Recap & quick check

Key takeaways

  • Migrations are executable schema history.
  • Safe rollout may require several releases.
  • Repositories model domain access.
  • Mappers isolate storage representation.

Quick check

1. Why stage a large required-column change?

2. What should a repository return?

3. What must migration tests cover?

Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.