PHP Mastery
Production engineering · A complete study handbook

Understand PHP.
Build systems that hold up.

12 pillars from language semantics and runtime internals to large datasets, client boundaries, failure recovery, and real-world delivery.

12Mastery pillars
52Code examples
36Practical labs
12Embedded diagrams

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

  1. Read and predict: write down expected outputs or behavior before running an example.
  2. Implement: reproduce the smallest working path using the stated setup.
  3. Inspect: look at input types, stack frames, memory, SQL, requests, logs, and process lifetime.
  4. Break and repair: inject malformed input, a race, a timeout, or a crash and verify the intended invariant.
  5. 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.

[71][72]
Label or environmentWhat you needHow to interpret the example
Standalone CLIPHP CLI and any extension named in the caption.Save the PHP block as a file and run it with php. Each standalone file is independent.
HTTP exampleA local web environment or explicitly configured HTTPS setup.Superglobals, sessions, and headers depend on a real request. CLI execution is not equivalent.
Database helperMySQL 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 projectThe pillar 5 autoload setup and development dependencies.File paths and namespaces are part of the contract.
Framework fragmentA matching Laravel application, named classes, and configured services.Wire the fragment into the actual framework lifecycle and test its behavior.
Configuration illustrationYour 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

The curriculum

12 pillars, one connected system

Progress by demonstrated capability

StagePrimary pillarsEvidence before moving forward
A · Predict and trace1–3; begin security and testing immediately.A small plain-PHP CLI/web application with validated inputs and an explained request/runtime trace.
B · Build correct features4–8; use Composer, MySQL, and one framework.A tested tenant-scoped feature with constraints, policy, and understandable responsibilities.
C · Survive scale and failure9–11.A measured bounded import/export plus replay-safe integration and crash-recovery evidence.
D · Operate and deliver12, 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

QuestionExample answer for the capstoneWhat 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

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
<?php
declare(strict_types=1);

function positiveId(mixed $raw): int
{
    if (!is_string($raw) || !preg_match('/^[1-9][0-9]*$/D', $raw)) {
        throw new InvalidArgumentException('Use a positive decimal ID.');
    }
    $id = filter_var($raw, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1],
    ]);
    if ($id === false) {
        throw new InvalidArgumentException('The ID is outside the integer range.');
    }
    return $id;
}

/** @param array<int, array{name: string, department: string}> $students */
function findStudent(array $students, int $id): ?array
{
    return $students[$id] ?? null;
}

$students = [
    1001 => ['name' => 'Rabin', 'department' => 'Computer Science'],
    1002 => ['name' => 'Anish', 'department' => 'Business'],
    1003 => ['name' => 'Anmol', 'department' => 'Computer Science'],
    1004 => ['name' => 'Maya', 'department' => 'Nursing'],
    1005 => ['name' => 'Sita', 'department' => 'Tourism'],
    1006 => ['name' => 'Alex', 'department' => 'Engineering'],
];

try {
    $id = positiveId($argv[1] ?? null);
    $student = findStudent($students, $id);
    echo $student === null
        ? "Student not found.\n"
        : $student['name'] . ' — ' . $student['department'] . "\n";
} catch (InvalidArgumentException $e) {
    fwrite(STDERR, $e->getMessage() . "\n");
    exit(1);
}
Read the code
  1. The first function accepts mixed because the boundary may contain missing or malformed input.
  2. The regular expression defines an intentionally narrow policy: no sign, leading zero, decimal point, or surrounding spaces.
  3. The integer filter checks range after the representation check. A huge digit string must not silently become a different ID.
  4. 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.
  5. The CLI wrapper decides how to show errors. In an HTTP application, the wrapper would instead choose a response status and body.
Expected result and interpretation
php student.php 1001 → Rabin — Computer Science
php student.php 9999 → Student not found.
php student.php abc  → validation error and exit code 1

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.

[1]
Predict these values before running
PHP Standalone CLI; each output answers a different question
<?php
declare(strict_types=1);
$row = ['nickname' => null];
var_dump('42' == 42);
var_dump('42' === 42);
var_dump((bool) '0');
var_dump((bool) 'false');
var_dump(isset($row['nickname']));
var_dump(array_key_exists('nickname', $row));
Read the code
  1. isset() is false for a missing value and for an existing null value.
  2. array_key_exists() asks whether the key exists, independently of whether its value is null.
  3. Choose the operation that matches your domain: “the field was supplied” and “the field has a non-null value” are different rules.
Expected result and interpretation
bool(true)
bool(false)
bool(false)
bool(true)
bool(false)
bool(true)

1.3 Scope, closures, and iteration

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.

[2]
Closure capture and mutation
PHP Standalone CLI; no framework
<?php
declare(strict_types=1);
$base = 3;
$doubleBase = fn (): int => $base * 2;
$base = 20;

$counter = 0;
$next = function () use (&$counter): int {
    return ++$counter;
};

echo $doubleBase(), "\n";
echo $next(), ' ', $next(), ' ', $counter, "\n";
Read the code
  1. The arrow function captured the earlier value of $base.
  2. The closure aliases $counter; its calls update the same outer variable.
  3. 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.

[3]

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
  1. The function defines the accepted domain and checks before multiplication.
  2. The result is 3,897 minor units. Formatting a display price is a separate operation.
  3. 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.

  1. Accept an ID from CLI first, then from an HTTP query parameter.
  2. Test missing input, an array input, zero, a negative number, decimal text, spaces, an oversized integer, and an unknown valid ID.
  3. Decide whether leading zeros are allowed and document the policy.
  4. 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.

  1. Compare null, false, 0, "0", "", "false", and an empty array using selected conditions.
  2. Record your prediction before execution.
  3. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: PHP language reference[74] · Type declarations[75] · Arrays[76]

PILLAR 02

Execution engine, memory, and process lifetime

Explain what executes your code and where its state lives.

What to master

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 executionThe cache branch changes how executable code is obtained. It does not remove the need to execute the application.misshitFPM receives a requestA worker owns this executionValid opcode cache?A code-cache decisionParse and compileSource → Zend opcodesRead cached opcodesCompilation can be skippedExecute applicationBusiness code and I/O stillrun
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
php --ini
php -m
php -r 'echo PHP_SAPI, " ", PHP_VERSION, PHP_EOL;'
Read the code
  1. php -v identifies the CLI runtime. php --ini shows its configuration files.
  2. php -m shows loaded modules. A missing pdo_mysql driver explains a connection failure even when PDO itself exists.
  3. 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.

[7][8]
Array values, aliases, objects, and clones
PHP Standalone CLI; each case has a different mutation relationship
<?php
declare(strict_types=1);
$original = ['views' => 1];
$copy = $original;
$copy['views'] = 2;
echo $original['views'], "\n";

$alias =& $original;
$alias['views'] = 3;
echo $original['views'], "\n";

$post = new stdClass();
$post->views = 1;
$samePost = $post;
$samePost->views = 4;
echo $post->views, "\n";

$separatePost = clone $post;
$separatePost->views = 9;
echo $post->views, ' ', $separatePost->views, "\n";
Read the code
  1. The first array mutation changes the separate array value.
  2. The explicit reference aliases the original variable.
  3. Both object variables identify the same object until a new object is created with clone.
  4. 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
<?php
declare(strict_types=1);
function report(string $label): void
{
    printf("%s: %d bytes\n", $label, memory_get_usage());
}
$a = range(1, 100000);
report('after creation');
$b = $a;
report('after assignment');
$b[0] = 999;
report('after mutation');
unset($b);
report('after unset');
echo $a[0], "\n";
Read the code
  1. Assignment can share the underlying array storage; mutation can require separation.
  2. The exact measurements depend on build, allocator, loaded extensions, and other live values.
  3. 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.

[10][11]
StateOrdinary FPM requestsPersistent application or queue worker
Local variablesOrdinary request data is cleaned up at request end.Per-call locals end normally, but references retained by persistent services can remain.
Static application accumulatorOrdinary userland request state normally resets between requests.Can grow across jobs or requests in the same application lifetime.
Database contentsDurable state lives outside the PHP request.Still external state; transaction boundaries remain essential.
OPcache codeShared compiled-code structures can outlive a request.Availability and behavior depend on the SAPI and configuration.
Persistent connection or extension allocationMay 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
  1. The static array still reaches every event. Cycle collection cannot infer that you no longer want them.
  2. A bounded buffer, per-job collector, explicit reset, or external logging changes the retention policy.
  3. 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.

  1. Assign and mutate an array, an explicit reference, an object, and a cloned object.
  2. Add a nested object inside the array and repeat the experiment.
  3. 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.

  1. Run the accumulator example in one CLI process.
  2. Create a local web endpoint that returns getmypid() and a request-local counter.
  3. Repeat requests under your FPM setup and record worker IDs.
  4. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: OPcache[6] · FPM configuration[58] · zval structure[7] · Memory management[8] · Persistent applications[67]

