Phase 4 · Databases & PersistenceModule 23~48 min read

CRUD, Transactions & Error Handling

Build complete persistence workflows and protect multi-step changes with atomic transactions.

What you'll learn

Build complete create, read, update, and delete workflows, then make multi-step changes atomic. Transactions ensure observers see all required changes or none of them.

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

  • Implement a complete CRUD boundary
  • Commit or roll back an atomic workflow
  • Handle concurrency and pagination deliberately

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
TransactionAtomic multi-step changeUse when partial success would violate a rule.
Optimistic lockLost-update defenseCheck a version when concurrent editing is plausible.
PaginationBounded query workReturn a stable ordered window, not an unbounded table.

Professional workflow

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

  1. Describe the transactional write 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

Enroll atomically

Capacity and enrollment change together; any exception rolls both back.

enroll.php
<?php
$pdo->beginTransaction();
try {
    $lock = $pdo->prepare('SELECT seats FROM courses WHERE id = :id FOR UPDATE');
    $lock->execute(['id' => $courseId]);
    $seats = $lock->fetchColumn();
    if ($seats === false || (int) $seats < 1) throw new RuntimeException('Course full');

    $pdo->prepare('INSERT INTO enrollments (course_id, student_id) VALUES (:c, :s)')
        ->execute(['c' => $courseId, 's' => $studentId]);
    $pdo->prepare('UPDATE courses SET seats = seats - 1 WHERE id = :id')
        ->execute(['id' => $courseId]);
    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) $pdo->rollBack();
    throw $e;
}

Page with a stable order

A deterministic secondary key prevents records from jumping when timestamps match.

paginate.php
<?php
$page = max(1, filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1);
$size = 20;
$statement = $pdo->prepare(
    'SELECT id, title FROM courses ORDER BY created_at DESC, id DESC LIMIT :size OFFSET :offset'
);
$statement->bindValue('size', $size, PDO::PARAM_INT);
$statement->bindValue('offset', ($page - 1) * $size, PDO::PARAM_INT);
$statement->execute();

Production practice

Contract

A write use case owns its transaction; low-level helpers should not unexpectedly commit it.

Verification

Force a failure after each step and assert the database returns to its original state.

Operations

Keep transactions short, inspect deadlocks, and retry only operations proven safe to repeat.

Common failure mode

Holding a transaction open while sending email or calling an API locks data during unpredictable network work. Commit durable state first, then dispatch follow-up work safely.

Independent workshop

Implement inventory checkout that creates an order, decrements stock, and rejects stale edits.

Your finished workshop must include:

  • Atomic service with rollback
  • Version-based concurrency check
  • Paginated order history

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

  • Transactions protect invariants across statements.
  • The use case defines the atomic boundary.
  • Stable ordering is required for pagination.
  • Concurrency failures need an explicit policy.

Quick check

1. When should a transaction roll back?

2. Why keep transactions short?

3. What makes offset pagination deterministic?

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