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.
| Concept | What it protects | Decision rule |
|---|---|---|
| Transaction | Atomic multi-step change | Use when partial success would violate a rule. |
| Optimistic lock | Lost-update defense | Check a version when concurrent editing is plausible. |
| Pagination | Bounded query work | Return 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.
- Describe the transactional write boundary: its inputs, outputs, invariants, and expected failures.
- Implement the smallest happy path behind an explicit contract.
- Add validation and translate low-level failures into language the caller understands.
- Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
- Refactor only after behavior is protected by a repeatable check.
Make the boundary visible
Guided code lab
Enroll atomically
Capacity and enrollment change together; any exception rolls both back.
<?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.
<?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
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
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.