PILLAR 03

HTTP and the web request lifecycle

Trace a request from browser input to a correct response.

What to master

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 decisionsRouting and middleware can stop the request before business work. External results return through application code before the response is completed.rejectedacceptedthrough applicationHTTP requestMethod, URL, headers, bodyRoute and middlewareParse input and identifycallerClient-error responseInvalid input or denied accessApplication serviceEnforce the business ruleDatabase or integrationPersistent or external workHTTP responseStatus, headers, body
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.
ConceptMeaningPHP consequence
Query parametersValues in the request target after a question mark.Read and validate $_GET; duplicate or bracketed keys can have surprising shapes.
Form bodyTypically URL-encoded or multipart form data.PHP normally exposes supported form POST data in $_POST; uploads have $_FILES metadata.
JSON bodyA representation with application/json content type.Read php://input and decode JSON explicitly.
Response headerMetadata such as content type, location, and cookies.Set before body output has been committed.
Response statusThe 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.

[12][13]

3.2 A complete small JSON endpoint

Parse, validate, and return JSON
PHP HTTP file api.php; requires mbstring; this stateless demonstration performs no persistent write
<?php
declare(strict_types=1);

function respond(int $status, array $body): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    exit;
}

if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
    header('Allow: POST');
    respond(405, ['error' => 'method_not_allowed']);
}
$type = strtolower(trim(explode(';', $_SERVER['CONTENT_TYPE'] ?? '')[0]));
if ($type !== 'application/json') {
    respond(415, ['error' => 'json_required']);
}
$raw = file_get_contents('php://input', false, null, 0, 8193);
if ($raw === false) {
    respond(500, ['error' => 'body_read_failed']);
}
if (strlen($raw) > 8192) {
    respond(413, ['error' => 'body_too_large']);
}
try {
    $input = json_decode($raw, false, 16, JSON_THROW_ON_ERROR);
} catch (JsonException) {
    respond(400, ['error' => 'invalid_json']);
}
if (!$input instanceof stdClass) {
    respond(422, ['error' => 'object_required']);
}
$input = get_object_vars($input);
if (array_diff(array_keys($input), ['name']) !== []) {
    respond(422, ['error' => 'unexpected_fields']);
}
$name = $input['name'] ?? null;
if (!is_string($name) || trim($name) === '' || mb_strlen($name, 'UTF-8') > 100) {
    respond(422, ['error' => 'invalid_name']);
}
respond(200, ['greeting' => 'Hello, ' . trim($name)]);
Read the code
  1. The endpoint rejects unsupported methods and representations before interpreting the payload.
  2. 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.
  3. JSON syntax, object shape, allowed fields, field type, and domain length are independent checks.
  4. 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
  1. The first request should return 200 with a JSON greeting.
  2. The GET request should return 405 and an Allow header.
  3. 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.

[14]

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
  1. The session update finishes before the delay, so a locking handler can release its lock.
  2. After closing, do not assume later writes to $_SESSION will be persisted automatically.
  3. A single-process development server can still serialize requests for a different reason. Identify that confounder before diagnosing session locking.
[15]

3.4 Response semantics and buffering

OutcomeTypical statusDesign note
Read succeeds200Return the documented representation.
Resource created201Return or identify the newly created resource.
Work durably accepted202Expose how its eventual status can be checked.
No response representation204Do not send a response body.
Malformed representation400Separate from valid syntax with invalid fields.
Authentication needed / access denied401 / 403Document the authentication scheme and denial policy.
Conflicting state409For example, a key reused for a different operation.
Rate limited / temporarily unavailable429 / 503A 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.

[16]

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.

[17]
LAB 07

Read the protocol evidence

Use the JSON endpoint to inspect request and response details.

  1. Send a correct request, an unsupported method, malformed JSON, an unexpected field, and a wrong field type.
  2. Record status, Content-Type, body, and the validation stage for each case.
  3. 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.

  1. Measure concurrent requests with the lock held during a delay.
  2. Close the session before the delay and measure again.
  3. Repeat with separate session cookies.
  4. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: Session locking[15] · Request lifecycle[26] · HTTP middleware standard[77]

PILLAR 04

Object design and application architecture

Make business rules understandable and changeable.

What to master

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 boundariesTwo input mechanisms use the same service. The service depends on a persistence contract, while an adapter owns SQL.valid valuestore postimplementationHTTP controllerParse protocol and identityCLI commandParse command argumentsCreatePost serviceEnforce application rulesTitle value objectOwn the title invariantPostStore interfaceDescribe persistence needsPDO adapterExecute SQL and map results
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
  1. Construction establishes the invariant once. Callers receive an already-valid title.
  2. The readonly class prevents property reassignment; this object stores a string, so nested mutable-object concerns do not arise.
  3. 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
  1. The constructor declares the persistence dependency; the service does not secretly create a connection.
  2. The memory implementation is a demonstration adapter. A real database adapter must preserve constraints and transaction behavior.
  3. An interface earns its place by describing a boundary you can test or replace.
  4. 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.

[18][19]

4.4 Transaction ownership and side effects

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.

ProblemUseful boundaryQuestion to ask
Business rule duplicated in controllersApplication service or domain operationCan both HTTP and import paths call the same rule?
SQL repeated across routesPersistence adapter or focused repositoryCan the adapter preserve the transaction contract?
Provider SDK types spread through the domainIntegration adapterCan the provider change without changing business values?
Everything depends on a global containerExplicit constructor or method dependenciesCan a reader see what this class needs?
Many one-line abstractionsSimpler code or a clearer real boundaryWhat 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.

  1. List each responsibility and its input/output.
  2. Move the business rule into one service.
  3. Inject the persistence and notification collaborators.
  4. Call the same service from a CLI command.
  5. 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.

  1. Implement a recording fake for tests.
  2. Implement a PDO adapter after studying pillar 6.
  3. Run the same service behavior against both adapters.
  4. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: Objects and references[78] · Service container[18]

PILLAR 05

Composer, standards, and framework literacy

Work productively in a maintained PHP codebase.

What to master

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 fileTransitive constraints must agree. The lock file records a concrete result so another installation can reproduce the chosen dependency versions.Application requirementscomposer.json constraintsLibrary ADepends on a shared packageLibrary BAlso depends on that packageComposer resolvesversionsCompatible constraints +platformLocked dependency setReused by composer install
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.

[20]
A minimal application manifest
JSON Save as composer.json; development dependencies can be added with Composer
{
  "name": "community/php-learning",
  "type": "project",
  "require": {
    "php": "^8.4",
    "ext-pdo": "*",
    "ext-pdo_mysql": "*",
    "ext-mbstring": "*"
  },
  "autoload": {
    "psr-4": {
      "Community\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Community\\Tests\\": "tests/"
    }
  }
}
Read the code
  1. The PHP constraint defines compatibility; it does not install the PHP binary.
  2. Extension requirements declare runtime facilities the application needs.
  3. 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
  1. The import supplies a convenient class name; it does not load the file itself.
  2. Composer registers the autoload logic. When the class is needed, the runtime invokes an autoloader.
  3. The PSR-4 mapping tells the loader how to locate the class file.
[21]
A deliberate dependency workflow
SHELL Run in a project you control; choose tool releases compatible with your PHP version
composer validate --strict
composer install
composer require --dev "phpunit/phpunit:^12.5" "phpstan/phpstan:^2.0"
composer audit
composer check-platform-reqs
composer dump-autoload --optimize

# Build a production dependency set from the committed lock:
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
composer check-platform-reqs --no-dev
Read the code
  1. validate checks the manifest and lock relationship.
  2. audit checks known dependency advisories; it is one input to dependency maintenance.
  3. check-platform-reqs verifies real runtime requirements, including extensions.
  4. The production command omits development packages and optimizes autoloading. Build steps and trusted Composer scripts must still be reviewed.
