Expanded edition · Research checked 9 October 2026 · PHP 8.4/8.5 core track · Self-contained HTML
How to use this handbook
Learn the mechanism. Prove the behavior.
This handbook expands the original 12-pillar curriculum into an engineering study guide. The target is a developer who can predict PHP behavior, explain the runtime, build correct features, diagnose failures, measure performance, and deliver a system another person can operate. The examples use a community platform serving several client organizations, with tenant-scoped posts, membership, imports, exports, and background work.
Use every chapter in five passes
Read and predict: write down expected outputs or behavior before running an example.
Implement: reproduce the smallest working path using the stated setup.
Inspect: look at input types, stack frames, memory, SQL, requests, logs, and process lifetime.
Break and repair: inject malformed input, a race, a timeout, or a crash and verify the intended invariant.
Explain: record the cause, the enforcing boundary, the remaining limitation, and the evidence that supports the result.
Example and version contract
Core examples target PHP 8.4 or 8.5 on a preferably 64-bit system. Both branches are actively supported at the research date, 9 October 2026. Some constructs used here were introduced earlier, but the handbook does not target legacy runtimes. Use a supported patch release and consult the migration guide for your actual version.
Save the PHP block as a file and run it with php. Each standalone file is independent.
HTTP example
A local web environment or explicitly configured HTTPS setup.
Superglobals, sessions, and headers depend on a real request. CLI execution is not equivalent.
Database helper
MySQL 8.4/InnoDB, PDO MySQL, the named schema, and validated arguments.
A function is one boundary of the application, not a complete authenticated product.
Composer project
The pillar 5 autoload setup and development dependencies.
File paths and namespaces are part of the contract.
Framework fragment
A matching Laravel application, named classes, and configured services.
Wire the fragment into the actual framework lifecycle and test its behavior.
Configuration illustration
Your server, operating-system users, paths, service manager, and policies.
The numbers and paths are illustrative and require environment-specific validation.
Open this HTML file in a browser to study it. PHP code is displayed as text; the browser does not execute it. Copy one example at a time into the stated PHP environment. SQL snippets are for a disposable local database with synthetic data. No provider credentials, private client datasets, or production writes are required for the labs.
Inspect the CLI environment before starting
SHELL Read-only environment checks; CLI results do not prove that FPM uses the same configuration
php -v
php --ini
php -m
composer --version
Useful extensions for the full track include PDO, pdo_mysql, mbstring, fileinfo, and curl. PHPUnit and PHPStan are Composer development dependencies. Xdebug is optional for the debugging/profiling track. Database sessions should use a deliberate UTC policy. IDs in these examples are positive and fit PHP’s integer range, even though a database BIGINT UNSIGNED column can represent values beyond that range.
Study tools in this file
Contents and search: search filters the 12 pillar chapters by their text, including examples and exercises. Press Ctrl/Command + K to focus search and Escape to clear it.
Copy and disclosure: code has a copy button; hints, solution approaches, and expected results are collapsible.
Progress and notes: checks and chapter notes save in this browser when local storage is available. Moving the file or changing browsers may create a different storage context.
Print: the print action opens the full handbook with details expanded, including chapters hidden by search. Your browser can save the print output as a PDF.
Offline reading: content, styles, diagrams, and study tools are embedded. Following a primary-source link requires internet access.
A small plain-PHP CLI/web application with validated inputs and an explained request/runtime trace.
B · Build correct features
4–8; use Composer, MySQL, and one framework.
A tested tenant-scoped feature with constraints, policy, and understandable responsibilities.
C · Survive scale and failure
9–11.
A measured bounded import/export plus replay-safe integration and crash-recovery evidence.
D · Operate and deliver
12, while revisiting every earlier pillar.
A staging release, rollback and restore drill, capacity report, and usable handoff.
The order gives dependencies, not a rule to postpone security until chapter 7 or testing until chapter 8. Revisit earlier examples with stronger contracts as you learn. A framework becomes easier to reason about when you can identify the ordinary PHP, HTTP, SQL, and process mechanisms beneath its APIs.
Choose scope through workload evidence
Question
Example answer for the capstone
What changes when the answer changes
Who uses the system?
Members and admins in several separate client organizations.
Tenant membership, representation visibility, and audit requirements.
What grows?
Posts, comments, uploads, import files, and queued tasks.
Indexes, storage lifecycle, memory bounds, and retention.
What must be immediate?
A permitted local write and its durable result.
Transaction boundaries and synchronous work.
What can be delayed?
An export or digest under a documented completion policy.
Queue interface, job progress, retries, and user-facing status.
What may repeat?
Requests, broker deliveries, webhook events, and import batches.
Stable operation/record identity and duplicate policy.
What must be recoverable?
Database and private file state under an agreed recovery target.
Backup scope, restoration checks, runbook, and reconciliation.
PILLAR 01
Language semantics and the type system
Write PHP whose behavior you can predict.
What to master
Variables, constants, control flow, functions, scope, includes, and error handling.
Scalar types, null, coercion, strict comparisons, truthiness, and missing versus null values.
Arrays, key conversion, destructuring, iterables, closures, arrow functions, and capture by value or reference.
Typed parameters, returns, properties, union and intersection types, enums, readonly data, and attributes.
Strings as bytes, Unicode operations, integer limits, float precision, dates, and time zones.
1.1 Think in values, contracts, and boundaries
A variable gives a value a name. A function gives a transformation a name. A type declaration describes a contract at a boundary. Start by asking three questions: what kind of value enters, what valid result leaves, and how invalid input is reported. A request parameter such as a student ID is initially untrusted input, even when a form presents a numeric field. The application must decide which representations it accepts before converting them.
An integer ID, the string "42", a missing parameter, an empty string, and an array parameter are different cases. A cast such as (int) $raw does not tell you whether the original value satisfied your rules. Design that decision explicitly. Keep boundary validation separate from the function that searches already-valid data.
Student lookup with an explicit input contract
PHP Standalone CLI file: save as student.php; run php student.php 1001
The first function accepts mixed because the boundary may contain missing or malformed input.
The regular expression defines an intentionally narrow policy: no sign, leading zero, decimal point, or surrounding spaces.
The integer filter checks range after the representation check. A huge digit string must not silently become a different ID.
The data is keyed by student ID, so the lookup does not need to inspect every student. The nullable result distinguishes a valid-but-unknown ID from invalid syntax.
The CLI wrapper decides how to show errors. In an HTTP application, the wrapper would instead choose a response status and body.
1.2 Comparisons and truthiness are business decisions
Use === when your rule requires both the value and its type to agree. Loose comparisons involve conversion rules, so they deserve a deliberate reason. Test the exact type returned by a library before choosing a condition: false can represent failure while 0 can be a legitimate result. This is why if (!$result) can discard valid information. A nonempty string such as "false" is not the boolean false.
A local variable belongs to a function call. A closure can capture variables from the surrounding scope. Capture by value preserves the captured value at creation; capture by reference follows the aliased variable. Arrow functions automatically capture used outer variables by value. These differences matter in callbacks and delayed work: a callback can otherwise observe a value you did not expect.
The arrow function captured the earlier value of $base.
The closure aliases $counter; its calls update the same outer variable.
A reference is an aliasing mechanism. Using one should communicate an intentional shared mutation.
Expected result and interpretation
6
1 2 2
With foreach ($rows as &$row), the loop variable can remain a reference to the last element after the loop. Use unset($row) when you intentionally iterate by reference, or prefer a transformation that does not leave that alias behind. Avoid modifying a collection while depending on an assumed iteration order unless you have verified the semantics.
1.4 Strings, money, and time
PHP strings store bytes. Byte count, Unicode code-point count, and visible grapheme count are different concepts. strlen() measures bytes; choose mbstring or intl functions when your requirement concerns encoded text or user-perceived characters. Specify the encoding rather than relying on an accidental default.
For currency, represent minor units as integers when the currency and business rules fit that model. Use a decimal library or a carefully specified decimal representation when more precision is required. Avoid letting a floating-point approximation decide a bill, tax, or ledger entry. Date storage, display timezone, business calendar, and elapsed-time measurement also have separate purposes.
A small money calculation with overflow checks
PHP Standalone CLI; cents are a chosen domain representation, not a universal currency model
<?php
declare(strict_types=1);
function lineTotalCents(int $unitCents, int $quantity): int
{
if ($unitCents < 0 || $quantity < 0) {
throw new InvalidArgumentException('Price and quantity must be nonnegative.');
}
if ($quantity !== 0 && $unitCents > intdiv(PHP_INT_MAX, $quantity)) {
throw new OverflowException('The total exceeds the integer range.');
}
return $unitCents * $quantity;
}
echo lineTotalCents(1299, 3), "\n";
Read the code
The function defines the accepted domain and checks before multiplication.
The result is 3,897 minor units. Formatting a display price is a separate operation.
Discounts and taxes need an explicit rounding policy; add it as a business rule with tests.
Expected result and interpretation
3897
LAB 01
Turn loose input into a clear contract
Extend the student program with department, semester, and enrolled courses.
Accept an ID from CLI first, then from an HTTP query parameter.
Test missing input, an array input, zero, a negative number, decimal text, spaces, an oversized integer, and an unknown valid ID.
Decide whether leading zeros are allowed and document the policy.
Keep lookup logic independent of how the input arrived.
Hint
Write a list of accepted representations before writing a cast. An HTML numeric input does not enforce the server contract.
Solution approach
Validate representation and range at the boundary. Return a typed ID to the lookup. Give invalid input and an unknown valid ID different outcomes. In the HTTP wrapper, use a client-error response for invalid input and a documented not-found response for the missing student.
Done when
Every listed edge case has a deliberate result.
Changing CLI input to HTTP input does not require rewriting the lookup.
LAB 02
Predict, run, and explain
Create a short notebook of surprising PHP values.
Compare null, false, 0, "0", "", "false", and an empty array using selected conditions.
Record your prediction before execution.
Explain each mismatch using a rule, then rewrite one ambiguous condition.
Hint
Do not generalize a comparison rule from one result. Check which conversion the operator performs.
Solution approach
Use strict comparisons when matching typed sentinel values. Use explicit presence and validation checks at request boundaries. Explain truthiness independently from equality.
Done when
You can explain the difference between value equality, identity, presence, and truthiness.
You can name the business rule each rewritten condition enforces.
LAB 03
Integrated pillar challenge
Build a command-line student search and report tool. Accept valid and invalid IDs, normalize input explicitly, represent enrollment status with an enum, and calculate totals using integer minor units for money. Predict outputs before running edge-case examples.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
PHP determines values' types at runtime. Type declarations enforce boundaries; they do not turn PHP into a statically compiled language. strict_types changes scalar coercion at relevant typed boundaries; it does not validate HTTP input or make every variable permanently typed. For arguments, the calling file matters. A PHP array is an ordered map, with different storage optimizations for list-like and mixed-key layouts.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Explain == versus ===, isset() versus array_key_exists(), and an absent key versus a null value.
Write typed functions with clear error contracts and explain a coercion or TypeError without guessing.
Reasoning check: Why can enabling strict_types still leave a malformed request parameter in your program?
strict_types affects scalar coercion at typed boundaries, with the calling file relevant for arguments. HTTP input still arrives as untrusted strings, arrays, missing fields, or raw bytes. The program must validate structure, syntax, range, and business meaning before constructing trusted typed values.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Request memory versus persistent process resources, and PHP memory accounting versus process RSS.
Ordinary FPM requests versus a CLI queue worker or persistent application server.
2.1 Source code becomes engine instructions
In a conventional setup, PHP source is parsed and compiled into Zend opcodes, which the engine executes. OPcache retains compiled script representations so subsequent executions can avoid repeating compilation for unchanged scripts. This is a code cache. The application still performs its business logic and external calls. JIT can translate selected execution paths into native code; its usefulness depends on the workload and configuration.
[6]Compilation and cached execution. The cache branch changes how executable code is obtained. It does not remove the need to execute the application.
2.2 SAPI connects the runtime to its environment
SAPI means Server API: the integration layer through which PHP receives its execution environment and returns output. CLI executes commands; FPM manages FastCGI workers for a web server. PHP extensions add facilities such as PDO drivers, mbstring, fileinfo, or cURL. Different binaries can load different INI files and extensions, so a successful CLI command does not establish that FPM has the same configuration.
Inspect your actual PHP environment
SHELL Run in your own development terminal; compare with your FPM configuration
php -v identifies the CLI runtime. php --ini shows its configuration files.
php -m shows loaded modules. A missing pdo_mysql driver explains a connection failure even when PDO itself exists.
Inspect web-runtime configuration through protected operational access. A public phpinfo page can disclose sensitive configuration.
2.3 Values and shared structures
A zval is the engine representation of a PHP value: it carries type information and a value or a reference to an underlying structure. Some structures use reference counts. Array assignment usually avoids copying an entire backing structure immediately; separation can occur when a shared array is modified. Objects have identity, so assigning an object handle creates another way to access the same object.
The first array mutation changes the separate array value.
The explicit reference aliases the original variable.
Both object variables identify the same object until a new object is created with clone.
Cloning is shallow by default: a nested object property can still identify the same nested object.
Expected result and interpretation
1
3
4
4 9
Reference counting handles many lifetimes efficiently, but a cycle can keep its members referenced even when application code no longer reaches them. PHP has a cycle collector for eligible structures. An ever-growing reachable array is a retention problem: the collector cannot discard data your program still keeps alive.
[9]Observe copy-on-write rather than assuming a byte count
PHP Standalone CLI; compare deltas on the same runtime and build
Assignment can share the underlying array storage; mutation can require separation.
The exact measurements depend on build, allocator, loaded extensions, and other live values.
PHP allocator accounting is different from total process resident memory. Memory freed for reuse need not immediately shrink RSS.
Expected result and interpretation
The printed byte counts depend on your environment.
The final value is 1.
2.4 Request lifetime is not process lifetime
The usual FPM model starts and cleans up ordinary request state for each request, while the operating-system worker process can remain alive. The engine also has persistent resources and shared code-cache structures. A CLI queue worker or a persistent application server can execute many jobs or requests without rebuilding all userland state each time. That changes the obligations of application code.
Ordinary request data is cleaned up at request end.
Per-call locals end normally, but references retained by persistent services can remain.
Static application accumulator
Ordinary userland request state normally resets between requests.
Can grow across jobs or requests in the same application lifetime.
Database contents
Durable state lives outside the PHP request.
Still external state; transaction boundaries remain essential.
OPcache code
Shared compiled-code structures can outlive a request.
Availability and behavior depend on the SAPI and configuration.
Persistent connection or extension allocation
May outlive ordinary request data.
Must be managed according to its actual lifetime.
A reachable accumulator is not garbage
PHP Standalone CLI simulation of repeated work in one process
<?php
declare(strict_types=1);
final class RetainedEvents
{
private static array $events = [];
public static function record(string $event): void
{
self::$events[] = $event;
}
public static function count(): int
{
return count(self::$events);
}
}
for ($job = 0; $job < 1000; $job++) {
RetainedEvents::record(str_repeat('x', 1024));
}
gc_collect_cycles();
echo RetainedEvents::count(), "\n";
Read the code
The static array still reaches every event. Cycle collection cannot infer that you no longer want them.
A bounded buffer, per-job collector, explicit reset, or external logging changes the retention policy.
Worker recycling can limit accumulated damage, but diagnosing the retention source remains necessary.
Expected result and interpretation
1000
LAB 04
Make a mutation map
Test shallow and nested values.
Assign and mutate an array, an explicit reference, an object, and a cloned object.
Add a nested object inside the array and repeat the experiment.
Draw which variables identify the same structure before and after each operation.
Hint
An array can be a separate array value while containing a handle to an object shared elsewhere.
Solution approach
Track the outer container and each nested object independently. A copy-on-write array operation does not automatically deep-clone every object stored inside it. Use explicit cloning or immutable domain values when that is the intended ownership model.
Done when
You correctly predict nested-object mutation.
Your diagram distinguishes an aliased variable from a shared object.
LAB 05
Inspect lifetimes in two runtimes
Compare repeated work in CLI with independent web requests.
Run the accumulator example in one CLI process.
Create a local web endpoint that returns getmypid() and a request-local counter.
Repeat requests under your FPM setup and record worker IDs.
Explain why observing a repeated PID does not mean all request variables persisted.
Hint
The PID describes an operating-system process. A request-local variable describes a much smaller lifetime.
Solution approach
Separate process reuse, engine request initialization/shutdown, persistent allocations, and application-worker reuse. A local development server has its own process model, so identify the runtime before interpreting observations.
Done when
You name the SAPI and configuration used.
Your conclusion distinguishes observation from an untested assumption.
LAB 06
Integrated pillar challenge
Run small experiments with array assignment, an explicit reference, object assignment, and clone. Measure memory before and after array mutation. Then run 1,000 jobs through one CLI process and investigate retained data. Observe FPM worker IDs across separate requests in a local server setup.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
The Zend Engine compiles PHP into opcodes and executes them; OPcache can retain compiled code in shared memory. It does not cache your database rows or rendered page by itself. Normal FPM request cleanup releases ordinary request state, while the worker process can remain alive. A persistent application worker can retain userland objects between requests or jobs. Array assignment and object assignment have different mutation behavior.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Explain the path from source file to opcodes to execution, and separate OPcache from application caching.
Show what persists in your actual runtime and why a request finishing need not terminate its operating-system process.
Reasoning check: Why might the same static accumulator be harmless across ordinary FPM requests but grow indefinitely in a persistent application worker?
Ordinary request-local state is cleaned up at the request boundary even when an FPM operating-system worker is reused. A persistent application boot or a CLI loop can keep the same accumulator reachable across many units of work. Garbage collection cannot discard intentionally reachable values; the code must bound or reset the state.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Trace a request from browser input to a correct response.
What to master
Methods, URLs, headers, content types, bodies, status codes, redirects, and caching headers.
GET parameters, form POST data, JSON via the request body, uploads, and explicit input parsing.
Routing, a front controller, middleware, controllers, templates, and response generation.
Cookies, server-side sessions, login state, session locking, and output buffering.
Browser behavior around caching, CORS, and the Post/Redirect/Get pattern.
3.1 Follow the request through distinct layers
An HTTP request describes an operation on a resource. The web server chooses how to handle it. A front controller such as public/index.php starts the application, which routes the request, applies middleware, calls an application service, and produces a response. In a common Nginx deployment, the dynamic request reaches FPM through FastCGI. The browser sees response bytes, not the server-side PHP source.
A web request and its decisions. Routing and middleware can stop the request before business work. External results return through application code before the response is completed.
Concept
Meaning
PHP consequence
Query parameters
Values in the request target after a question mark.
Read and validate $_GET; duplicate or bracketed keys can have surprising shapes.
Form body
Typically URL-encoded or multipart form data.
PHP normally exposes supported form POST data in $_POST; uploads have $_FILES metadata.
JSON body
A representation with application/json content type.
Read php://input and decode JSON explicitly.
Response header
Metadata such as content type, location, and cookies.
Set before body output has been committed.
Response status
The documented outcome of the request.
Choose http_response_code() or a framework response object.
The content type controls interpretation. A successful JSON parse does not establish that the resulting value has your required shape. Likewise, GET versus POST is a protocol and application-design decision; changing the method does not make unvalidated input trustworthy.
The endpoint rejects unsupported methods and representations before interpreting the payload.
It reads at most the limit plus one byte, so an oversized body can be detected without loading the entire request into application memory. Configure upstream limits too.
JSON syntax, object shape, allowed fields, field type, and domain length are independent checks.
The response wrapper owns protocol output. The business computation need not know about HTTP.
Inspect the endpoint from a terminal
SHELL Place api.php in public/; PHP’s built-in server is for local development
php -S 127.0.0.1:8000 -t public
# In a second terminal:
curl -i -X POST http://127.0.0.1:8000/api.php \
-H 'Content-Type: application/json' \
--data '{"name":"Rabin"}'
curl -i http://127.0.0.1:8000/api.php
Read the code
The first request should return 200 with a JSON greeting.
The GET request should return 405 and an Allow header.
Change the content type, break the JSON, or send a list to distinguish transport, syntax, and semantic failures.
PHP’s built-in development server is useful for learning, but its concurrency and operating model are not the production FPM model. Keep it bound to loopback for local exercises and use the manual to understand platform-specific worker options.
3.3 Cookies and sessions connect separate requests
A cookie is data the browser can send on later eligible requests. With a server-side session, an identifier connects the request to stored session state. Login usually stores an authenticated identity in that state; authorization still needs to evaluate each requested operation. Session storage can impose a lock, so several requests sharing one session can wait for one another even when other workers are idle.
Release the session before slow unrelated work
PHP HTTP lab; observe under a runtime that can serve concurrent requests
<?php
declare(strict_types=1);
// Local exercise only. Add cookie_secure=true under production HTTPS.
session_start([
'use_strict_mode' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
]);
$_SESSION['requests'] = (int) ($_SESSION['requests'] ?? 0) + 1;
$number = $_SESSION['requests'];
session_write_close();
// Artificial delay for observing concurrency in an appropriate runtime.
usleep(1_000_000);
header('Content-Type: text/plain; charset=utf-8');
echo "Request number: $number\n";
Read the code
The session update finishes before the delay, so a locking handler can release its lock.
After closing, do not assume later writes to $_SESSION will be persisted automatically.
A single-process development server can still serialize requests for a different reason. Identify that confounder before diagnosing session locking.
Document the authentication scheme and denial policy.
Conflicting state
409
For example, a key reused for a different operation.
Rate limited / temporarily unavailable
429 / 503
A client needs a bounded recovery policy.
A safe method is intended for retrieval without requested state changes. Idempotency means repeating an operation has the same intended effect, not that every response is identical. After a form write, a 303 redirect to a GET can reduce accidental resubmission on refresh. These protocol semantics guide your API contract; application code must implement the behavior.
Output buffering can postpone when bytes reach the web server. Web-server, proxy, and client buffering can add further delays. Calling flush() does not force every layer to transmit immediately. Likewise, a stray echo or whitespace before headers can break a redirect or JSON response when buffering is absent. Keep output ownership explicit.
Use the JSON endpoint to inspect request and response details.
Send a correct request, an unsupported method, malformed JSON, an unexpected field, and a wrong field type.
Record status, Content-Type, body, and the validation stage for each case.
Repeat one case in browser developer tools and compare the observation with curl.
Hint
A 200 response with an error-looking body is still a protocol success unless you deliberately documented that contract.
Solution approach
Map each failure to a stable machine-readable code and a suitable status. Keep syntax failures separate from field validation. Verify headers and body together rather than judging only visible page text.
Done when
Your table identifies where each request stopped.
The response matches the contract in both clients.
LAB 08
Separate session waiting from worker waiting
Build two endpoints that use the same session.
Measure concurrent requests with the lock held during a delay.
Close the session before the delay and measure again.
Repeat with separate session cookies.
Document the server runtime and number of available workers.
Hint
Changing two variables at once makes a timing result hard to interpret.
Solution approach
Use the same runtime, delay, and request pattern while changing only lock duration or session identity. Explain what the measurements show about that configuration. Do not infer FPM behavior from a single-process server.
Done when
The experiment has a control case.
You can explain why idle workers do not guarantee a session-specific request can proceed.
LAB 09
Integrated pillar challenge
Build a small plain-PHP app with HTML and JSON endpoints. Inspect requests with curl and browser developer tools. Submit a form, follow its redirect, upload a file, and demonstrate two requests sharing a session waiting on a session lock.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
The browser sends HTTP; a typical Nginx setup forwards dynamic work to PHP-FPM through FastCGI. PHP exposes request information and returns output to the web server. JSON bodies are not automatically the same thing as form fields in $_POST. With a locking session handler, requests sharing a session can serialize until the lock is released. Headers and body bytes also have an order that buffering can obscure.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Trace a request through routing, middleware, application code, and response output.
Diagnose a wrong content type, headers-already-sent problem, or request waiting on a session lock.
Reasoning check: A user's two AJAX requests run sequentially even though there are free FPM workers. Which shared resource would you inspect?
Inspect the configured session handler and its locking behavior. With the common file session handler, two requests with the same session can wait for the lock even when other workers are available. Also distinguish this from a single-worker local server, downstream lock, or exhausted worker pool.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Make business rules understandable and changeable.
What to master
Classes, visibility, constructors, interfaces, inheritance, traits, composition, and method contracts.
Dependency injection: passing collaborators explicitly rather than creating them throughout the code.
Domain objects, value objects, DTOs, application services, and database or API adapters.
Business invariants, immutable data where useful, exception boundaries, and transaction ownership.
SOLID principles and selected patterns as tools for a concrete problem; modular boundaries before distributed services.
4.1 Model responsibilities before choosing patterns
A controller translates HTTP into an application call. An application service coordinates a use case. A domain object expresses a business concept and its invariants. A repository or adapter deals with external persistence. These names are useful when they separate responsibilities that change for different reasons. A small application may not need every layer as a separate class.
For a community platform, title validation is a business rule; extracting a string from JSON is a transport concern; inserting a row is a persistence concern. Keeping those boundaries clear lets a CLI import and an HTTP endpoint share the same use case. A framework can construct the objects, but it does not decide which business rule belongs where.
A use case and its boundaries. Two input mechanisms use the same service. The service depends on a persistence contract, while an adapter owns SQL.
4.2 Encapsulation protects invariants
A small immutable title value
PHP Standalone CLI; requires mbstring; valid UTF-8 input is part of this contract
<?php
declare(strict_types=1);
final readonly class PostTitle
{
public string $value;
public function __construct(string $value)
{
$value = trim($value);
if ($value === ''
|| !mb_check_encoding($value, 'UTF-8')
|| mb_strlen($value, 'UTF-8') > 120) {
throw new InvalidArgumentException('A title needs valid UTF-8 and 1–120 characters.');
}
$this->value = $value;
}
}
$title = new PostTitle(' First post ');
echo $title->value, "\n";
Read the code
Construction establishes the invariant once. Callers receive an already-valid title.
The readonly class prevents property reassignment; this object stores a string, so nested mutable-object concerns do not arise.
The object does not know about request bodies, databases, or templates.
Expected result and interpretation
First post
Composition gives an object collaborators that perform other responsibilities. Inheritance expresses a substitutable kind of an existing contract. Traits share implementation; they do not create an independent service boundary. Prefer the simplest arrangement that keeps dependencies and business rules understandable.
4.3 Dependency injection makes dependencies visible
A use case with a replaceable persistence adapter
PHP Standalone CLI architecture demonstration; authentication and authorization precede the use-case call
<?php
declare(strict_types=1);
interface DemoPostStore
{
public function insert(int $tenantId, int $authorId, string $title): int;
}
final class DemoMemoryPostStore implements DemoPostStore
{
private array $rows = [];
public function insert(int $tenantId, int $authorId, string $title): int
{
$id = count($this->rows) + 1;
$this->rows[$id] = compact('tenantId', 'authorId', 'title');
return $id;
}
}
final readonly class DemoCreatePost
{
public function __construct(private DemoPostStore $posts) {}
public function execute(int $trustedTenantId, int $authorId, string $title): int
{
$title = trim($title);
if ($trustedTenantId < 1 || $authorId < 1 || $title === '') {
throw new InvalidArgumentException('Invalid post data.');
}
return $this->posts->insert($trustedTenantId, $authorId, $title);
}
}
$service = new DemoCreatePost(new DemoMemoryPostStore());
echo $service->execute(1, 1001, 'Hello'), "\n";
Read the code
The constructor declares the persistence dependency; the service does not secretly create a connection.
The memory implementation is a demonstration adapter. A real database adapter must preserve constraints and transaction behavior.
An interface earns its place by describing a boundary you can test or replace.
Passing a tenant ID is not authorization. The HTTP wrapper must derive it from verified membership and policy, not trust a submitted number.
Expected result and interpretation
1
A container can resolve constructor dependencies from bindings or reflection. It is an object-building mechanism. Avoid turning it into a global service locator used everywhere: code that pulls arbitrary dependencies from a container hides its true requirements. Read one container resolution path in your framework to remove the sense of magic.
If a use case changes several database records atomically, one layer must own the transaction. Repositories that independently commit their own changes can break the use case’s atomicity. External email or payment calls cannot generally be rolled back by a database rollback. Record durable intent in the transaction when a reliable later side effect is required, then process it separately. Pillar 11 develops this design.
Problem
Useful boundary
Question to ask
Business rule duplicated in controllers
Application service or domain operation
Can both HTTP and import paths call the same rule?
SQL repeated across routes
Persistence adapter or focused repository
Can the adapter preserve the transaction contract?
Provider SDK types spread through the domain
Integration adapter
Can the provider change without changing business values?
Everything depends on a global container
Explicit constructor or method dependencies
Can a reader see what this class needs?
Many one-line abstractions
Simpler code or a clearer real boundary
What change or test does this abstraction make easier?
4.5 Readability is a maintenance feature
Name operations after the domain outcome: publishPost, reserveSeat, or issueRefund. Keep parsing, authorization, business mutation, and response formatting visible as separate decisions. Use guard clauses for invalid conditions when they make the normal path easier to read. Keep an exception meaningful: callers need to know whether a failure means invalid input, denied access, temporary dependency failure, or a programming defect.
LAB 10
Extract one use case
Refactor a controller that mixes parsing, permissions, SQL, and email.
List each responsibility and its input/output.
Move the business rule into one service.
Inject the persistence and notification collaborators.
Call the same service from a CLI command.
Explain how the transaction and later notification stay consistent.
Hint
Do not introduce a class solely to shorten a file. Establish a useful contract.
Solution approach
Keep the controller responsible for HTTP. Derive trusted identity before the call. Put coordinated mutation in the use case and keep a single transaction boundary. Use a durable event or outbox if notification must survive a crash.
Done when
The business rule has one implementation.
Each abstraction has a stated responsibility and a practical reason.
LAB 11
Replace an adapter without changing the rule
Use the DemoPostStore example as a starting point.
Implement a recording fake for tests.
Implement a PDO adapter after studying pillar 6.
Run the same service behavior against both adapters.
Document what the fake does not prove about database constraints.
Hint
A fake can test calls and decisions but cannot establish real SQL behavior.
Solution approach
Use a unit test for the use case’s decisions and an integration test for the adapter’s SQL and constraints. Shared behavior does not imply identical failure modes.
Done when
The service code remains unchanged.
The tests clearly separate application rules from persistence guarantees.
LAB 12
Integrated pillar challenge
Implement CreatePost or PlaceOrder as an application service. Inject a repository and a notification interface. Keep HTTP parsing outside the business rule. Replace the notification implementation with a fake in a test and a real adapter in the application.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
Assigning an object to another variable usually keeps both variables pointing to the same object; cloning has its own shallow-copy behavior unless you implement deeper copying. An interface sets a contract. A dependency injection container constructs and connects objects, often using reflection or configured factories. It is still executing ordinary PHP methods.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Change email providers or persistence details without rewriting the business rule.
Explain every abstraction in terms of a responsibility or change it makes easier; remove unnecessary ones.
Reasoning check: If a controller constructs the database connection, computes permissions, writes data, and sends email, where should the boundaries move?
Keep transport parsing and response mapping in the controller. Put the use-case rule and transaction ownership in an application service; express policy and domain invariants clearly. Inject focused data and notification adapters. Persist durable notification intent where needed instead of assuming a remote email send can be rolled back with SQL.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
composer.json, version constraints, transitive dependencies, composer.lock, install versus update, and platform requirements.
Namespaces, PSR-4 autoloading, generated autoload maps, and vendor/autoload.php.
Relevant PHP-FIG contracts, including logging and HTTP interfaces; consistent coding style.
One framework in depth: choose Laravel for the capstone, then learn its routes, container, middleware, validation, ORM, events, and queues.
Reading framework source and stack traces; package maintenance, audits, and upgrade guides.
5.1 Dependencies are part of the application
An application depends on its own source, libraries, extensions, and runtime. Composer records package requirements and resolves compatible versions, including transitive requirements. The application lock file records exact resolved versions. Installing from that file differs from resolving a new set of versions. Treat a dependency change as a code change that deserves review and validation.
Dependency constraints meet a lock file. Transitive constraints must agree. The lock file records a concrete result so another installation can reproduce the chosen dependency versions.
For an application, commit composer.lock. Run install against the committed lock in a build or deployment. Run update intentionally in a development branch when changing the resolved dependency set. A library has a different relationship to its consumers: its constraints, rather than its private lock file, govern the dependency versions consumers resolve.
The PHP constraint defines compatibility; it does not install the PHP binary.
Extension requirements declare runtime facilities the application needs.
The namespace prefix maps Community classes to src/. File and class-name case matters, especially when moving from a case-insensitive filesystem to Linux.
5.2 Autoloading connects class names to files
A class in src/Title.php
PHP One file in the Composer project; this smaller Title variant is used by the PHPUnit example
<?php
declare(strict_types=1);
namespace Community;
final readonly class Title
{
public function __construct(public string $value)
{
if (trim($value) === '') {
throw new \InvalidArgumentException('A title cannot be blank.');
}
}
}
Use the generated autoloader
PHP Save as bin/demo.php; run after composer install
<?php
declare(strict_types=1);
use Community\Title;
require dirname(__DIR__) . '/vendor/autoload.php';
$title = new Title('First post');
echo $title->value, "\n";
Read the code
The import supplies a convenient class name; it does not load the file itself.
Composer registers the autoload logic. When the class is needed, the runtime invokes an autoloader.
The PSR-4 mapping tells the loader how to locate the class file.
Application code can depend on a logging contract.
PSR-7
HTTP message interfaces
Middleware and clients can share message abstractions.
PSR-15
Server request handlers and middleware
HTTP processing can be composed through compatible interfaces.
PSR-11
Container interface
Container entries and errors use a common contract.
PSR-18
HTTP client interface
Clients can implement a shared synchronous send contract.
These standards do not make every implementation identical. Study the contract and the concrete behavior your application relies on. For example, a logger abstraction does not determine your retention policy, and an HTTP client interface does not automatically supply retries or asynchronous I/O.
5.4 Learn one framework through a vertical feature
After one small plain-PHP application, implement one complete Laravel feature: route, middleware, request validation, controller, service, database operation, response, and test. Locate the entry point and watch container construction. Read the SQL emitted by the ORM. Inspect when a queued job is serialized and what the worker reconstructs. This makes framework behavior explainable rather than mysterious.
Learn conventions because they help a team navigate a codebase, but retain the ability to reason about PHP, HTTP, SQL, and process lifetimes underneath. Learn Symfony or another framework later through the same boundaries when a job or project calls for it.
Create the minimal Composer project and its namespaced class.
Generate the autoloader and run bin/demo.php.
Commit the manifest and lock in your own project.
Create a clean checkout or copy without vendor/ and run composer install.
Compare the resolved dependency versions and verify runtime extensions.
Break a namespace-to-file mapping deliberately and diagnose the error.
Hint
Importing a class with use is different from loading its source file.
Solution approach
Check the namespace prefix, directory, filename case, class name, and generated autoloader. Rebuild the autoloader after changing mappings. Use the lock file for installation and review intentional changes to it.
Done when
A clean environment runs the same feature.
You can explain why a class-not-found error occurred.
LAB 14
Trace a framework feature
Build one Laravel endpoint equivalent to a plain-PHP endpoint.
Locate the application entry point.
Trace routing, middleware, controller construction, validation, service, and response.
Set a breakpoint or add temporary local tracing.
Find one framework source method behind a helper you used.
Hint
Follow the stack and request lifecycle documentation. Do not try to read the entire framework at once.
Solution approach
Trace one concrete request. Record which layer owns each action and which ordinary PHP method performs it. Remove temporary tracing after the experiment.
Done when
You can explain the feature without relying only on its framework command names.
Your notes identify the actual code paths.
LAB 15
Integrated pillar challenge
Convert the plain-PHP project to a Composer project with PSR-4. Then implement one feature in Laravel and trace it through the router, middleware, controller, service, and database call. Reinstall dependencies from the committed lock file in a clean environment.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
Composer resolves dependency versions and produces autoloading infrastructure. In an application, the lock file makes dependency installation reproducible. An autoloader maps a class name to code that must be loaded. Framework conveniences eventually invoke PHP functions and methods; tracing a request exposes those boundaries.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Explain why composer install is appropriate for a deployment using an existing lock file.
Find the real PHP implementation behind one framework helper and explain its contract.
Reasoning check: Why can running composer update during a deployment change behavior even when your application source has not changed?
update resolves allowed dependency versions and can modify the lock file. With an existing committed lock file, install uses that recorded dependency graph. Different dependency code can change behavior even when application files are identical; platform requirements and the actual runtime still need verification.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
ORM-generated SQL, N+1 queries, result hydration, connection limits, and replication lag.
6.1 Constraints make persistent rules durable
PHP variables disappear or change with runtime lifetimes; the database is a separate source of persistent truth. Put durable relationships and uniqueness rules into the schema. Application validation gives useful error messages, while database constraints protect against concurrent callers and other write paths. Neither should be assumed to replace the other.
Core schema with organization membership boundaries
SQL MySQL 8.4 teaching schema; create the database with utf8mb4 and set sessions to UTC
CREATE TABLE tenants (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(120) NOT NULL
) ENGINE=InnoDB;
CREATE TABLE users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(254) NOT NULL UNIQUE,
display_name VARCHAR(80) NOT NULL,
password_hash VARCHAR(255) NOT NULL
) ENGINE=InnoDB;
CREATE TABLE memberships (
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
role VARCHAR(20) NOT NULL,
PRIMARY KEY (tenant_id, user_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;
CREATE TABLE posts (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
author_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(120) NOT NULL,
body TEXT NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (tenant_id, author_id)
REFERENCES memberships(tenant_id, user_id),
INDEX feed_order (tenant_id, created_at, id)
) ENGINE=InnoDB;
Read the code
A user can belong to several organizations through memberships.
The composite foreign key prevents a post author from being linked through a membership belonging to a different tenant.
The feed index matches a common tenant-filtered ordering. It is a starting design to validate with actual queries.
Global email uniqueness is a chosen product rule here. Change it deliberately if the account model requires a different policy.
6.2 PDO connects PHP to a specific driver
Explicit connection configuration
PHP Requires pdo_mysql and environment configuration; use a least-privilege database account
Placeholders represent data literals, not column names, table names, directions, or complete SQL fragments. For a submitted sort option, map allowed names to fixed SQL expressions in application code. Native and emulated prepares differ; read the specific driver’s behavior and never build the remaining query structure from arbitrary input.
6.3 Indexes are access paths with tradeoffs
A composite index is ordered by its component keys. The leading columns strongly influence which filtering and ordering patterns it can serve. Indexes can reduce work for selected queries, but they consume storage and add work to writes. Study the query shape, row distribution, and execution plan rather than indexing every column independently.
[28]A tenant feed index narrows an ordered range. This is a logical view of the access pattern, not a physical dump of InnoDB pages. Verify whether the optimizer actually chooses the intended index.Inspect a feed query and its actual work
SQL EXPLAIN ANALYZE executes the supported statement; use a controlled dataset
EXPLAIN
SELECT id, author_id, title, created_at
FROM posts
WHERE tenant_id = 1
ORDER BY created_at DESC, id DESC
LIMIT 50;
-- In a controlled development dataset:
EXPLAIN ANALYZE
SELECT id, author_id, title, created_at
FROM posts
WHERE tenant_id = 1
ORDER BY created_at DESC, id DESC
LIMIT 50;
Read the code
Read the chosen access path, estimated rows, and ordering work.
Compare estimates with actual execution observations, including loops and returned rows.
Record the data shape and index definitions. A plan result without its workload context is hard to interpret.
SQL Bind the same validated timestamp to two distinct placeholders with native prepares
SELECT id, author_id, title, created_at
FROM posts
WHERE tenant_id = :tenant
AND (
created_at < :before_time_1
OR (created_at = :before_time_2 AND id < :before_id)
)
ORDER BY created_at DESC, id DESC
LIMIT 50;
Read the code
The timestamp and ID together describe the last item of the previous page.
The ID breaks ties between equal timestamps. An order without a tie breaker is not a stable cursor contract.
Keyset pagination avoids scanning a large offset merely to skip earlier rows. Concurrent inserts and mutable order keys still require a documented consistency policy.
6.5 Transactions and concurrent writes
A transaction groups database changes into a commit-or-rollback boundary. Isolation determines what concurrent transactions observe; locks, atomic statements, and constraints determine how competing changes interact. A transaction alone does not automatically turn “read, check, write” into a correct concurrency design.
PHP Separate inventory lab: InnoDB table with id, tenant_id, and nonnegative available; this is one reservation attempt
<?php
declare(strict_types=1);
function reserveOne(PDO $db, int $trustedTenantId, int $itemId): bool
{
$stmt = $db->prepare(
'UPDATE inventory
SET available = available - 1
WHERE tenant_id = :tenant AND id = :id AND available >= 1'
);
$stmt->execute(['tenant' => $trustedTenantId, 'id' => $itemId]);
return $stmt->rowCount() === 1;
}
Read the code
The condition and decrement happen in one database statement.
Two requests cannot both decrement a last unit using this statement while retaining a nonnegative result.
If recording the reservation is a second database write, wrap both operations in one transaction.
Repeated client requests are separate reservation attempts here. Add an operation key if retries must represent one business attempt.
Deadlocks can be a normal consequence of competing transactions. Keep transactions short, acquire resources consistently, and retry the whole failed transaction only under a bounded policy when its operations are safe to repeat. Do not hold a database transaction open around a slow remote network call without understanding the costs and failure model.
Loading 50 posts and then making a separate author query for each post can create an N+1 pattern. Inspect query count and generated SQL. A join, eager load, or bounded batch query can reduce round trips, but blindly loading all relationships can increase the amount of materialized data. The correct fix follows the representation the endpoint actually needs.
Keep tenant distribution and created_at values documented.
Record the query plan, latency, rows examined where available, and result correctness.
Add or adjust one index for a justified query shape.
Compare both read behavior and the write/storage tradeoff.
Implement stable pagination and test equal timestamps.
Hint
A smaller dataset can hide an inefficient access path. A fast cached execution does not show every cost.
Solution approach
Use the same query and comparable conditions for each measurement. Include an explicit tie breaker. Compare plans and verify returned rows; do not conclude that every index improves every query.
Done when
Your report states data, hardware, indexes, and measurement conditions.
The result set remains correct.
LAB 17
Make the last-unit rule survive concurrency
Create the small inventory lab table and seed one available unit.
First implement a naive SELECT followed by UPDATE and observe a controlled race.
Replace it with the conditional atomic UPDATE.
Send two concurrent attempts under a concurrent-capable runtime.
Add a reservation record in the same transaction and force a rollback.
Hint
The critical rule must be checked at the same concurrency boundary as the mutation.
Solution approach
Use the database’s atomic conditional update and inspect its affected-row result. Add uniqueness or operation identity for retry semantics. Roll back both the inventory change and reservation record when a coordinated write fails.
Done when
At most one competing attempt claims the last unit.
A failed coordinated transaction leaves no partial reservation.
LAB 18
Integrated pillar challenge
Move the project to MySQL with migrations and constraints. Record a slow query plan before and after a justified index. Use stable keyset pagination. Run two requests that compete to reserve the last available item; make the rule hold under concurrency.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
The database engine plans and executes SQL; PHP communicates through a driver and materializes the results. Binding protects data values, but placeholders cannot stand for arbitrary SQL identifiers or syntax. Transactions group work, while isolation, locks, conditional updates, and constraints determine what concurrent clients can safely do. Adding an index also changes storage and write costs.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Show query-count and execution-plan evidence for an improvement.
Demonstrate that a database invariant survives concurrent requests and a rollback.
Reasoning check: Why can checking that a username is free before inserting it still produce duplicate users without a unique constraint?
Two concurrent requests can both read that a name is available before either insert commits. Enforce the intended uniqueness in the database and handle the losing insert as a normal conflict. The uniqueness and normalization/collation rule must match the business definition.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
password_hash(), password_verify(), password reset design, and secret management.
Upload validation and storage, path traversal, command injection, SSRF, and unsafe deserialization.
Trust boundaries, allowlists, dependency updates, and avoiding sensitive data in logs.
7.1 Follow the trust boundary
Every request supplies claims: a cookie, an ID, a tenant selector, a filename, a role, or a JSON field. The server decides which claims are authentic and what authority they grant. Authentication identifies a principal; authorization decides whether that principal may perform this operation on this resource now. A valid session does not establish permission for every resource ID.
A resource ID must pass through authorization. The browser can select a tenant, but the server must establish membership and policy. Apply the same rules to HTTP routes, downloads, search results, imports, jobs, and administrative tools.
A useful default is to deny actions unless a rule grants them. Centralize the policy where it can be reviewed and tested, and still keep tenant restrictions in the data access path. Do not trust a client-supplied role or assume that hiding an interface button protects the endpoint.
[34]Delete a post only within an authorized tenant and role
PHP MySQL and the core schema; identity comes from authentication, not a submitted user_id
<?php
declare(strict_types=1);
function deletePermittedPost(
PDO $db,
int $authenticatedUserId,
int $selectedTenantId,
int $postId
): bool {
$stmt = $db->prepare(
"DELETE p FROM posts AS p
JOIN memberships AS m
ON m.tenant_id = p.tenant_id AND m.user_id = :actor
WHERE p.tenant_id = :tenant
AND p.id = :post
AND (p.author_id = m.user_id OR m.role = 'admin')"
);
$stmt->execute([
'actor' => $authenticatedUserId,
'tenant' => $selectedTenantId,
'post' => $postId,
]);
return $stmt->rowCount() === 1;
}
Read the code
The join establishes that the actor belongs to the selected tenant.
The predicate grants this example policy to the author or that tenant’s admin.
The post is located inside the selected tenant. Changing the submitted tenant or post ID cannot bypass these predicates.
A false result intentionally combines missing and unauthorized resources here. Decide your API’s disclosure policy explicitly.
Once comments or other child rows exist, their foreign keys and deletion policy matter. Handle a restrictive constraint or implement an explicit archival/cascade policy.
7.2 Passwords need a password hashing API
Hash, verify, and recognize a future rehash
PHP Standalone CLI demonstration; store the hash in a suitably sized column, never this demo password
The API creates the salt and encodes the information needed for verification in the returned hash.
PASSWORD_DEFAULT can evolve. The schema uses VARCHAR(255) so the application does not assume a fixed hash length.
Rehash after successful authentication when algorithm or work-factor policy changes.
Choose password length, hashing cost, and rate limits as one policy. Some algorithms have input-length constraints; benchmark and consult the current password storage guidance.
Do not replace this with fast SHA-256, a fixed salt, reversible encryption, or an invented algorithm. Login also needs abuse controls, secure recovery, session renewal after authentication, and an error policy that does not expose unnecessary account information. A good hash solves one part of the authentication system.
7.3 Encode output for the destination context
Render untrusted text inside an HTML element
PHP CLI prints HTML source; a browser should display the submitted characters as text
7.4 Sessions and CSRF protect different properties
A browser can attach a session cookie automatically. A cross-site request can therefore carry credentials even when the attacker cannot read the response. A CSRF defense establishes that a state-changing browser request belongs to an intended application interaction. It does not replace authorization, and XSS can undermine the protections around a session.
[39]A server-rendered form with a session CSRF token
PHP HTTPS HTTP example; Secure cookies are deliberately enabled and will not work over ordinary HTTP
<?php
declare(strict_types=1);
// This example expects HTTPS and a configured session handler.
ini_set('session.use_strict_mode', '1');
session_set_cookie_params([
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
'path' => '/',
]);
session_start();
if (!isset($_SESSION['csrf']) || !is_string($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf'];
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$submitted = $_POST['csrf'] ?? null;
if (!is_string($submitted) || !hash_equals($token, $submitted)) {
http_response_code(403);
exit('Invalid request token');
}
// Authenticate, authorize, validate, and then execute the action.
session_write_close();
echo 'Token accepted; the business action is not implemented.';
exit;
}
session_write_close();
header('Content-Type: text/html; charset=UTF-8');
$encoded = htmlspecialchars($token, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo '<form method="post">';
echo '<input type="hidden" name="csrf" value="' . $encoded . '">';
echo '<button type="submit">Submit protected action</button></form>';
Read the code
Configure cookie and strict-mode settings before starting the session.
Generate an unpredictable token and compare it with hash_equals after checking the submitted type.
Keep the token out of URL query strings and ordinary request logs.
Close the session after the values needed by this request have been obtained.
Renew the session ID at the relevant authentication or privilege transition; define logout and expiry behavior separately.
Accept a bounded plain-text attachment into private storage
PHP HTTP helper; fileinfo extension, trusted private directory outside the web root, and prior authentication/authorization/CSRF checks
<?php
declare(strict_types=1);
function storeTextAttachment(array $upload, string $privateDirectory): string
{
if (!isset($upload['error'], $upload['tmp_name'])
|| !is_int($upload['error'])
|| !is_string($upload['tmp_name'])
|| $upload['error'] !== UPLOAD_ERR_OK) {
throw new InvalidArgumentException('Invalid upload');
}
$temporary = $upload['tmp_name'];
if (!is_uploaded_file($temporary)) {
throw new InvalidArgumentException('Expected an HTTP upload');
}
$bytes = filesize($temporary);
if ($bytes === false || $bytes < 1 || $bytes > 1_000_000) {
throw new InvalidArgumentException('Invalid attachment size');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($temporary);
if ($mime !== 'text/plain') {
throw new InvalidArgumentException('Expected plain text');
}
$directory = realpath($privateDirectory);
if ($directory === false || !is_dir($directory)) {
throw new RuntimeException('Storage directory is unavailable');
}
$storageKey = bin2hex(random_bytes(16)) . '.txt';
if (!move_uploaded_file($temporary, $directory . '/' . $storageKey)) {
throw new RuntimeException('Could not store attachment');
}
return $storageKey;
}
Read the code
The original filename and client-reported MIME type do not determine the storage path.
Actual upload provenance, size, and server-detected type are checked before moving the file.
A random storage key is metadata, not permission to access the attachment.
Plain-text detection is not a complete content-security policy. Serve downloads through an authorized route with an attachment disposition and appropriate Content-Type and nosniff headers.
Configure server/body limits, tenant quotas, directory permissions, cleanup, and any required content scanning around this helper.
7.6 Outbound requests and dangerous interpretation
Fetching a submitted URL can turn your server into a way to reach private services. A URL parser alone is not an SSRF defense: redirects, DNS resolution, alternate address forms, and network placement matter. Prefer fixed provider endpoints or a narrow approved destination policy plus controlled egress. Do not let an authenticated user supply arbitrary internal URLs merely because the request has a session.
Keep data out of interpreters: do not unserialize untrusted PHP objects, assemble shell commands from user input, or dynamically include paths chosen by a request. For imports, parse the documented data format into validated values. For processes, use a library with an argument-vector API and a fixed executable. Review error logs and tracing for secrets as well as the response body.
LAB 19
Prove isolation across two client organizations
Create tenants A and B, a regular member in each, and one admin in A.
Build a permission matrix for viewing, editing, deleting, exporting, and downloading.
Try every operation with another tenant’s resource ID.
Test the same resource through search, direct URLs, and a queued operation.
Check that cache keys and export files preserve tenant scope.
Verify the intended behavior for removed memberships and revoked roles.
Hint
An endpoint can be scoped correctly while a background job or cached representation is not.
Solution approach
Derive identity from authentication, check current membership and operation policy, and carry the trusted scope through every data path. Tests should exercise both allowed and denied cases with real resource identifiers.
Done when
The matrix has explicit decisions for every actor/action combination.
Forbidden requests cause no mutation or disclosure.
LAB 20
Test the browser and upload boundaries
Use a disposable local application and synthetic payloads.
Submit an HTML-looking display name and verify it renders as text.
Submit a missing, wrong, and array-shaped CSRF field.
Attempt an oversized, disallowed-type, and malformed upload.
Try a filename containing path separators and confirm it is never used as a storage path.
Check that a stored file cannot be fetched by bypassing the authorized download route.
Hint
Use observations from the browser, response status, storage directory, and database together.
Solution approach
Test each destination context and boundary separately. Keep attachments outside the public root, authorize delivery, and document which content types the application intentionally supports.
Done when
The tests demonstrate safe rendering and rejected state changes.
The original filename does not control the filesystem destination.
LAB 21
Integrated pillar challenge
Threat-model your own local project. Test access with two users and two client organizations. Try changing resource IDs, supplying unexpected fields, uploading deceptive files, and replaying requests. Add regression tests for every weakness you fix.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
A browser may attach session cookies automatically, which creates a CSRF concern for cookie-authenticated state changes. XSS involves data becoming executable browser content. SQL injection involves data becoming SQL syntax. Authentication identifies the caller; authorization must still decide whether that caller can operate on this particular object. Tenant IDs from requests require checks against trusted membership.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Prove that one organization cannot read, edit, export, or download another organization's resources.
Explain the correct defense at each interpreter or trust boundary, and use established password APIs.
Reasoning check: Why does a prepared query that fetches a post by ID still allow a data leak when the ownership check is missing?
Parameter binding prevents submitted values from becoming SQL syntax. It does not decide who may read the row. Authentication, tenant membership, operation policy, and a scoped lookup must establish authority separately.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Unit tests for business rules, database integration tests, HTTP feature tests, and contract tests.
Fixtures, factories, useful test doubles, deterministic time, and tests for failure paths.
PHPUnit, PHPStan, Xdebug breakpoints, call stacks, and variable inspection.
Exceptions versus PHP errors, structured logs, request identifiers, and minimal reproductions.
Regression tests, code review, and CI checks chosen around meaningful risks.
8.1 Test the promise at the right boundary
A useful test proves a behavior that matters when the implementation changes. “The mock was called once” can help for a specific protocol, but it is often weaker than proving that a denied request caused no write or that a replay produced one logical effect. Make test failures explain the violated contract.
Different tests observe different failure boundaries. These categories overlap. Choose the cheapest test that can faithfully expose the failure, then add broader tests where the boundary itself is part of the risk.
Test
Good example
What it cannot establish alone
Unit
A title rejects whitespace and accepts the expected normalized text.
MySQL collation, transaction isolation, or route middleware.
Integration
A unique constraint rejects the second operation key in the real database.
Browser cookie handling and a complete deployment.
HTTP feature
A member of A cannot update B’s post and no row changes.
Behavior under realistic simultaneous database contention.
Contract
A provider adapter maps the documented error payload to a stable result.
The provider will never change or experience an outage.
Load or resilience
Two workers race safely; a crash before acknowledgment is replayed.
Every possible timing and failure interleaving.
8.2 A focused PHPUnit test
Test the Community Title class from pillar 5
PHP Save as tests/TitleTest.php; Composer PSR-4 setup from pillar 5 and PHPUnit 12.5
<?php
declare(strict_types=1);
namespace Community\Tests;
use Community\Title;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
final class TitleTest extends TestCase
{
public function testItKeepsAnOrdinaryTitle(): void
{
$title = new Title('First post');
self::assertSame('First post', $title->value);
}
public function testItRejectsWhitespaceOnlyText(): void
{
$this->expectException(InvalidArgumentException::class);
new Title(" \t\n ");
}
}
Read the code
The first assertion checks an observable contract, not a private implementation detail.
The exception expectation is installed before the invalid constructor call.
These two tests do not claim to cover every domain requirement. Add cases when the title contract grows.
Do not make unit tests rely on an actual clock, random network delays, or a live provider account when those dependencies are not the behavior being tested. Inject a clock or transport where it improves control. Conversely, do not replace the actual database with a fake when the point is to verify a SQL constraint or transaction rule.
8.3 Static analysis adds information before execution
Describe a row shape and handle the absent row
PHP Standalone CLI; PHPDoc shape gives an analyzer more detail than the native array type
A baseline can help introduce an analyzer to an existing codebase, but every ignored finding needs an ownership and reduction plan. Broadly declaring everything mixed or silencing all errors removes the information you wanted the analyzer to provide. Favor precise types where they reflect a real contract.
8.4 Debug the actual path
Reproduce: record the request, data, version, and configuration needed to trigger the problem.
Narrow: identify the earliest boundary where actual values differ from the contract.
Inspect: use a breakpoint to read the stack and local values; inspect SQL and external request metadata where needed.
Explain: write the causal chain, including the hidden assumption.
Repair: add a focused regression case and change the responsible boundary.
Verify: run the relevant tests and observe the previously failing behavior.
Xdebug step debugging is useful for following a request through middleware, a controller, a service, and an adapter. Configure it deliberately for development; avoid exposing debugger access or collecting sensitive values through careless production debugging. A profiler answers a different question from a breakpoint debugger.
An exception type, correlation ID, operation name, elapsed time, and safe resource identifiers can explain a failure without recording raw passwords, cookies, complete uploaded documents, or provider secrets. Structured logs help compare events across requests and workers. Metrics expose trends; traces expose a path; neither excuses a missing explanation of the failure.
Symptom
First evidence to collect
Useful regression
A malformed ID becomes a different valid ID
Raw input type and validation path, with sensitive fields redacted.
Reject an array, exponent string, or numeric suffix at the boundary.
A feed suddenly makes many queries
Query count and SQL for the representation being rendered.
An integration/feature query-budget assertion with a realistic fixture.
Replay the same operation and verify its documented effect.
LAB 22
Turn a discovered bug into a regression
Choose a real mistake from an earlier lab.
Describe the observed behavior and the desired contract in one sentence.
Write the smallest test that fails for that reason.
Verify that the failure is not just a broken test fixture.
Apply the fix, run the focused test, then run the relevant surrounding checks.
Remove a line of the fix temporarily and confirm the test catches the regression.
Hint
A test that passes before the fix may not exercise the bug.
Solution approach
Keep the failure-producing inputs and assert the meaningful result or absence of a side effect. Use a real integration boundary if the bug depends on it; use an isolated unit test if the rule is purely local.
Done when
The test fails for the original behavior and passes for the repaired behavior.
The assertion states a contract rather than copying private implementation steps.
LAB 23
Build a tenant authorization test matrix
Reuse the two-tenant fixtures from pillar 7.
Test permitted member and admin actions.
Test another tenant, a revoked membership, and a nonexistent resource.
Assert both response behavior and the final database state.
Add one test for an export or job initiated through the same feature.
Introduce a deliberate missing tenant predicate and see which test fails.
Hint
A denial response does not prove that a write did not happen before the response.
Solution approach
Inspect persistent state after each request and use stable fixtures. Test the data path and the policy together at an appropriate integration or HTTP boundary.
Done when
An unauthorized request leaves all relevant rows unchanged.
At least one test catches a missing tenant restriction.
LAB 24
Integrated pillar challenge
Introduce one null-handling bug, one SQL behavior bug, and one authorization bug in your local app. Find their causes using different tools. Add a regression test for each and run it in CI. Make a failed external call test deterministic.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
Static analysis reasons about code without executing all its paths. Tests execute selected behavior. Neither alone proves every production condition. A debugger lets you stop execution and inspect the live stack and values; a profiler measures where time is spent. Database integration tests should exercise the engine whose behavior matters to production.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Reproduce a bug, identify its cause, write a failing test, and verify the repair.
Explain what a passing suite does and does not establish; inspect a stack trace confidently.
Reasoning check: A unit test using a fake repository passes, but MySQL rejects the production write. What behavior did your test omit?
The fake usually omitted a database property such as a foreign key, unique key, collation, type conversion, locking rule, or transaction behavior. Preserve the unit test for its local rule and add an integration test against the relevant real database contract.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Improve a measured bottleneck without sacrificing correctness.
What to master
Latency, throughput, p50/p95/p99, CPU time, waiting time, worker utilization, and load-test methodology.
Generators, file streams, bounded batches, bulk writes, and resumable imports.
PDO result buffering, ORM hydration, narrow selects, and end-to-end streaming.
OPcache versus Redis application caching; cache keys, invalidation, TTL, stampedes, and stale data.
FPM capacity, process memory, database capacity, output buffering, and when analytics needs a separate system.
9.1 Name the workload before choosing a technique
“Big data” describes several different problems. Ten million stored rows with selective queries is different from a million-row export, a high arrival rate of small requests, or a CPU-heavy transformation. State the input size, access pattern, acceptable latency, freshness, concurrency, and failure behavior first. PHP often coordinates databases, storage, queues, and analytical engines; moving every dataset into a PHP array is rarely a useful goal.
Push suitable aggregation into the data system; orchestrate specialist tools where justified.
Streaming must remain bounded through the whole path. A generator avoids building its own complete result array. It cannot undo buffering already performed by a driver, a caller, a serializer, or an HTTP output layer.
PDO MySQL commonly uses buffered query results by default. Eloquent’s cursor keeps relatively little application state, but the database driver can still retain the raw result. A cursor method’s name is not proof of end-to-end bounded memory. With an unbuffered MySQL result, you also have restrictions on issuing another query on that connection until the result is consumed.
PHP CLI with mbstring and a trusted input path; counts validated records and does not implement database writes
<?php
declare(strict_types=1);
/**
* Expected CSV header: id,email,name
* @return Generator<int, array{id:int, email:string, name:string}>
*/
function peopleFromCsv(string $path): Generator
{
$handle = fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException('Could not open input');
}
try {
$header = fgetcsv($handle, null, ',', '"', '');
if ($header !== ['id', 'email', 'name']) {
throw new InvalidArgumentException('Unexpected CSV header');
}
$record = 1;
while (($fields = fgetcsv($handle, null, ',', '"', '')) !== false) {
$record++;
if (count($fields) !== 3
|| !is_string($fields[0])
|| preg_match('/^[1-9][0-9]*$/D', $fields[0]) !== 1
|| !is_string($fields[1])
|| !is_string($fields[2])) {
throw new InvalidArgumentException("Malformed record {$record}");
}
$id = filter_var(
$fields[0],
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1]]
);
$name = trim($fields[2]);
if ($id === false
|| filter_var($fields[1], FILTER_VALIDATE_EMAIL) === false
|| !mb_check_encoding($name, 'UTF-8')
|| $name === ''
|| mb_strlen($name, 'UTF-8') > 80) {
throw new InvalidArgumentException("Invalid record {$record}");
}
yield ['id' => $id, 'email' => $fields[1], 'name' => $name];
}
if (!feof($handle)) {
throw new RuntimeException('Input read failed');
}
} finally {
fclose($handle);
}
}
$path = $argv[1] ?? null;
if (!is_string($path)) {
throw new InvalidArgumentException('Usage: php import-read.php people.csv');
}
$count = 0;
foreach (peopleFromCsv($path) as $person) {
// Validate further and write a bounded batch here.
$count++;
}
echo "Valid records: {$count}" . PHP_EOL;
Read the code
The parser produces one CSV record at a time, including records with quoted delimiters or newlines.
An empty escape argument is explicit. Relying on fgetcsv’s default escape is deprecated from PHP 8.4.
The header and each record are validated before yielding a useful shape.
The finally block closes the handle when the generator completes or is disposed after an error.
Memory can still grow if the caller retains yielded rows. A single enormous record can also be large; enforce upload/file/record policies outside this simple teaching parser.
A resumable import needs more than a generator. Define a job identity, immutable source identity or checksum, a row/record identity, validation policy, batch transaction, durable checkpoint, duplicate rule, and a final report. Commit the data and its checkpoint together when they share a database. Decide whether one bad record rejects the whole file or is recorded as a row-level error. Do not silently change that contract halfway through a large import.
9.3 Bound database exports with a high-water mark
Export numeric post metadata in fixed-size batches
PHP Core schema, authorized tenant, writable stream; IDs must fit PHP integers, preferably a 64-bit runtime
<?php
declare(strict_types=1);
/** @param resource $output */
function exportPostMetadata(PDO $db, int $trustedTenantId, $output): void
{
$max = $db->prepare(
'SELECT COALESCE(MAX(id), 0) FROM posts WHERE tenant_id = :tenant'
);
$max->execute(['tenant' => $trustedTenantId]);
$upperId = (int) $max->fetchColumn();
$max->closeCursor();
$page = $db->prepare(
'SELECT id, author_id, created_at FROM posts
WHERE tenant_id = :tenant AND id > :after AND id <= :upper
ORDER BY id ASC LIMIT 500'
);
if (fputcsv($output, ['id', 'author_id', 'created_at'], ',', '"', '') === false) {
throw new RuntimeException('Could not write export header');
}
$lastId = 0;
while (true) {
$page->bindValue(':tenant', $trustedTenantId, PDO::PARAM_INT);
$page->bindValue(':after', $lastId, PDO::PARAM_INT);
$page->bindValue(':upper', $upperId, PDO::PARAM_INT);
$page->execute();
$rows = $page->fetchAll(PDO::FETCH_ASSOC); // At most 500 rows.
$page->closeCursor();
if ($rows === []) {
break;
}
foreach ($rows as $row) {
$written = fputcsv($output, [
(string) $row['id'],
(string) $row['author_id'],
(string) $row['created_at'],
], ',', '"', '');
if ($written === false) {
throw new RuntimeException('Could not write export row');
}
$lastId = (int) $row['id'];
}
}
}
Read the code
The initial maximum ID stops ordinary later inserts from extending the export indefinitely.
fetchAll is bounded by a fixed batch limit here. The dangerous property is unbounded materialization, not the method name alone.
The cursor advances through an immutable primary key; selecting only required columns limits row size.
This is not a repeatable snapshot: concurrent updates or deletes can affect what the export observes. Define and implement snapshot semantics when required.
Only controlled numeric and timestamp fields are exported here. Exporting untrusted text to spreadsheets also needs a CSV/formula-injection policy.
For a private export, prefer a job that writes to authorized private storage and publishes completion metadata. If you stream over HTTP, consider reverse-proxy buffering, disconnects, timeouts, partial files, and session locks. Sending some bytes does not make the operation resumable. Large exports may need an asynchronous interface rather than keeping a web worker occupied for minutes.
CSV quoting solves CSV syntax; it does not necessarily prevent a spreadsheet from interpreting a text cell as a formula. If you export user-authored names, titles, or other text, choose a spreadsheet-safe representation policy and test it with the actual consuming software. Preserve the underlying data contract and do not assume one escaping trick behaves identically in every spreadsheet.
Both paths perform the same summation, making the result a useful correctness check.
Peak allocated memory is a high-water measurement for this process; it is not current retained memory or total system memory.
Separate processes avoid letting the first run’s peak contaminate the second run’s measurement.
The exact time and bytes depend on runtime, configuration, platform, and environment. Record your observations rather than copying a universal speed claim.
Expected result and interpretation
Both sums should be 125000250000.
Elapsed time and peak memory are measurements to collect on your system.
Compiled PHP code for the configured execution environment.
How is deployed code refreshed? Which runtime uses this cache?
Application cache, such as Redis
A computed representation or reusable data with a deliberate lifetime.
What is the key, scope, freshness, invalidation rule, and miss behavior?
HTTP cache
A response under HTTP cache policy.
Is the response public or private? Which headers and variants control reuse?
Per-job or per-request memoization
A value reused during one bounded unit of work.
Can it leak across users if the process lives longer than a request?
A useful feed cache key might include tenant, viewer permission scope, representation version, page cursor, and a version of the feed data. If viewers have different visibility, a tenant-only key can disclose data. After a committed mutation, invalidate or advance the relevant version under a documented freshness policy. TTL limits a key’s lifetime; it does not guarantee immediate freshness. When many requests miss together, bound rebuild work rather than overwhelming the database.
9.6 Worker occupancy explains one capacity constraint
For an ordinary FPM worker handling one request at a time, the request occupies a worker while PHP waits as well as while it computes. A slow network call or session wait can consume capacity even at low CPU usage. The calculator below is an idealized arithmetic illustration using mean occupancy time. It omits CPU competition, database limits, arrival bursts, service-time variance, and other queues.
Illustrative steady-workload model. At or near full utilization, real systems have little headroom and queueing can rise sharply. Benchmark the actual workload.
Collect median, p95, p99, error rate, throughput, queue depth, worker memory, query count, and downstream latency where relevant. State whether caches are warm, which dataset was used, and which workload mix generated the report. A faster average that hides a failing tail or a rising error rate is not a complete improvement.
LAB 25
Build a resumable import with bounded memory
Extend the CSV reader into a job that writes a documented data model.
Generate a synthetic input at several sizes without using private client data.
Validate records and write batches with a durable checkpoint.
Kill the job after a committed batch, restart it, and inspect duplicates.
Define behavior for a malformed record and a changed source file.
Record peak memory, throughput, database work, and final row counts.
Hint
A checkpoint written before its data commits can skip data; a checkpoint written too late can replay data.
Solution approach
Couple data and checkpoint atomically where possible and give rows or operations stable identities. Use an immutable source contract. Keep error reporting bounded as well as the successful path.
Done when
Restart follows the documented duplicate and error policy.
Increasing record count does not require retaining all parsed records.
LAB 26
Improve one measured bottleneck
Choose a feed, export, or import with a reproducible workload.
Write the workload and correctness contract.
Measure before changing anything.
Use query plans, profiling, or instrumentation to identify the bottleneck.
Change one justified mechanism and repeat comparable measurements.
Test cold cache and failure behavior if caching was introduced.
Hint
A guessed bottleneck can move complexity into the wrong layer.
Solution approach
Tie the change to an observed cost, and verify results as well as speed. Report limits and tradeoffs, including extra writes, storage, staleness, or operational dependencies.
Done when
The report includes before/after evidence and comparable conditions.
The changed path preserves the original correctness contract.
LAB 27
Integrated pillar challenge
Grow synthetic data from 10,000 to 100,000 to 1,000,000 rows as your machine permits. Compare complete loading with bounded batches and an export that writes progressively. Record peak PHP memory, process memory, query counts, latency, and errors at several concurrency levels.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
yield avoids constructing the complete output collection in that generator; it cannot undo buffering already performed by a database driver or consumer. MySQL buffered queries can retain a whole result set even if you call fetch() repeatedly. Ordinary FPM has finite request workers, and a worker waiting on a slow database or remote API is still occupied. Performance depends on the entire data path.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Produce a before/after report with hardware, dataset, request mix, concurrency, and correctness checks.
Keep a large import within a chosen memory budget and resume it after interruption.
Reasoning check: Why might replacing fetchAll() with a fetch() loop still fail to make a MySQL export use bounded memory?
The MySQL driver may already buffer the whole result before the fetch loop consumes it. Inspect the driver mode and other materialization stages. A bounded batch query or carefully managed unbuffered result can limit a different layer of memory.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Session or token authentication choices and authorization on every relevant API operation.
HTTP clients, connection and request timeouts, status handling, bounded retries, and backoff with jitter.
Idempotency keys, signed webhooks, deduplication, event order, and durable event acceptance.
Email, payments, object storage, and provider limits using maintained SDKs where appropriate.
10.1 An API is a contract across an unreliable boundary
Specify methods, authentication, authorization, field types, size limits, status behavior, pagination, error shape, and compatibility policy. The consumer should be able to distinguish a rejected operation from an accepted operation whose response was lost. Stable contracts are especially valuable when several clients deploy on different schedules.
Contract decision
Example policy
Reason
Resource representation
Return a stable post ID, UTC timestamp, and documented nullable fields.
Consumers need predictable meaning and types.
Validation error
A consistent code plus field-level details safe to disclose.
Consumers can correct input without parsing changing prose.
Pagination
Bounded size and an opaque cursor carrying the ordering contract.
The server controls cost and the client can continue reliably.
Rate limiting
A documented response and retry policy.
Clients can reduce pressure instead of retrying aggressively.
Compatibility
Additive fields where permitted; deliberate removal/version policy.
Independent releases can coexist.
Operation identity
A scoped idempotency key for selected mutations.
A lost response can be retried without inventing a second business attempt.
A timeout can leave the result unknown to the caller. A timeout does not prove the remote action failed. An operation record and provider-supported idempotency can make recovery more reliable, but the durability boundaries must be designed.
10.2 Bound outbound calls and classify the result
Call a fixed local mock service with timeouts
PHP CLI helper with curl; start a disposable mock at the fixed address; no automatic retries are implemented
<?php
declare(strict_types=1);
function fetchMockStatus(): array
{
// Fixed local lab endpoint. Use the provider's approved HTTPS URL in production.
$handle = curl_init('http://127.0.0.1:8090/status');
if ($handle === false) {
throw new RuntimeException('Could not initialize transport');
}
try {
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 500,
CURLOPT_TIMEOUT_MS => 1500,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($handle);
if ($body === false) {
throw new RuntimeException('Transport failure: ' . curl_error($handle));
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if ($status !== 200) {
throw new RuntimeException("Unexpected remote status {$status}");
}
if (strlen($body) > 64_000) {
throw new RuntimeException('Mock response is too large');
}
$decoded = json_decode($body, false, 16, JSON_THROW_ON_ERROR);
if (!$decoded instanceof stdClass
|| !isset($decoded->state)
|| !is_string($decoded->state)) {
throw new RuntimeException('Invalid remote representation');
}
return ['state' => $decoded->state];
} finally {
unset($handle); // Release this CurlHandle reference.
}
}
Read the code
Connection and total timeouts bound two different waits.
Transport failure and HTTP rejection are distinct observations.
The response shape is checked instead of trusting valid JSON to imply a valid contract.
The body-size check occurs after buffering in this teaching example. For an untrusted or potentially large response, enforce the byte limit during receipt with a bounded transport/stream callback.
Keep TLS peer and hostname verification enabled for real HTTPS calls. Do not turn verification off to conceal configuration problems.
Modern PHP uses a CurlHandle object. The example releases its reference rather than calling curl_close, which has no effect since PHP 8.0 and is deprecated in PHP 8.5.
Retry only when the operation and observed failure support it. A connection failure before sending and a timeout after a possible commit are not equivalent. Use a bounded attempt count and deadline, backoff with jitter, and any provider retry guidance. Respect throttling signals. Retrying an unsafe mutation without an operation identity can multiply effects.
10.3 Design the idempotency record, not just a header
Part of the record
Example
Design responsibility
Scope
tenant_id + operation_name + key
Two clients or two operation types should not accidentally share identity.
Request identity
Digest of a documented canonical request representation
The same key with a different request must not silently reuse the earlier result.
State
Accepted, processing, completed, or a defined failure state
Concurrent duplicates need a deliberate response or wait policy.
Result
Status and safe response data, or durable resource reference
A lost response can be recovered.
Lifetime
A documented retention window
Explain what happens after the key expires.
External correlation
Provider operation key and request/reference ID
Reconcile an uncertain provider outcome.
For a change entirely inside one database, insert or claim the unique operation record and commit the business effect and completed result under a coherent transaction design. For a remote provider, a local transaction cannot atomically include the provider’s database. Persist intent, use the provider’s documented idempotency contract where available, and reconcile unknown outcomes. Do not hold a database transaction open and describe a remote call as part of its rollback guarantee.
After verification, validate the event schema and persist durable receipt or work before acknowledging it according to the provider contract. Deliveries can be repeated and arrive out of order. An old event must not overwrite a newer state simply because it was received later. Where the event is a notification rather than an authoritative complete record, fetch the current provider state through the approved API when needed.
10.5 Error responses and integration observability
A useful error representation includes a stable application code, safe explanatory text, and a correlation ID. Field errors may be appropriate for validation. Stack traces, SQL, access tokens, internal network addresses, and raw provider secrets do not belong in a public response. Internally, record the operation identity, attempt number, provider request ID, latency, and outcome category so an uncertain action can be investigated.
LAB 28
Lose the response after a successful operation
Build a mock mutation API with a scoped operation key.
Commit a synthetic business effect and its result record.
Simulate the response being lost after commit.
Repeat the same key and body, then the same key with a different body.
Send two simultaneous requests with the same key.
Define and test behavior while the first attempt is still processing.
Hint
A cache-only key may disappear at exactly the wrong time.
Solution approach
Use a durable uniqueness and state policy. Keep the business effect and completed result in the same transaction when both are local. Return the documented prior result for a true replay and reject a conflicting request identity.
Done when
One logical operation produces the documented number of effects.
Conflicting key reuse and concurrent duplicates have deterministic policies.
LAB 29
Receive duplicate and out-of-order events
Use the custom signature lab and a local event fixture.
Reject a changed raw body and an expired timestamp.
Deliver the same event ID more than once.
Deliver an older state event after a newer state event.
Crash after durable receipt but before acknowledging.
Document when the receiver can safely acknowledge and how failed processing resumes.
Hint
Arrival order is not necessarily business order, and valid signatures do not remove duplicates.
Solution approach
Separate authenticity, schema validity, durable receipt, ordering policy, and application of the effect. Use event identity and appropriate version/current-state rules rather than unconditional last-arrival-wins updates.
Done when
Invalid signatures do not enqueue trusted work.
Duplicate or old events follow the documented effect policy.
LAB 30
Integrated pillar challenge
Build a JSON API and integrate a local fake payment provider or a provider's test environment. Simulate a timeout after a successful operation. Deliver the same signed event twice and deliver events out of order. Persist accepted work before returning success.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
A request can succeed at the remote service while its response is lost. Retrying the operation without a suitable idempotency design can repeat the side effect. A webhook provider may retry deliveries or deliver events out of order. Signature checks authenticate the payload; they do not by themselves make event processing idempotent.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Make repeated delivery safe and document which operations can be retried.
Produce a clear API contract and show useful behavior when the external provider is slow or unavailable.
Reasoning check: If a payment request times out, how will you distinguish a failed payment from a successful payment whose response was lost?
A timeout alone leaves the outcome uncertain. Use durable operation identity, the provider’s documented idempotency support, correlation/reference IDs, and reconciliation through an approved status or event path. Retry according to that protocol instead of assuming a timeout means no effect.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Concurrency, background jobs, and distributed consistency
Keep work correct when several processes run and some fail.
What to master
FPM concurrency, CLI workers, job queues, schedulers, and concurrency versus parallel execution.
Job reservation, acknowledgments, visibility timeouts, retries, dead-letter handling, and idempotency.
Atomic database operations, uniqueness, locks, lease expiry, and crash recovery.
Dispatch after commit, the remaining database-to-queue failure gap, and transactional outbox patterns.
Persistent worker state, restart behavior, graceful shutdown, bounded memory, and optional fibers or event loops.
11.1 Concurrency exists even when one request is synchronous
One PHP request may execute ordinary code sequentially while many other PHP workers run at the same time. They can compete for database rows, cache keys, files, and provider operations. A queue adds more competing processes; it does not remove the need for invariants. Concurrency reasoning starts with the shared resource and its enforcement boundary.
Mechanism
What it means
Boundary to understand
FPM workers
Several processes can handle different requests concurrently.
Shared database/cache state and each process’s capacity.
Queue workers
Processes receive jobs and often live across many jobs.
Fibers are not an instruction to run ordinary CPU-bound PHP code simultaneously on several cores. Suspending a fiber does not make a blocking database or network call nonblocking by itself. A runtime needs an appropriate scheduler and I/O integration. Understand the deployment model before adopting an asynchronous style.
Consider a worker that applies an effect and then crashes before acknowledging the message. The broker can redeliver it because it cannot know the worker’s application state. A handler must therefore define what a repeat means. At-least-once delivery can be useful, but it requires idempotent processing or durable deduplication at the relevant effect boundary.
Crash point
Possible observation
Required design
Before any durable effect
The job can be delivered again.
Retry with bounded attempts and classification.
After database commit, before acknowledgment
The same event may run again.
Receipt and local effect committed atomically, or an equivalent idempotent operation.
After a remote provider accepts, before local result recording
The local outcome is uncertain.
Provider operation identity and reconciliation.
While holding a claim or lease
Another worker may eventually take over.
Lease expiry, ownership checks, and stale-owner protection appropriate to the effect.
After repeated deterministic failure
The job keeps failing.
Stop automatic retries, preserve evidence, expose a repair/replay path.
11.3 Persist business changes and event intent together
Transactional outbox separates commit from publication. A publisher can crash after sending but before marking the outbox row as published, so publication and consumption must tolerate duplicates. The outbox closes the local change-versus-intent gap, not every distributed failure gap.
The outbox pattern stores the intended event in the same transaction as the local business change. A separate process publishes it later. This avoids committing a post and losing the only record that a notification should be produced. It also avoids pretending that sending to a broker participates in a normal PDO transaction.
This use case explicitly owns the local transaction; callers must not assume an independent nested transaction.
Both the post and event row commit, or both roll back.
The payload contains stable identifiers and a schema version rather than serializing a live PHP object.
No remote publish occurs while this transaction is open.
A retried create request still needs its own operation identity if the client retry must refer to the same post.
A production publisher needs a bounded batch, a claim/ownership policy for multiple publishers, a retry schedule, and visibility into stuck rows. A useful shape is: claim a batch in a short database transaction, send outside that transaction, then mark progress only if the claim still belongs to this publisher. Decide whether per-resource ordering is required. A lease alone does not prevent a stale worker from applying an external effect; use appropriate ownership/fencing or provider idempotency where needed.
11.4 Deduplicate a local effect atomically
Apply one internal event to a tenant counter
PHP MySQL appendix tables; seed one counter row per tenant; trusted validated internal event envelope
<?php
declare(strict_types=1);
function applyPostCreated(
PDO $db,
int $trustedEventTenantId,
string $eventId
): bool {
if (preg_match('/^[a-f0-9]{32}$/D', $eventId) !== 1) {
throw new InvalidArgumentException('Invalid internal event ID');
}
if ($db->inTransaction()) {
throw new LogicException('This handler owns its transaction');
}
$db->beginTransaction();
try {
$receipt = $db->prepare(
'INSERT INTO consumer_receipts
(consumer_name, event_id, tenant_id)
VALUES (:consumer, :event, :tenant)'
);
try {
$receipt->execute([
'consumer' => 'tenant-post-counter-v1',
'event' => $eventId,
'tenant' => $trustedEventTenantId,
]);
} catch (PDOException $failure) {
if ($failure->getCode() === '23000'
&& (int) ($failure->errorInfo[1] ?? 0) === 1062) {
$db->rollBack();
return false; // This consumer already handled this event.
}
throw $failure;
}
$counter = $db->prepare(
'UPDATE post_counters
SET created_count = created_count + 1
WHERE tenant_id = :tenant'
);
$counter->execute(['tenant' => $trustedEventTenantId]);
if ($counter->rowCount() !== 1) {
throw new RuntimeException('Tenant counter is not initialized');
}
$db->commit();
return true;
} catch (Throwable $failure) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $failure;
}
}
Read the code
The unique consumer/event key prevents two successful receipts for this handler identity.
Receipt and counter change are in the same database transaction.
Only the specific MySQL duplicate-key error is treated as a replay. Other integrity errors remain failures.
A counter failure rolls the receipt back so a corrected retry can still apply it.
This guarantees the selected local database effect under these assumptions. Sending an email inside the transaction would not inherit the same guarantee.
The event identity must be trustworthy and bound to the intended tenant. Do not accept arbitrary event IDs and tenant IDs from a public endpoint as trusted internal messages. Receipt retention is also part of the guarantee: deleting the receipt while old deliveries remain possible can let a replay apply again.
11.5 Framework dispatch timing and process lifetime
Dispatch after commit in a Laravel transaction
PHP Laravel fragment with DB facade and a real ShouldQueue job using dispatch traits; not a complete file
DB::transaction(function () use ($actorId, $tenantId) {
// Perform the authorized local write.
GenerateExport::dispatch($actorId, $tenantId)->afterCommit();
});
Read the code
Dispatching after commit avoids a job observing an uncommitted row in a normal setup.
A job receives stable IDs and should apply the documented policy when it runs.
After-commit dispatch is useful timing control. It is not identical to a durable outbox: a process crash after database commit can still leave an external dispatch gap.
Long-lived queue and application workers retain a different lifecycle from ordinary request-local code. Avoid storing a current user, tenant, request, growing collection, or stale container binding in a shared object. Restart workers during releases so new code is loaded, and bound job execution and resource use. The exact reset behavior depends on the runtime.
A still-running job can overlap with a redelivery if timing is wrong.
Set compatible values for the actual broker/runtime and test a slow job.
Bounded retry and backoff
Transient failures need recovery without an infinite storm.
Classify retryable failures and define a terminal repair queue.
Graceful shutdown
A release or termination can interrupt an in-flight job.
Stop taking new work and use the runtime’s supported drain/restart mechanism.
Memory and work limits
Retained state can accumulate over many jobs.
Measure repeated jobs; recycling is a guard, not a substitute for fixing retention.
Idempotent or deduplicated effects
Redelivery is possible at awkward crash points.
Crash after commit and before acknowledgment.
LAB 31
Crash between effect and acknowledgment
Use the counter handler and a durable event fixture.
Deliver the same event twice and check counter and receipt rows.
Force an error after receipt insertion and before the counter change.
Retry after correcting the error.
Crash after commit and simulate redelivery.
Run two consumers concurrently with the same event.
Hint
Receipt and effect must share the atomic boundary for this local guarantee.
Solution approach
The unique receipt key arbitrates concurrent attempts. The transaction prevents a successful receipt without its counter change. Redelivery finds the prior receipt; external effects need their own protocol.
Done when
The counter changes once for a successfully processed event identity.
A rolled-back attempt does not prevent a later valid attempt.
LAB 32
Test the outbox publication gaps
Add a small publisher around the outbox table.
Commit a post while the broker or mock publisher is unavailable.
Confirm that the outbox intent remains visible and retryable.
Simulate a send followed by a crash before published_at is recorded.
Run multiple publishers and test claim expiry.
Describe which ordering and duplicate guarantees the consumer actually provides.
Hint
The publisher cannot usually make its database update and broker acceptance one ordinary local transaction.
Solution approach
Use durable intent, ownership-aware progress, bounded retries, and a duplicate-safe consumer. Include operational inspection and repair tooling instead of hiding stuck events.
Done when
A temporary publication failure does not lose committed event intent.
A duplicate send follows the documented consumer policy.
LAB 33
Integrated pillar challenge
Queue an email digest and a large import. Crash a worker after part of the work is durable and replay the job. Add an outbox record in the same transaction as the business change; interrupt the publisher and verify that work is eventually published without repeating the business effect.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
Multiple requests or workers can modify shared database state even when each PHP process executes one ordinary flow. A worker can perform a side effect and crash before acknowledging its job, so delivery can repeat. Dispatch-after-commit avoids reading uncommitted data, but a crash between database commit and publishing can still lose notification work. An outbox records publication intent with business data in one database transaction. Its relay and consumers still need duplicate handling. Fibers provide cooperative suspension; an async runtime and suitable I/O are needed for useful asynchronous execution.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Explain the difference between a job running twice and its intended business effect occurring twice.
Recover from a crashed worker without silent lost work or inconsistent client data.
Reasoning check: What happens if the database commits successfully but the process dies before it publishes a job to a separate queue?
The change is durable but the queue may never receive the intended work. A same-transaction outbox preserves publishable intent for a separate publisher. Publication can still duplicate after a crash, so the consumer needs an appropriate idempotent effect or durable receipt protocol.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Secrets, file permissions, HTTPS, production error handling, health checks, logs, metrics, and tracing.
Backups, restore drills, incidents, capacity planning, and operational runbooks.
Client requirements, acceptance criteria, data policies, documentation, cost tradeoffs, and maintenance handoff.
12.1 A production release includes the running system
A repository is only part of the product. The deployed runtime also has extensions, configuration, secrets, a database schema, storage permissions, web-server routing, queue workers, scheduled tasks, telemetry, and backups. A professional delivery makes those dependencies explicit and recoverable.
Use compatibility to keep a release reversible. This illustrates an expand-and-contract strategy. Exact sequencing depends on your change. Rolling back application code does not automatically undo a destructive schema migration or restore lost data.
12.2 The public root should be small
An illustrative Nginx front-controller configuration
NGINX Illustration, not a complete server installation; adapt TLS, paths, socket, routing, and tested server policies
server {
listen 443 ssl;
server_name community.example.test;
root /srv/community/current/public;
index index.php;
# Configure TLS certificates and policies for your environment.
client_max_body_size 2m;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /index.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root/index.php;
fastcgi_pass unix:/run/php/community-fpm.sock;
}
# Only the intended front controller can execute as PHP.
location ~* \.php(?:/|$) {
return 404;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Read the code
The document root points to public, keeping application code and secrets out of direct static serving.
Only the exact front controller is forwarded to PHP-FPM.
Private uploads and export files belong outside this public tree and require authorized delivery.
Request size limits must align with PHP, application, and storage limits.
Verify the deployed route and static-file behavior; a configuration example alone is not evidence that the host is correctly configured.
CLI and FPM can load different configuration files and extensions. Verify the running environment, not just the output of one local php command. OPcache settings, code refresh behavior, and worker restarts belong in the release procedure. If timestamp checks are disabled, establish an explicit refresh/restart strategy rather than expecting edited files to appear automatically.
Define recovery needs with the client: acceptable data loss, acceptable outage, retention, and which systems must be restored together. A database backup alone may not contain private attachments, export files, configuration, or the identities needed to decrypt or reconnect services. Test restoration into an isolated environment and verify application behavior and data relationships, not merely that a backup file exists.
For an incident, first identify the affected users and operations, establish a safe containment step, preserve useful evidence, and communicate through the agreed process. After recovery, document cause, contributing conditions, detection gaps, and a concrete prevention or mitigation task. A useful incident report explains why the system behaved that way without inventing certainty.
12.7 Client delivery includes decisions and limits
Deliverable
What it should answer
Acceptance criteria
Which user-visible outcomes and denied actions define completion?
Architecture and data map
Where does data live, who can access it, and which dependencies are authoritative?
Operational runbook
How are releases, rollback, queue repair, and restoration performed?
Capacity evidence
What workload was measured, under which conditions, and where are the limits?
Access and ownership
Who owns domains, credentials, hosting, provider accounts, and support responsibilities?
Maintenance plan
How are supported runtime/dependency upgrades and compatibility changes managed?
Known tradeoffs
Which staleness, retention, availability, and workflow choices were agreed?
LAB 34
Release a compatible change and roll the code back
Use a disposable staging deployment with a database copy containing synthetic data.
Add a compatible field or table and deploy code that can coexist with the previous version.
Run a bounded backfill if needed.
Verify web routes, queue worker revision, permissions, and a smoke workflow.
Trigger the documented rollback criteria in a controlled way.
Roll back code and verify that the retained additive schema is still safe.
Hint
A destructive down migration can turn a code rollback into data loss.
Solution approach
Plan compatibility before activation. Separate schema expansion, data migration, behavior changes, and later removal. Measure and verify both the forward and recovery paths.
Done when
The old version still works during the declared compatibility period.
The procedure states what rollback can and cannot recover.
LAB 35
Restore the capstone and hand it to another engineer
Prepare a synthetic backup and a concise operational handoff.
Restore database and private storage into a fresh isolated environment.
Recreate configuration using documented secret provisioning.
Verify tenant isolation, a post workflow, export access, and queue recovery.
Have another person follow the setup/runbook without your undocumented local assumptions.
Record restore duration, missing steps, and ownership of follow-up fixes.
Hint
Success means the application and its critical data work, not just that a restore command returned zero.
Solution approach
Use observable acceptance checks and keep the runbook tied to the release. Include what to inspect when the web application is healthy but background work is stalled.
Done when
The restored system passes the selected user and data checks.
The handoff exposes environment assumptions and recovery responsibilities.
LAB 36
Integrated pillar challenge
Deploy to a staging environment. Run a compatible migration, restart persistent workers, and verify health. Perform a deliberate failed-release rollback and a database restore drill. Give another person the setup guide, API contract, and incident runbook.
State the input, output, permission, and failure contracts relevant to this challenge.
Build the smallest useful baseline and reproduce its behavior.
Collect evidence from the relevant runtime, request, database, or process boundary.
Inject at least one malformed input or failure, then verify the intended invariant.
Write a short explanation of the mechanism and the remaining limits.
Hint
A release must coordinate application code, dependencies, compiled caches, database schema, and running workers. A worker can continue using previously loaded code until it is restarted. A backup is useful only if the recovery procedure works and the recovered data satisfies the agreed recovery target. Client requirements become measurable contracts about behavior, access, performance, and support.
Solution approach
Use the chapter’s worked examples as building blocks. Integrate them under one explicit contract rather than copying them into an unreviewed controller. Explain which boundary enforces each rule and retain the evidence required by the mastery checks.
Done when
Recover from a bad release and restore data in a demonstrated procedure.
Explain the system's limits and operating cost to a client using observed evidence.
Reasoning check: Why might changing the files on disk fail to update an already-running queue worker?
A persistent worker has already loaded code and may keep the same application state. Replacing files does not necessarily replace that process or its loaded code. Use the runtime’s supported restart/drain mechanism and verify the new revision is actually processing jobs.
Mastery evidence
Check these after you can demonstrate and explain the behavior.
Build one coherent system and use it to prove all 12 pillars. Users can belong to multiple client organizations. Within each organization they can publish posts, comment, follow other members, import a defined dataset, and request private exports. Administrators have explicit tenant-scoped privileges. Background jobs produce digests and exports. A JSON API and a local fake external provider exercise integration behavior.
Begin with a modular application and one relational database. Introduce a cache or queue when its purpose and failure model are clear. The learning goal is a correct, measured, explainable system; infrastructure count is not a mastery score.
Capstone boundaries and authoritative state. The database is authoritative for membership and local business state in this design. Private storage, worker execution, and a provider require their own access, lifecycle, and recovery contracts.
A suggested module map
Module
Responsibilities
Boundary tests
Identity and membership
Authentication, memberships, role transitions, recovery/session policy.
Bad release, stale worker, database restore, stuck job.
Extend the relational schema deliberately
Start with the tenants, users, memberships, and posts tables in pillar 6. The following appendix adds the relationships and internal event tables used by later examples. Run it only in the disposable learning database after that core schema. It is a teaching schema: choose retention, deletion, indexing, and application policies through the project requirements.
Capstone relationships and internal event support
SQL MySQL 8.4/InnoDB appendix; no cascade policy is chosen automatically
-- Required for a standard composite foreign-key target.
ALTER TABLE posts
ADD CONSTRAINT uq_posts_tenant_id UNIQUE (tenant_id, id);
CREATE TABLE comments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
post_id BIGINT UNSIGNED NOT NULL,
author_id BIGINT UNSIGNED NOT NULL,
body TEXT NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
CONSTRAINT fk_comments_post
FOREIGN KEY (tenant_id, post_id) REFERENCES posts (tenant_id, id),
CONSTRAINT fk_comments_author
FOREIGN KEY (tenant_id, author_id) REFERENCES memberships (tenant_id, user_id),
INDEX comments_page (tenant_id, post_id, id)
) ENGINE=InnoDB;
CREATE TABLE follows (
tenant_id BIGINT UNSIGNED NOT NULL,
follower_id BIGINT UNSIGNED NOT NULL,
followed_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (tenant_id, follower_id, followed_id),
CONSTRAINT fk_follows_follower
FOREIGN KEY (tenant_id, follower_id) REFERENCES memberships (tenant_id, user_id),
CONSTRAINT fk_follows_followed
FOREIGN KEY (tenant_id, followed_id) REFERENCES memberships (tenant_id, user_id),
CONSTRAINT ck_follows_distinct CHECK (follower_id <> followed_id)
) ENGINE=InnoDB;
CREATE TABLE outbox_events (
event_id CHAR(32) CHARACTER SET ascii COLLATE ascii_bin PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
event_type VARCHAR(80) NOT NULL,
payload JSON NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
published_at DATETIME(6) NULL,
claimed_until DATETIME(6) NULL,
claim_token CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NULL,
CONSTRAINT fk_outbox_tenant FOREIGN KEY (tenant_id) REFERENCES tenants (id),
INDEX outbox_pending (published_at, created_at, event_id)
) ENGINE=InnoDB;
CREATE TABLE consumer_receipts (
consumer_name VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
event_id CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
handled_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (consumer_name, event_id),
CONSTRAINT fk_receipt_tenant FOREIGN KEY (tenant_id) REFERENCES tenants (id)
) ENGINE=InnoDB;
CREATE TABLE post_counters (
tenant_id BIGINT UNSIGNED PRIMARY KEY,
created_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
CONSTRAINT fk_counter_tenant FOREIGN KEY (tenant_id) REFERENCES tenants (id)
) ENGINE=InnoDB;
CREATE TABLE inventory (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
available INT UNSIGNED NOT NULL,
CONSTRAINT fk_inventory_tenant FOREIGN KEY (tenant_id) REFERENCES tenants (id),
CONSTRAINT ck_inventory_available CHECK (available >= 0)
) ENGINE=InnoDB;
Read the code
Composite foreign keys bind child relationships to the same tenant as their parent membership or post.
The explicit unique parent key avoids relying on deprecated nonstandard foreign-key targets.
The follow primary key prevents duplicate relationships; the check rejects following yourself.
Internal event IDs are 32 hexadecimal characters in the PHP examples, compared with a binary ASCII collation.
The receipt key is global for a given consumer and internal event ID. Internal events must have trustworthy globally unique identity; external providers may require a different schema and scope.
Restrictive foreign keys intentionally make deletion decisions explicit. Archival, anonymization, retention, or cascading needs a coordinated project policy.
[73]Seed synthetic organizations and initialize the counter lab
SQL Fresh local schema only; this is fixture data, not a repeatable migration or production seed
INSERT INTO tenants (id, name) VALUES
(1, 'Example Organization A'),
(2, 'Example Organization B');
-- Register users through the application using password_hash,
-- then add their actual IDs to memberships with an explicit role.
-- Example role contract for these labs: member or admin.
INSERT INTO post_counters (tenant_id, created_count) VALUES (1, 0), (2, 0);
INSERT INTO inventory (tenant_id, available) VALUES (1, 1);
Define operation identity when you add the mutation API
A possible local operation-record schema
SQL A design starting point: implement and test canonical request hashing, state transitions, retention, and concurrency policy
CREATE TABLE api_operations (
tenant_id BIGINT UNSIGNED NOT NULL,
operation_name VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
operation_key VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
request_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
state VARCHAR(20) NOT NULL,
result_json JSON NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (tenant_id, operation_name, operation_key),
CONSTRAINT fk_operation_tenant FOREIGN KEY (tenant_id) REFERENCES tenants (id)
) ENGINE=InnoDB;
Read the code
The key is scoped by tenant and operation name.
Application validation must bound key syntax/length and define what request_hash includes.
The schema alone does not implement idempotency. A correct claim, effect, result, retry, and recovery protocol must use it.
A remote provider still has an independent atomicity boundary.
Capstone milestones and evidence
Milestone
Build
Evidence to keep
1 · Plain PHP vertical slice
One validated read and write path using explicit response contracts.
Predicted/actual outputs and a request trace.
2 · Tenant-scoped persistence
Membership, posts, comments, constraints, and policy.
Permission matrix, SQL plans, concurrency and rollback observations.
3 · Composer and framework structure
Namespaced components, tests, analyzer, one traced framework feature.
Locked install, test results, explanation of framework boundaries.
Rate each pillar using the same evidence ladder. A checklist reflects your assessment, so keep the experiment, test, trace, or report that supports it. Strong performance in one pillar should not hide a missing security or recovery boundary in another.
Level
Observable capability
Example
0 · Unfamiliar
Cannot yet explain the main mechanism.
Unsure where request data enters the application.
1 · Recognize
Can describe terms and follow a working example.
Can run the typed lookup and identify validation versus lookup.
2 · Implement
Can build the feature with reference documentation.
Can add a scoped endpoint and its relevant tests.
3 · Diagnose
Can reproduce a failure, explain its cause, and verify a repair.
Can isolate a session lock, missing index, race, or duplicate effect.
4 · Operate and teach
Can handle adverse conditions, justify tradeoffs, and make the result maintainable.
Can demonstrate bounded processing, recovery, and a handoff another engineer follows.
Aim for level 3 or 4 across all pillars for this handbook’s informal 9/10 target. Keep a small portfolio of explanations: a request/runtime trace, a security matrix, a query improvement report, a crash/replay report, and a release/restore runbook. Real production judgment grows through repeated feedback and varied systems.
A practical study session
Choose one rule or mechanism rather than reading many chapters passively.
Predict the result in writing.
Run a small valid case, then one carefully chosen edge case.
Inspect the enforcing boundary and collect a concrete observation.
Change the environment or input enough to test the hidden assumption.
Write a concise explanation and an acceptance check someone else can repeat.
Questions that reveal shallow understanding
Which values are untrusted at this point, and who established authority?
What survives after this request or job ends in the actual runtime?
Which component buffers the data, and what makes that amount bounded?
Which operation is atomic, and what lies outside that transaction?
What happens if this line succeeds and the next line never runs?
What is repeated on a retry, and which identity prevents an unwanted repeat?
What evidence supports the claimed performance and its limits?
Can another engineer restore and operate the system from the documented state?
Keep the vocabulary precise
Glossary
Term
Meaning in this handbook
SAPI
The interface connecting PHP execution to an environment such as CLI or a web server/FastCGI service.
Zend Engine
PHP’s core language execution machinery, including compilation and execution behavior.
Opcode
An instruction in the engine’s compiled representation of PHP code.
OPcache
A cache of compiled PHP code; its lifecycle and refresh rules depend on configuration.
JIT
Compilation of selected execution work into native machine code under a configured runtime; benchmark suitability for the workload.
zval
The engine’s value container concept; exact internal layout is version-specific.
Reference counting
Tracking ownership of selected shared values as part of memory management.
Copy-on-write
Sharing suitable storage until a mutation requires separation rather than eagerly copying everything.
PHP reference
An aliasing relationship between variables, not a universal synonym for an object handle or raw pointer.
Reachability
Whether a value remains accessible through live program roots and references.
Cyclic garbage collection
Reclaiming eligible unreachable cycles; it does not clear intentionally retained data.
FPM
FastCGI Process Manager: a way to manage PHP worker processes for web requests.
Persistent worker
A process/application lifecycle that can retain booted state across multiple requests or jobs.
Type coercion
Conversion between types under applicable language rules.
strict_types
A per-file declaration affecting relevant scalar type coercion; it is not request validation.
Ordered map
PHP array behavior combining keyed lookup with order; keys and storage have language-specific rules.
Closure
A function value that can carry captured surrounding state.
Generator
An iterator produced by a function that yields values without necessarily building a full result collection.
Readonly
A restriction on property reassignment under PHP’s rules, not automatic deep immutability of referenced objects.
Synchronous call
The caller waits for the operation to complete before continuing that execution path.
Fiber
A cooperatively suspendable execution context; overlapping I/O requires a suitable scheduler and I/O integration.
Middleware
Code around a request-handling pipeline, often handling concerns before and after the next handler.
Front controller
The common entry point that routes web requests into an application.
Session lock
A coordination lock in the configured session handler that may serialize requests sharing a session.
Dependency injection
Providing collaborators to a component instead of having it locate or construct every dependency itself.
Value object
An object representing a meaningful value and its invariants, rather than a persistence identity.
Composition root
The location where an application wires its concrete collaborators and starts the use case or transport.
Adapter
A component translating an external mechanism or protocol into the contract the application needs.
PSR
A PHP-FIG interoperability recommendation; applicability depends on the particular specification.
PSR-4
A namespace-to-file autoloading convention used by Composer and many PHP libraries.
Lock file
The recorded resolved dependency graph used for repeatable application installs.
Prepared statement
A database statement that separates bound data values from SQL structure under driver-specific behavior.
Index
A maintained access structure that can accelerate suitable queries while adding storage and write costs.
Composite index
An index ordered by several key columns; leading columns matter to usable access patterns.
Keyset pagination
Continuing an ordered result after a stable cursor rather than skipping a growing offset.
N+1 query pattern
A base query followed by one additional query per result or relationship access.
Transaction
A local database boundary for coordinated commit/rollback under the database’s guarantees.
Isolation level
Rules governing interactions and visibility between concurrent database transactions.
Deadlock
A cycle of incompatible lock waits that the database resolves by aborting an involved operation/transaction.
Invariant
A rule that must remain true across permitted state changes and failures.
Tenant
A client/organization scope in a shared application; authority must establish access to that scope.
Authentication
Establishing the identity of a principal under the application’s policy.
Authorization
Deciding whether a principal may perform an operation on a resource.
CSRF
Cross-site request forgery: abuse of browser-carried credentials to induce an unintended request.
XSS
Cross-site scripting: untrusted content being interpreted as executable browser content.
SSRF
Server-side request forgery: induced outbound server requests reaching unintended destinations.
Static analysis
Examining code and type information without relying on executing every path.
Regression test
A test preserving the expected behavior associated with a discovered or anticipated failure.
p95 latency
A percentile summarizing the observed latency distribution; its meaning depends on the measured workload and sampling.
Backpressure
A way to constrain or signal incoming work when a downstream stage cannot keep up.
TTL
Time to live: a configured lifetime/expiry concept; it does not by itself prove current business truth.
Cache stampede
Many misses triggering simultaneous rebuild work for the same representation.
Idempotency
A property or application protocol in which repeats of a defined operation preserve its intended logical effect.
At-least-once delivery
A delivery model permitting repeated deliveries in order to support eventual receipt under its assumptions.
Acknowledgment
A signal that work was accepted or handled according to a protocol; the exact durability meaning must be defined.
Transactional outbox
Persisting event intent beside a local business change in the same database transaction.
Consumer receipt
A durable record that a named handler processed a given event identity.
Lease
Time-bounded ownership; expiry can permit takeover, so stale owners still need an appropriate safety protocol.
Reconciliation
Comparing uncertain or divergent state with authoritative records and applying a defined repair.
High-water mark
A boundary captured for a bounded scan; it does not necessarily create a consistent snapshot.
RPO
Recovery point objective: the agreed target for acceptable data loss, often expressed as time.
RTO
Recovery time objective: the agreed target for restoring the relevant service after disruption.
Expand and contract
A staged compatibility approach: add support, migrate usage/data, then remove the old form later.
Runbook
Concrete instructions and checks for operating, investigating, or recovering a system.
Primary-source reading
Sources and further investigation
Research checked on 9 October 2026. The handbook uses the PHP manual and internals references, Composer and PHP-FIG specifications, framework/tool documentation, MySQL and Redis references, OWASP guidance, HTTP specifications, and provider documentation. Numbered references connect mechanisms to the relevant source. The explanations, examples, exercises, and assessment framework are an original educational synthesis.
Some live documentation URLs redirect to a newer version. Match the documentation to your deployed runtime, database, framework, and tool versions. The PHP Internals Book contains version-specific material; use the relevant PHP source revision when inspecting exact engine structures. For production decisions, validate the mechanism against your actual environment and workload.