[22]

5.3 Standards give libraries a shared vocabulary

StandardShared contractUseful consequence
PSR-4Class autoloadingLibraries can be arranged and loaded predictably.
PSR-3Logger interfaceApplication code can depend on a logging contract.
PSR-7HTTP message interfacesMiddleware and clients can share message abstractions.
PSR-15Server request handlers and middlewareHTTP processing can be composed through compatible interfaces.
PSR-11Container interfaceContainer entries and errors use a common contract.
PSR-18HTTP client interfaceClients 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.

[23][24][25]

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.

[26]
LAB 13

Prove an installation is reproducible

Create the minimal Composer project and its namespaced class.

  1. Generate the autoloader and run bin/demo.php.
  2. Commit the manifest and lock in your own project.
  3. Create a clean checkout or copy without vendor/ and run composer install.
  4. Compare the resolved dependency versions and verify runtime extensions.
  5. 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.

  1. Locate the application entry point.
  2. Trace routing, middleware, controller construction, validation, service, and response.
  3. Set a breakpoint or add temporary local tracing.
  4. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: Composer basic usage[20] · PSR-4[21] · Laravel lifecycle[26]

PILLAR 06

Relational data, transactions, and query design

Keep persistent data correct and queries efficient.

What to master

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
  1. A user can belong to several organizations through memberships.
  2. The composite foreign key prevents a post author from being linked through a membership belonging to a different tenant.
  3. The feed index matches a common tenant-filtered ordering. It is a starting design to validate with actual queries.
  4. 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
<?php
declare(strict_types=1);
function connectDatabase(): PDO
{
    $dsn = getenv('APP_DB_DSN');
    $user = getenv('APP_DB_USER');
    $password = getenv('APP_DB_PASSWORD');
    if ($dsn === false || $user === false || $password === false) {
        throw new RuntimeException('Database configuration is missing.');
    }
    $db = new PDO($dsn, $user, $password, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
    $db->exec("SET time_zone = '+00:00'");
    return $db;
}
Read the code
  1. For MySQL, the DSN can use mysql:host=127.0.0.1;dbname=community;charset=utf8mb4.
  2. Error mode, fetch mode, and prepare behavior are explicit rather than accidental defaults.
  3. Store credentials in your environment or secret system. Do not print the password while diagnosing a connection.
  4. Connection establishment, authentication, and query execution are separate failure stages.
Read a resource within trusted organization scope
PHP Requires the core schema; caller derives tenant membership and policy before calling
<?php
declare(strict_types=1);
function findPost(PDO $db, int $trustedTenantId, int $postId): ?array
{
    $stmt = $db->prepare(
        'SELECT id, author_id, title, body, created_at
         FROM posts WHERE tenant_id = :tenant AND id = :id'
    );
    $stmt->execute(['tenant' => $trustedTenantId, 'id' => $postId]);
    $row = $stmt->fetch();
    return $row === false ? null : $row;
}
Read the code
  1. The query binds data values rather than concatenating them into SQL syntax.
  2. Tenant scope is part of the resource lookup, but membership and operation-specific policy still belong in the authorization design.
  3. The sentinel false from fetch is handled explicitly. The nullable return contract describes a not-found result.
[27]

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 rangeThis is a logical view of the access pattern, not a physical dump of InnoDB pages. Verify whether the optimizer actually chooses the intended index.selectedordered rangeLIMITComposite indextenant_id → created_at → idTenant 1 rangeOnly this range is requestedTenant 2 rangeOutside this request scopeTenant 3 rangeOutside this request scopeNewest matching keysOrdering includes a tiebreakerReturn a bounded pageMaterialize only needed fields
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
  1. Read the chosen access path, estimated rows, and ordering work.
  2. Compare estimates with actual execution observations, including loops and returned rows.
  3. Record the data shape and index definitions. A plan result without its workload context is hard to interpret.
[29]

6.4 Stable pagination needs an ordering contract

Continue after a composite cursor
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
  1. The timestamp and ID together describe the last item of the previous page.
  2. The ID breaks ties between equal timestamps. An order without a tie breaker is not a stable cursor contract.
  3. 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.

[30][31]
Reserve one available unit atomically
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
  1. The condition and decrement happen in one database statement.
  2. Two requests cannot both decrement a last unit using this statement while retaining a nonnegative result.
  3. If recording the reservation is a second database write, wrap both operations in one transaction.
  4. 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.

[32]

6.6 An ORM still issues SQL

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.

[33]
LAB 16

Measure a query before and after an index

Populate a synthetic feed at several sizes.

  1. Keep tenant distribution and created_at values documented.
  2. Record the query plan, latency, rows examined where available, and result correctness.
  3. Add or adjust one index for a justified query shape.
  4. Compare both read behavior and the write/storage tradeoff.
  5. 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.

  1. First implement a naive SELECT followed by UPDATE and observe a controlled race.
  2. Replace it with the conditional atomic UPDATE.
  3. Send two concurrent attempts under a concurrent-capable runtime.
  4. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: PDO prepare[27] · Transactions[30] · MySQL indexes[79] · InnoDB locking[32]

PILLAR 07

Security, authorization, and client data boundaries

Protect accounts and each client's data.

What to master

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 authorizationThe 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.authenticatecheckevaluatetrusted scopepermitted actionUntrusted requestSession cookie, tenant and IDAuthenticated principalValidated server-side identityTenant membershipPrincipal belongs to tenantOperation policyOwnership, role, resourcestateScoped lookup ormutationTrusted scope plus 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
  1. The join establishes that the actor belongs to the selected tenant.
  2. The predicate grants this example policy to the author or that tenant’s admin.
  3. The post is located inside the selected tenant. Changing the submitted tenant or post ID cannot bypass these predicates.
  4. A false result intentionally combines missing and unauthorized resources here. Decide your API’s disclosure policy explicitly.
  5. 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
<?php
declare(strict_types=1);
$password = 'local-demo-passphrase-only';
$storedHash = password_hash($password, PASSWORD_DEFAULT);

var_dump(password_verify($password, $storedHash));
var_dump(password_verify('wrong passphrase', $storedHash));

if (password_verify($password, $storedHash)
    && password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($password, PASSWORD_DEFAULT);
    // Persist after successful authentication.
}
Read the code
  1. The API creates the salt and encodes the information needed for verification in the returned hash.
  2. PASSWORD_DEFAULT can evolve. The schema uses VARCHAR(255) so the application does not assume a fixed hash length.
  3. Rehash after successful authentication when algorithm or work-factor policy changes.
  4. 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.
Expected result and interpretation
bool(true)
bool(false)
[35][36]

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
<?php
declare(strict_types=1);
$displayName = '<script>alert("demo")</script>';
$safeName = htmlspecialchars(
    $displayName,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);
echo '<p>Hello, ' . $safeName . '</p>';
Read the code
  1. The encoding occurs when data enters an HTML text context.
  2. ENT_SUBSTITUTE handles invalid encoding sequences without silently treating the input as safe markup.
  3. This technique is not a universal encoding for JavaScript, CSS, URLs, or arbitrary HTML. Choose a destination-specific approach.
  4. A template engine’s normal escaping helps only when its output context and escaping mode match the use.
[37][38]
DestinationPractical ruleFailure to look for
HTML text or a properly quoted ordinary attributeUse the framework’s context-appropriate escaping; validate attribute meaning as well.Rendering a raw post title or user name.
A link URLAllow the expected scheme and destination, then encode for the containing HTML attribute.Accepting an attacker’s javascript: URL.
JavaScript dataPrefer separate JSON responses or safe serialization with the framework’s documented mechanism.Concatenating user text into a script statement.
User-authored rich HTMLUse a maintained allowlist sanitizer with a defined policy.Removing only the literal word script.
SQL valuesBind parameters; whitelist query structure separately.Treating HTML escaping as SQL protection.

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
  1. Configure cookie and strict-mode settings before starting the session.
  2. Generate an unpredictable token and compare it with hash_equals after checking the submitted type.
  3. Keep the token out of URL query strings and ordinary request logs.
  4. Close the session after the values needed by this request have been obtained.
  5. Renew the session ID at the relevant authentication or privilege transition; define logout and expiry behavior separately.
[40][41][42]

7.5 Uploads are a storage and delivery boundary

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
  1. The original filename and client-reported MIME type do not determine the storage path.
  2. Actual upload provenance, size, and server-detected type are checked before moving the file.
  3. A random storage key is metadata, not permission to access the attachment.
  4. 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.
  5. Configure server/body limits, tenant quotas, directory permissions, cleanup, and any required content scanning around this helper.
[43][44]

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.

[45]

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.

  1. Build a permission matrix for viewing, editing, deleting, exporting, and downloading.
  2. Try every operation with another tenant’s resource ID.
  3. Test the same resource through search, direct URLs, and a queued operation.
  4. Check that cache keys and export files preserve tenant scope.
  5. 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.

  1. Submit an HTML-looking display name and verify it renders as text.
  2. Submit a missing, wrong, and array-shaped CSRF field.
  3. Attempt an oversized, disallowed-type, and malformed upload.
  4. Try a filename containing path separators and confirm it is never used as a storage path.
  5. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: OWASP authorization[34] · XSS prevention[38] · CSRF prevention[39] · Password hashing[35]

PILLAR 08

Testing, debugging, and static analysis

Diagnose failures and change code with evidence.

What to master

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 boundariesThese 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.logicintegration contractuser-facing behaviorinject failureverify recoveryBusiness ruleValid title, permitted actionUnit testDeterministic logic inisolationIntegration testReal database or adapterHTTP feature testRouting, auth, response,storageFailure and recoverytestTimeout, replay, rollback,crash
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.
TestGood exampleWhat it cannot establish alone
UnitA title rejects whitespace and accepts the expected normalized text.MySQL collation, transaction isolation, or route middleware.
IntegrationA unique constraint rejects the second operation key in the real database.Browser cookie handling and a complete deployment.
HTTP featureA member of A cannot update B’s post and no row changes.Behavior under realistic simultaneous database contention.
ContractA provider adapter maps the documented error payload to a stable result.The provider will never change or experience an outage.
Load or resilienceTwo 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
  1. The first assertion checks an observable contract, not a private implementation detail.
  2. The exception expectation is installed before the invalid constructor call.
  3. These two tests do not claim to cover every domain requirement. Add cases when the title contract grows.
[46]
Run one file before broadening the test run
SHELL Project root; install development dependencies before running
vendor/bin/phpunit --bootstrap vendor/autoload.php tests/TitleTest.php
vendor/bin/phpunit --bootstrap vendor/autoload.php tests

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
<?php
declare(strict_types=1);
/**
 * @param array{id: int, name: string}|null $student
 */
function greeting(?array $student): string
{
    if ($student === null) {
        return 'Student not found';
    }
    return 'Hello, ' . $student['name'];
}
echo greeting(['id' => 100, 'name' => 'Amina']) . PHP_EOL;
echo greeting(null) . PHP_EOL;
Read the code
  1. The native type handles nullability; the documented shape explains the required keys and value types to the analyzer.
  2. The null branch establishes the precondition before array access.
  3. PHPDoc does not validate external JSON at runtime. A boundary still has to construct the promised shape.
Expected result and interpretation
Hello, Amina
Student not found
[47]
A small analysis configuration
NEON Save as phpstan.neon; choose a level your project can enforce without ignoring unexplained errors
parameters:
    level: 8
    paths:
        - src
    tmpDir: var/phpstan
Run analysis and syntax checks
SHELL Composer development setup; syntax checking does not execute or test business behavior
vendor/bin/phpstan analyse --no-progress
php -l src/Title.php
[48]

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

  1. Reproduce: record the request, data, version, and configuration needed to trigger the problem.
  2. Narrow: identify the earliest boundary where actual values differ from the contract.
  3. Inspect: use a breakpoint to read the stack and local values; inspect SQL and external request metadata where needed.
  4. Explain: write the causal chain, including the hidden assumption.
  5. Repair: add a focused regression case and change the responsible boundary.
  6. 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.

[49]

8.5 Diagnostics should preserve evidence

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.

SymptomFirst evidence to collectUseful regression
A malformed ID becomes a different valid IDRaw 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 queriesQuery count and SQL for the representation being rendered.An integration/feature query-budget assertion with a realistic fixture.
Two requests both claim a last itemTransaction statements, locking behavior, concurrency timing.A controlled concurrent race against real MySQL.
A worker’s memory keeps risingPer-job memory, retained references, cache/collection sizes.Repeated jobs with bounded state and a documented memory envelope.
A provider action appears twiceOperation key, provider request IDs, timeout/retry sequence.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.

  1. Describe the observed behavior and the desired contract in one sentence.
  2. Write the smallest test that fails for that reason.
  3. Verify that the failure is not just a broken test fixture.
  4. Apply the fix, run the focused test, then run the relevant surrounding checks.
  5. 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.

  1. Test permitted member and admin actions.
  2. Test another tenant, a revoked membership, and a nonexistent resource.
  3. Assert both response behavior and the final database state.
  4. Add one test for an export or job initiated through the same feature.
  5. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: PHPUnit manual[80] · PHPStan[48] · Xdebug[49]

PILLAR 09

Performance, memory, and large data workloads

Improve a measured bottleneck without sacrificing correctness.

What to master

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.

WorkloadLikely first investigationUseful technique
Large stored relational datasetSelectivity, indexes, query plan, requested columns.Bounded queries, appropriate indexes, cursor pagination.
Large import or exportFile parsing, row size, buffering, restart behavior.Streaming or chunks, durable checkpoints, validation and quotas.
Many simultaneous web requestsWorker occupancy, database saturation, queues, tail latency.Reduce work, bound concurrency, cache carefully, queue suitable tasks.
Repeated expensive read representationCache hit/miss behavior and acceptable staleness.Tenant-aware cache keys, invalidation, expiry, stampede control.
Heavy analytics or transformationCPU, storage scans, aggregation location.Push suitable aggregation into the data system; orchestrate specialist tools where justified.
Streaming must remain bounded through the whole pathA 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.readdelivermaterializeprocessflush pathDatabase or fileLarge total datasetDriver or parserMay buffer before PHP sees itApplication stageGenerator or bounded batchUnbounded collectionfetchAll or iterator_to_arrayBounded sinkWrite one row or batchOutput bufferingServer and client also matter
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.

[50][33]

9.2 Parse and validate incrementally

Read a CSV without retaining every parsed record
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
  1. The parser produces one CSV record at a time, including records with quoted delimiters or newlines.
  2. An empty escape argument is explicit. Relying on fgetcsv’s default escape is deprecated from PHP 8.4.
  3. The header and each record are validated before yielding a useful shape.
  4. The finally block closes the handle when the generator completes or is disposed after an error.
  5. 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.
[51][52]
A small input file for the parser
CSV Save as people.csv, then run php import-read.php people.csv
id,email,name
100,amina@example.test,Amina
101,jules@example.test,"Jules, Jr."
102,mei@example.test,Mei
Expected result and interpretation
Valid records: 3

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
  1. The initial maximum ID stops ordinary later inserts from extending the export indefinitely.
  2. fetchAll is bounded by a fixed batch limit here. The dangerous property is unbounded materialization, not the method name alone.
  3. The cursor advances through an immutable primary key; selecting only required columns limits row size.
  4. This is not a repeatable snapshot: concurrent updates or deletes can affect what the export observes. Define and implement snapshot semantics when required.
  5. Only controlled numeric and timestamp fields are exported here. Exporting untrusted text to spreadsheets also needs a CSV/formula-injection policy.
[53]

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.

[54]

9.4 Measure memory and time without inventing results

Compare eager and generated iteration in separate runs
PHP Standalone 64-bit CLI; run php measure.php array and php measure.php generator as separate processes
<?php
declare(strict_types=1);
function numbers(int $count): Generator
{
    for ($i = 1; $i <= $count; $i++) {
        yield $i;
    }
}
$mode = $argv[1] ?? 'generator';
if (!in_array($mode, ['generator', 'array'], true)) {
    throw new InvalidArgumentException('Use generator or array');
}
$count = 500_000;
$start = hrtime(true);
$values = $mode === 'array' ? range(1, $count) : numbers($count);
$sum = 0;
foreach ($values as $number) {
    $sum += $number;
}
printf(
    "%s sum=%d elapsed_ms=%.2f peak_mib=%.2f\n",
    $mode,
    $sum,
    (hrtime(true) - $start) / 1_000_000,
    memory_get_peak_usage(true) / 1_048_576
);
Read the code
  1. Both paths perform the same summation, making the result a useful correctness check.
  2. Peak allocated memory is a high-water measurement for this process; it is not current retained memory or total system memory.
  3. Separate processes avoid letting the first run’s peak contaminate the second run’s measurement.
  4. 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.
[55][56]

9.5 Cache only a defined representation

Cache layerWhat it can storeQuestions you must answer
OPcacheCompiled PHP code for the configured execution environment.How is deployed code refreshed? Which runtime uses this cache?
Application cache, such as RedisA computed representation or reusable data with a deliberate lifetime.What is the key, scope, freshness, invalidation rule, and miss behavior?
HTTP cacheA response under HTTP cache policy.Is the response public or private? Which headers and variants control reuse?
Per-job or per-request memoizationA 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.

[57]

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.

[58]

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.

  1. Generate a synthetic input at several sizes without using private client data.
  2. Validate records and write batches with a durable checkpoint.
  3. Kill the job after a committed batch, restart it, and inspect duplicates.
  4. Define behavior for a malformed record and a changed source file.
  5. 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.

  1. Write the workload and correctness contract.
  2. Measure before changing anything.
  3. Use query plans, profiling, or instrumentation to identify the bottleneck.
  4. Change one justified mechanism and repeat comparable measurements.
  5. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: Generators[52] · PDO/MySQL buffering[50] · Eloquent streaming caveat[81] · FPM capacity[58]

PILLAR 10

API design and external integrations

Expose stable contracts and handle unreliable services.

What to master

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 decisionExample policyReason
Resource representationReturn a stable post ID, UTC timestamp, and documented nullable fields.Consumers need predictable meaning and types.
Validation errorA consistent code plus field-level details safe to disclose.Consumers can correct input without parsing changing prose.
PaginationBounded size and an opaque cursor carrying the ordering contract.The server controls cost and the client can continue reliably.
Rate limitingA documented response and retry policy.Clients can reduce pressure instead of retrying aggressively.
CompatibilityAdditive fields where permitted; deliberate removal/version policy.Independent releases can coexist.
Operation identityA 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 callerA 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.requestresponseuncertain outcomesame keyrecorded resultCaller sends operationStable operation key KService commits changeEffect exists durablyResponse is lostCaller observes a timeoutCaller retries KSame logical requestOperation recordMatch request, return resultCaller receives resultNo second logical effect
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
  1. Connection and total timeouts bound two different waits.
  2. Transport failure and HTTP rejection are distinct observations.
  3. The response shape is checked instead of trusting valid JSON to imply a valid contract.
  4. 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.
  5. Keep TLS peer and hostname verification enabled for real HTTPS calls. Do not turn verification off to conceal configuration problems.
[59]

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.

[60]
The disposable status mock
PHP Save as mock.php and run php -S 127.0.0.1:8090 mock.php; local development only
<?php
declare(strict_types=1);
if (($_SERVER['REQUEST_URI'] ?? '') !== '/status') {
    http_response_code(404);
    exit;
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(['state' => 'ready'], JSON_THROW_ON_ERROR);

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 recordExampleDesign responsibility
Scopetenant_id + operation_name + keyTwo clients or two operation types should not accidentally share identity.
Request identityDigest of a documented canonical request representationThe same key with a different request must not silently reuse the earlier result.
StateAccepted, processing, completed, or a defined failure stateConcurrent duplicates need a deliberate response or wait policy.
ResultStatus and safe response data, or durable resource referenceA lost response can be recovered.
LifetimeA documented retention windowExplain what happens after the key expires.
External correlationProvider operation key and request/reference IDReconcile 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.

[61]

10.4 Verify webhooks before interpreting their body

Verify a custom lab signature over timestamp and raw body
PHP Standalone 64-bit CLI example of a custom lab protocol; real providers have their own signature formats and SDKs
<?php
declare(strict_types=1);
function validLabSignature(
    string $rawBody,
    string $timestamp,
    string $signature,
    string $secret,
    int $now
): bool {
    if ($secret === ''
        || preg_match('/^[0-9]{10}$/D', $timestamp) !== 1
        || preg_match('/^[a-f0-9]{64}$/D', $signature) !== 1) {
        return false;
    }
    $sentAt = (int) $timestamp;
    if (abs($now - $sentAt) > 300) {
        return false;
    }
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $signature);
}

$body = '{"event_id":"demo-1","type":"post.created"}';
$timestamp = '1800000000';
$secret = 'local-lab-secret-only';
$signature = hash_hmac('sha256', $timestamp . '.' . $body, $secret);
var_dump(validLabSignature($body, $timestamp, $signature, $secret, 1800000000));
var_dump(validLabSignature($body . ' ', $timestamp, $signature, $secret, 1800000000));
Read the code
  1. The exact received bytes are part of the signature. Re-encoding parsed JSON changes that input.
  2. The timestamp window limits one replay dimension and requires a reasonably synchronized clock.
  3. A valid signature does not prove the event has never been processed. Persist event identity and a duplicate policy separately.
  4. Use a real provider’s maintained verifier and documented secret-rotation rules for that provider. This custom protocol is not a Stripe verifier.
Expected result and interpretation
bool(true)
bool(false)
[62][63]

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.

  1. Commit a synthetic business effect and its result record.
  2. Simulate the response being lost after commit.
  3. Repeat the same key and body, then the same key with a different body.
  4. Send two simultaneous requests with the same key.
  5. 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.

  1. Reject a changed raw body and an expired timestamp.
  2. Deliver the same event ID more than once.
  3. Deliver an older state event after a newer state event.
  4. Crash after durable receipt but before acknowledging.
  5. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: Stripe webhook behavior[63] · Stripe idempotent requests[61]

PILLAR 11

Concurrency, background jobs, and distributed consistency

Keep work correct when several processes run and some fail.

What to master

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.

MechanismWhat it meansBoundary to understand
FPM workersSeveral processes can handle different requests concurrently.Shared database/cache state and each process’s capacity.
Queue workersProcesses receive jobs and often live across many jobs.Retry, acknowledgment, timeout, retained state, deployment restart.
FibersCooperatively suspend and resume execution contexts.A scheduler and suitable nonblocking operations are required for overlapping I/O.
Processes or specialist parallel workersWork can run in separate execution environments.Communication, isolation, resource budgets, result coordination.
Database locks and constraintsThe data system enforces selected concurrent rules.Transaction duration, deadlocks, uniqueness, isolation, lock scope.

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.

[64]

11.2 A job can run more than once

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 pointPossible observationRequired design
Before any durable effectThe job can be delivered again.Retry with bounded attempts and classification.
After database commit, before acknowledgmentThe same event may run again.Receipt and local effect committed atomically, or an equivalent idempotent operation.
After a remote provider accepts, before local result recordingThe local outcome is uncertain.Provider operation identity and reconciliation.
While holding a claim or leaseAnother worker may eventually take over.Lease expiry, ownership checks, and stale-owner protection appropriate to the effect.
After repeated deterministic failureThe 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 publicationA 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.same commitsame commitpending rowspublishdeliver / redeliverApplication transactionOne local commit boundaryBusiness rowNew post existsOutbox event rowIntent is durablePublisher processClaim, send, record progressBrokerMay deliver repeatedlyConsumer transactionReceipt plus local effect
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.

[65]
Create a post and its durable event intent
PHP Core schema plus outbox appendix; PostTitle from pillar 4; caller has checked policy and body; IDs fit PHP integers
<?php
declare(strict_types=1);
function createPostWithOutbox(
    PDO $db,
    int $trustedTenantId,
    int $authenticatedAuthorId,
    PostTitle $title,
    string $validatedBody
): int {
    if ($db->inTransaction()) {
        throw new LogicException('This use case owns its transaction');
    }
    $eventId = bin2hex(random_bytes(16));
    $db->beginTransaction();
    try {
        $post = $db->prepare(
            'INSERT INTO posts (tenant_id, author_id, title, body)
             VALUES (:tenant, :author, :title, :body)'
        );
        $post->execute([
            'tenant' => $trustedTenantId,
            'author' => $authenticatedAuthorId,
            'title' => $title->value,
            'body' => $validatedBody,
        ]);
        $postId = (int) $db->lastInsertId();
        $event = $db->prepare(
            'INSERT INTO outbox_events (event_id, tenant_id, event_type, payload)
             VALUES (:event, :tenant, :type, :payload)'
        );
        $event->execute([
            'event' => $eventId,
            'tenant' => $trustedTenantId,
            'type' => 'post.created',
            'payload' => json_encode([
                'event_id' => $eventId,
                'tenant_id' => $trustedTenantId,
                'post_id' => $postId,
                'schema_version' => 1,
            ], JSON_THROW_ON_ERROR),
        ]);
        $db->commit();
        return $postId;
    } catch (Throwable $failure) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }
        throw $failure;
    }
}
Read the code
  1. This use case explicitly owns the local transaction; callers must not assume an independent nested transaction.
  2. Both the post and event row commit, or both roll back.
  3. The payload contains stable identifiers and a schema version rather than serializing a live PHP object.
  4. No remote publish occurs while this transaction is open.
  5. 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
  1. The unique consumer/event key prevents two successful receipts for this handler identity.
  2. Receipt and counter change are in the same database transaction.
  3. Only the specific MySQL duplicate-key error is treated as a replay. Other integrity errors remain failures.
  4. A counter failure rolls the receipt back so a corrected retry can still apply it.
  5. 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
  1. Dispatching after commit avoids a job observing an uncommitted row in a normal setup.
  2. A job receives stable IDs and should apply the documented policy when it runs.
  3. 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.
[66]

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.

[67]
Worker setting or ruleWhy it mattersCheck
Job timeout and broker visibility/retry intervalA 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 backoffTransient failures need recovery without an infinite storm.Classify retryable failures and define a terminal repair queue.
Graceful shutdownA 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 limitsRetained state can accumulate over many jobs.Measure repeated jobs; recycling is a guard, not a substitute for fixing retention.
Idempotent or deduplicated effectsRedelivery 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.

  1. Deliver the same event twice and check counter and receipt rows.
  2. Force an error after receipt insertion and before the counter change.
  3. Retry after correcting the error.
  4. Crash after commit and simulate redelivery.
  5. 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.

  1. Commit a post while the broker or mock publisher is unavailable.
  2. Confirm that the outbox intent remains visible and retryable.
  3. Simulate a send followed by a crash before published_at is recorded.
  4. Run multiple publishers and test claim expiry.
  5. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: Laravel queues[66] · Transactional outbox[65] · PHP fibers[64]

PILLAR 12

Deployment, operations, and client delivery

Own the system after it leaves your laptop.

What to master

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 reversibleThis 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.compatiblecompatiblesafe return pathmigrate datalater releaseExpand schemaAdd compatible fields ortablesOld code remains validExisting readers keep workingDeploy compatible newcodeBoth schema forms understoodBackfill and observeBounded work; verify resultsRollback code if neededAdditive schema can remainContract laterRemove only aftercompatibility ends
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
  1. The document root points to public, keeping application code and secrets out of direct static serving.
  2. Only the exact front controller is forwarded to PHP-FPM.
  3. Private uploads and export files belong outside this public tree and require authorized delivery.
  4. Request size limits must align with PHP, application, and storage limits.
  5. Verify the deployed route and static-file behavior; a configuration example alone is not evidence that the host is correctly configured.
[68][69]

12.3 Budget PHP workers with the downstream system

An illustrative dedicated FPM pool
INI Illustrative Linux pool; users, permissions, paths, timeouts, and numbers require workload-specific validation
[community]
user = community
group = community
listen = /run/php/community-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500

request_slowlog_timeout = 2s
slowlog = /var/log/php/community-slow.log
request_terminate_timeout = 30s

php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php/community-error.log
Read the code
  1. max_children bounds concurrently served requests in this pool.
  2. Worker memory plus runtime/shared memory and operating-system needs constrain the process budget.
  3. Each worker can also consume database connections; coordinate the web and queue budgets with the database.
  4. max_requests can recycle processes, but does not explain or repair unbounded retained state.
  5. A slow log helps identify long requests. Keep diagnostic access and log contents appropriately restricted.
[58]

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.

[70]

12.4 Use a safe last-resort exception boundary

A small HTTP exception boundary with a correlation ID
PHP HTTP bootstrap fragment; production display_errors off; place before application work and response output
<?php
declare(strict_types=1);
$requestId = bin2hex(random_bytes(16));
set_exception_handler(static function (Throwable $failure) use ($requestId): void {
    error_log(json_encode([
        'event' => 'unhandled_exception',
        'request_id' => $requestId,
        'exception_type' => $failure::class,
    ], JSON_THROW_ON_ERROR));

    if (headers_sent()) {
        exit; // A partial response cannot safely be replaced here.
    }
    http_response_code(500);
    header('Content-Type: application/json; charset=UTF-8');
    header('X-Request-ID: ' . $requestId);
    header('Cache-Control: no-store');
    echo json_encode([
        'error' => [
            'code' => 'internal_error',
            'message' => 'The request could not be completed.',
            'request_id' => $requestId,
        ],
    ], JSON_THROW_ON_ERROR);
});
Read the code
  1. The public response carries a stable code and request ID rather than exception text or a stack trace.
  2. The log intentionally uses a minimal safe envelope. An approved internal reporter can add redacted diagnostic context.
  3. Expected domain and validation failures should be mapped deliberately before reaching this final boundary.
  4. If output has already started, headers/status may no longer be repairable. Avoid premature output and use the framework’s response pipeline.
  5. This handler does not guarantee recovery from every fatal startup error, exhausted-memory condition, or process termination.

12.5 Build one release artifact and verify it

Release stageConcrete workEvidence
Before buildReview dependency lock, configuration requirements, and schema compatibility.Versioned lock file and deployment notes.
ValidationRun relevant tests, static analysis, syntax and dependency checks.CI results for the artifact’s revision.
BuildInstall production dependencies from the lock file and assemble assets.Immutable artifact/revision identity.
Schema/data changeRun an approved compatible migration and bounded backfill.Migration state, row checks, observable progress.
ActivateSwitch traffic/code using the environment’s supported method.Health, readiness, route and permission checks.
WorkersRestart or drain persistent processes and confirm new revision.Worker version and job processing evidence.
ObserveWatch errors, latency, saturation, queues, and key business signals.A defined observation window and rollback criteria.
Recover if necessaryUse the documented rollback or restore procedure.Verified behavior and any data reconciliation.
Representative project validation commands
SHELL Run in CI or a controlled development build; use your project’s actual test/bootstrap configuration
composer validate --strict
composer install --no-interaction --prefer-dist
composer audit
composer check-platform-reqs
vendor/bin/phpunit --bootstrap vendor/autoload.php tests
vendor/bin/phpstan analyse --no-progress
Representative Laravel release commands
SHELL Deployment illustration after project/environment validation; migration and activation are separate planned steps
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
composer check-platform-reqs --no-dev
php artisan optimize
php artisan queue:restart
Read the code
  1. Install the exact locked production dependency graph.
  2. Framework caches depend on the correct release configuration. Learn which configuration reads occur at runtime versus cache build time.
  3. Queue restart depends on the deployed queue/cache/process-manager setup; verify that a supervisor replaces workers and loads the new code.
  4. Do not use this command list as a substitute for migration, health, rollback, or backup procedures.
[22][69]

12.6 A backup is useful when restoration works

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

DeliverableWhat it should answer
Acceptance criteriaWhich user-visible outcomes and denied actions define completion?
Architecture and data mapWhere does data live, who can access it, and which dependencies are authoritative?
Operational runbookHow are releases, rollback, queue repair, and restoration performed?
Capacity evidenceWhat workload was measured, under which conditions, and where are the limits?
Access and ownershipWho owns domains, credentials, hosting, provider accounts, and support responsibilities?
Maintenance planHow are supported runtime/dependency upgrades and compatibility changes managed?
Known tradeoffsWhich 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.

  1. Add a compatible field or table and deploy code that can coexist with the previous version.
  2. Run a bounded backfill if needed.
  3. Verify web routes, queue worker revision, permissions, and a smoke workflow.
  4. Trigger the documented rollback criteria in a controlled way.
  5. 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.

  1. Restore database and private storage into a fresh isolated environment.
  2. Recreate configuration using documented secret provisioning.
  3. Verify tenant isolation, a post workflow, export access, and queue recovery.
  4. Have another person follow the setup/runbook without your undocumented local assumptions.
  5. 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.

  1. State the input, output, permission, and failure contracts relevant to this challenge.
  2. Build the smallest useful baseline and reproduce its behavior.
  3. Collect evidence from the relevant runtime, request, database, or process boundary.
  4. Inject at least one malformed input or failure, then verify the intended invariant.
  5. 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.

Read further: Laravel deployment[69] · FPM configuration[58] · PHP support lifecycle[71]

The evidence project

A private community platform for several clients

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 stateThe 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.HTTPtransactionsauthorized metadatawork interfacedurable intentbounded callsBrowser and API clientsUntrusted claims and requestsPHP applicationIdentity, policy, use casesMySQLBusiness state and eventintentPrivate file storageUploads and completed exportsWorkers and publisherBounded, retry-aware workMock external providerIndependent failure boundary
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

ModuleResponsibilitiesBoundary tests
Identity and membershipAuthentication, memberships, role transitions, recovery/session policy.Invalid session, revoked membership, cross-tenant requests.
Posts and commentsValidated values, publishing rules, visibility, editing/deletion policy.Invalid input, foreign keys, permission matrix, transaction failure.
Feed and followsBounded representation, stable pagination, permitted relationships.Equal timestamps, N+1, missing tenant predicates.
Media and exportsPrivate storage metadata, quotas, authorized delivery, expiry/cleanup.Disallowed upload, direct-access bypass, partial export.
Import jobsSource identity, record validation, batch commit, checkpoint, report.Crash/restart, changed source, duplicates, malformed records.
Integration and eventsOperation identity, webhook verification, durable intent/receipt, retry.Lost response, duplicate delivery, out-of-order event.
OperationsRelease, worker lifecycle, telemetry, backups, restore, runbook.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
  1. Composite foreign keys bind child relationships to the same tenant as their parent membership or post.
  2. The explicit unique parent key avoids relying on deprecated nonstandard foreign-key targets.
  3. The follow primary key prevents duplicate relationships; the check rejects following yourself.
  4. Internal event IDs are 32 hexadecimal characters in the PHP examples, compared with a binary ASCII collation.
  5. 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.
  6. 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
  1. The key is scoped by tenant and operation name.
  2. Application validation must bound key syntax/length and define what request_hash includes.
  3. The schema alone does not implement idempotency. A correct claim, effect, result, retry, and recovery protocol must use it.
  4. A remote provider still has an independent atomicity boundary.

Capstone milestones and evidence

MilestoneBuildEvidence to keep
1 · Plain PHP vertical sliceOne validated read and write path using explicit response contracts.Predicted/actual outputs and a request trace.
2 · Tenant-scoped persistenceMembership, posts, comments, constraints, and policy.Permission matrix, SQL plans, concurrency and rollback observations.
3 · Composer and framework structureNamespaced components, tests, analyzer, one traced framework feature.Locked install, test results, explanation of framework boundaries.
4 · Large workloadBounded import/export, pagination, measured feed behavior.Dataset definition, peak memory, latency/error report, restart result.
5 · Repeated and failed workAPI operation identity, verified events, outbox and consumer.Crash-point table, duplicate results, reconciliation policy.
6 · Operated releaseStaging deployment, worker restart, telemetry, rollback, restoration.Release identity, runbook, restore checks, handoff notes.

Final demonstration checklist

How to assess mastery

Measure independence and evidence

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.

LevelObservable capabilityExample
0 · UnfamiliarCannot yet explain the main mechanism.Unsure where request data enters the application.
1 · RecognizeCan describe terms and follow a working example.Can run the typed lookup and identify validation versus lookup.
2 · ImplementCan build the feature with reference documentation.Can add a scoped endpoint and its relevant tests.
3 · DiagnoseCan reproduce a failure, explain its cause, and verify a repair.Can isolate a session lock, missing index, race, or duplicate effect.
4 · Operate and teachCan 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

  1. Choose one rule or mechanism rather than reading many chapters passively.
  2. Predict the result in writing.
  3. Run a small valid case, then one carefully chosen edge case.
  4. Inspect the enforcing boundary and collect a concrete observation.
  5. Change the environment or input enough to test the hidden assumption.
  6. Write a concise explanation and an acceptance check someone else can repeat.

Questions that reveal shallow understanding

Keep the vocabulary precise

Glossary

TermMeaning in this handbook
SAPIThe interface connecting PHP execution to an environment such as CLI or a web server/FastCGI service.
Zend EnginePHP’s core language execution machinery, including compilation and execution behavior.
OpcodeAn instruction in the engine’s compiled representation of PHP code.
OPcacheA cache of compiled PHP code; its lifecycle and refresh rules depend on configuration.
JITCompilation of selected execution work into native machine code under a configured runtime; benchmark suitability for the workload.
zvalThe engine’s value container concept; exact internal layout is version-specific.
Reference countingTracking ownership of selected shared values as part of memory management.
Copy-on-writeSharing suitable storage until a mutation requires separation rather than eagerly copying everything.
PHP referenceAn aliasing relationship between variables, not a universal synonym for an object handle or raw pointer.
ReachabilityWhether a value remains accessible through live program roots and references.
Cyclic garbage collectionReclaiming eligible unreachable cycles; it does not clear intentionally retained data.
FPMFastCGI Process Manager: a way to manage PHP worker processes for web requests.
Persistent workerA process/application lifecycle that can retain booted state across multiple requests or jobs.
Type coercionConversion between types under applicable language rules.
strict_typesA per-file declaration affecting relevant scalar type coercion; it is not request validation.
Ordered mapPHP array behavior combining keyed lookup with order; keys and storage have language-specific rules.
ClosureA function value that can carry captured surrounding state.
GeneratorAn iterator produced by a function that yields values without necessarily building a full result collection.
ReadonlyA restriction on property reassignment under PHP’s rules, not automatic deep immutability of referenced objects.
Synchronous callThe caller waits for the operation to complete before continuing that execution path.
FiberA cooperatively suspendable execution context; overlapping I/O requires a suitable scheduler and I/O integration.
MiddlewareCode around a request-handling pipeline, often handling concerns before and after the next handler.
Front controllerThe common entry point that routes web requests into an application.
Session lockA coordination lock in the configured session handler that may serialize requests sharing a session.
Dependency injectionProviding collaborators to a component instead of having it locate or construct every dependency itself.
Value objectAn object representing a meaningful value and its invariants, rather than a persistence identity.
Composition rootThe location where an application wires its concrete collaborators and starts the use case or transport.
AdapterA component translating an external mechanism or protocol into the contract the application needs.
PSRA PHP-FIG interoperability recommendation; applicability depends on the particular specification.
PSR-4A namespace-to-file autoloading convention used by Composer and many PHP libraries.
Lock fileThe recorded resolved dependency graph used for repeatable application installs.
Prepared statementA database statement that separates bound data values from SQL structure under driver-specific behavior.
IndexA maintained access structure that can accelerate suitable queries while adding storage and write costs.
Composite indexAn index ordered by several key columns; leading columns matter to usable access patterns.
Keyset paginationContinuing an ordered result after a stable cursor rather than skipping a growing offset.
N+1 query patternA base query followed by one additional query per result or relationship access.
TransactionA local database boundary for coordinated commit/rollback under the database’s guarantees.
Isolation levelRules governing interactions and visibility between concurrent database transactions.
DeadlockA cycle of incompatible lock waits that the database resolves by aborting an involved operation/transaction.
InvariantA rule that must remain true across permitted state changes and failures.
TenantA client/organization scope in a shared application; authority must establish access to that scope.
AuthenticationEstablishing the identity of a principal under the application’s policy.
AuthorizationDeciding whether a principal may perform an operation on a resource.
CSRFCross-site request forgery: abuse of browser-carried credentials to induce an unintended request.
XSSCross-site scripting: untrusted content being interpreted as executable browser content.
SSRFServer-side request forgery: induced outbound server requests reaching unintended destinations.
Static analysisExamining code and type information without relying on executing every path.
Regression testA test preserving the expected behavior associated with a discovered or anticipated failure.
p95 latencyA percentile summarizing the observed latency distribution; its meaning depends on the measured workload and sampling.
BackpressureA way to constrain or signal incoming work when a downstream stage cannot keep up.
TTLTime to live: a configured lifetime/expiry concept; it does not by itself prove current business truth.
Cache stampedeMany misses triggering simultaneous rebuild work for the same representation.
IdempotencyA property or application protocol in which repeats of a defined operation preserve its intended logical effect.
At-least-once deliveryA delivery model permitting repeated deliveries in order to support eventual receipt under its assumptions.
AcknowledgmentA signal that work was accepted or handled according to a protocol; the exact durability meaning must be defined.
Transactional outboxPersisting event intent beside a local business change in the same database transaction.
Consumer receiptA durable record that a named handler processed a given event identity.
LeaseTime-bounded ownership; expiry can permit takeover, so stale owners still need an appropriate safety protocol.
ReconciliationComparing uncertain or divergent state with authoritative records and applying a defined repair.
High-water markA boundary captured for a bounded scan; it does not necessarily create a consistent snapshot.
RPORecovery point objective: the agreed target for acceptable data loss, often expressed as time.
RTORecovery time objective: the agreed target for restoring the relevant service after disruption.
Expand and contractA staged compatibility approach: add support, migrate usage/data, then remove the old form later.
RunbookConcrete 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.

  1. [1]PHP comparison operators
    https://www.php.net/manual/en/language.operators.comparison.php
  2. [2]Anonymous functions
    https://www.php.net/manual/en/functions.anonymous.php
  3. [3]PHP string semantics
    https://www.php.net/manual/en/language.types.string.php
  4. [4]Enumerations
    https://www.php.net/manual/en/language.enumerations.php
  5. [5]Properties and readonly behavior
    https://www.php.net/manual/en/language.oop5.properties.php
  6. [6]OPcache
    https://www.php.net/manual/en/book.opcache.php
  7. [7]zval structure
    https://www.phpinternalsbook.com/php7/zvals/basic_structure.html
  8. [8]Value memory management
    https://www.phpinternalsbook.com/php7/zvals/memory_management.html
  9. [9]Collecting cycles
    https://www.php.net/manual/en/features.gc.collecting-cycles.php
  10. [10]PHP lifecycle
    https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html
  11. [11]Zend memory manager
    https://www.phpinternalsbook.com/php7/memory_management/zend_memory_manager.html
  12. [12]PHP POST data
    https://www.php.net/manual/en/reserved.variables.post.php
  13. [13]php://input
    https://www.php.net/manual/en/wrappers.php.php
  14. [14]Built-in development server
    https://www.php.net/manual/en/features.commandline.webserver.php
  15. [15]Session closing and locking
    https://www.php.net/manual/en/function.session-write-close.php
  16. [16]HTTP semantics: RFC 9110
    https://www.rfc-editor.org/rfc/rfc9110.html
  17. [17]User-level output buffers
    https://www.php.net/manual/en/outcontrol.user-level-output-buffers.php
  18. [18]Laravel service container
    https://laravel.com/docs/container
  19. [19]PSR-11 container interface
    https://www.php-fig.org/psr/psr-11/
  20. [20]Composer basic usage
    https://getcomposer.org/doc/01-basic-usage.md
  21. [21]PSR-4 autoloading
    https://www.php-fig.org/psr/psr-4/
  22. [22]Composer command reference
    https://getcomposer.org/doc/03-cli.md
  23. [23]PSR-3 logging
    https://www.php-fig.org/psr/psr-3/
  24. [24]PSR-7 messages
    https://www.php-fig.org/psr/psr-7/
  25. [25]PSR-18 clients
    https://www.php-fig.org/psr/psr-18/
  26. [26]Laravel request lifecycle
    https://laravel.com/docs/lifecycle
  27. [27]PDO parameter binding
    https://www.php.net/manual/en/pdo.prepare.php
  28. [28]MySQL composite indexes
    https://dev.mysql.com/doc/refman/8.4/en/multiple-column-indexes.html
  29. [29]MySQL EXPLAIN
    https://dev.mysql.com/doc/refman/8.4/en/explain.html
  30. [30]PDO transactions
    https://www.php.net/manual/en/pdo.transactions.php
  31. [31]InnoDB isolation
    https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html
  32. [32]InnoDB locking
    https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html
  33. [33]Eloquent ORM behavior
    https://laravel.com/docs/eloquent
  34. [34]OWASP authorization guidance
    https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
  35. [35]PHP password hashing
    https://www.php.net/manual/en/function.password-hash.php
  36. [36]OWASP password storage
    https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
  37. [37]PHP HTML encoding
    https://www.php.net/manual/en/function.htmlspecialchars.php
  38. [38]OWASP XSS prevention
    https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  39. [39]OWASP CSRF prevention
    https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
  40. [40]PHP session security settings
    https://www.php.net/manual/en/session.security.ini.php
  41. [41]PHP random bytes
    https://www.php.net/manual/en/function.random-bytes.php
  42. [42]PHP timing-safe comparison
    https://www.php.net/manual/en/function.hash-equals.php
  43. [43]PHP upload mechanics
    https://www.php.net/manual/en/features.file-upload.post-method.php
  44. [44]OWASP file upload design
    https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
  45. [45]OWASP SSRF prevention
    https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
  46. [46]PHPUnit 12.5 test writing
    https://docs.phpunit.de/en/12.5/writing-tests-for-phpunit.html
  47. [47]PHPStan PHPDoc types
    https://phpstan.org/writing-php-code/phpdoc-types
  48. [48]PHPStan getting started
    https://phpstan.org/user-guide/getting-started
  49. [49]Xdebug step debugging
    https://xdebug.org/docs/step_debug
  50. [50]PDO MySQL buffered and unbuffered queries
    https://www.php.net/manual/en/mysqlinfo.concepts.buffering.php
  51. [51]PHP CSV reading
    https://www.php.net/manual/en/function.fgetcsv.php
  52. [52]PHP generators
    https://www.php.net/manual/en/language.generators.overview.php
  53. [53]PHP CSV writing
    https://www.php.net/manual/en/function.fputcsv.php
  54. [54]OWASP CSV injection
    https://community.owasp.org/attacks/CSV_Injection
  55. [55]PHP high-resolution time
    https://www.php.net/manual/en/function.hrtime.php
  56. [56]PHP peak memory measurement
    https://www.php.net/manual/en/function.memory-get-peak-usage.php
  57. [57]Redis expiry behavior
    https://redis.io/docs/latest/commands/expire/
  58. [58]PHP FPM worker configuration
    https://www.php.net/manual/en/install.fpm.configuration.php
  59. [59]PHP cURL options
    https://www.php.net/manual/en/function.curl-setopt.php
  60. [60]PHP cURL handle lifecycle
    https://www.php.net/manual/en/function.curl-close.php
  61. [61]Stripe idempotent request contract
    https://docs.stripe.com/api/idempotent_requests
  62. [62]PHP HMAC API
    https://www.php.net/manual/en/function.hash-hmac.php
  63. [63]Stripe webhook signatures and delivery
    https://docs.stripe.com/webhooks
  64. [64]PHP fibers
    https://www.php.net/manual/en/language.fibers.php
  65. [65]Transactional outbox pattern reference
    https://microservices.io/patterns/data/transactional-outbox.html
  66. [66]Laravel queues and transaction timing
    https://laravel.com/docs/queues
  67. [67]Laravel Octane process lifetime
    https://laravel.com/docs/octane
  68. [68]Nginx FastCGI module
    https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html
  69. [69]Laravel deployment guidance
    https://laravel.com/docs/deployment
  70. [70]PHP OPcache configuration
    https://www.php.net/manual/en/opcache.configuration.php
  71. [71]PHP supported versions
    https://www.php.net/supported-versions.php
  72. [72]PHP migration guides
    https://www.php.net/manual/en/appendices.php
  73. [73]MySQL foreign-key requirements
    https://dev.mysql.com/doc/refman/8.4/en/create-table-foreign-keys.html
  74. [74]PHP language reference
    https://www.php.net/manual/en/langref.php
  75. [75]Type declarations
    https://www.php.net/manual/en/language.types.declarations.php
  76. [76]Arrays
    https://www.php.net/manual/en/language.types.array.php
  77. [77]HTTP middleware standard
    https://www.php-fig.org/psr/psr-15/
  78. [78]Objects and references
    https://www.php.net/manual/en/language.oop5.references.php
  79. [79]MySQL indexes
    https://dev.mysql.com/doc/refman/8.4/en/optimization-indexes.html
  80. [80]PHPUnit manual
    https://docs.phpunit.de/en/12.5/
  81. [81]Eloquent streaming caveat
    https://laravel.com/docs/eloquent#cursors