Build a PHP permit review service for permit 447. It starts at revision 8 with one reviewer session and no attachments. A typed form command saves a note, an approval transaction advances the record once under a replay key, and a private upload becomes downloadable only after validation and a clean scan. The same reviewer opens a long export in another tab. The project is complete when malformed field shapes, session races, lost responses, stale revisions, and upload failures all have named outcomes and leave consistent records.
Project: PHP Permit Review Intake
Build contract
- Convert superglobal fields into a typed command before authorization or persistence.
- Rotate the session at login and release its lock before slow read-only work.
- Commit a versioned permit update, replay identity, audit event, and outbox intent together.
- Keep uploaded bytes private under server-generated names until a clean-state transition.
- Expire browser and server session state on logout and recheck current permissions.
Implementation checkpoint
<?php
$permit = ['id' => 447, 'revision' => 8, 'status' => 'pending'];
$expectedRevision = 8;
if ($permit['revision'] === $expectedRevision) {
$permit['revision']++;
$permit['status'] = 'approved';
}
echo $permit['revision'] . ' ' . $permit['status'];Cost and boundaries
Validation and file inspection are linear in admitted input bytes. Index-backed replay lookup adds a small database operation to each mutation, and an outbox adds retained intent records and worker delivery. Holding a session lock through a long export can serialize same-session requests for the entire export duration. A private upload temporarily occupies both PHP temporary storage and final storage, while a scanner adds pending time. Measure lock wait, transaction duration, replay hits, outbox age, pending scan age, and orphan bytes; a fast response does not prove background effects were completed.
Failure drill
Submit an array-shaped permit ID, malformed revision, and note containing markup. Start a 23-second export and save a note in another tab; the note must not wait on a session lock. Commit approval and drop the response, then retry with the same key and with a changed body under that key. Race a stale reviewer against the version update. Upload a renamed archive, a body over post_max_size, and two files with the same display name. Force a failure after moving bytes but before the database record commits. Every retained file must be traceable to a private owner or cleanup task.
Acceptance checks
- Convert superglobal fields into a typed command before authorization or persistence.
- Rotate the session at login and release its lock before slow read-only work.
- Commit a versioned permit update, replay identity, audit event, and outbox intent together.
- Keep uploaded bytes private under server-generated names until a clean-state transition.
- Expire browser and server session state on logout and recheck current permissions.
Common Mistakes
- Checking only successful responses instead of failed intermediate states.
- Conflating a local cancellation with rollback of an external effect.
- Leaving resource ownership implicit after client disconnect.
Related lessons
PHP Request, Session, and Persistence Boundaries; PHP Superglobal Input and Output Trust; PHP Session Rotation, Locking, and Logout; PHP PDO Transactions, Replay, and Query Identity; PHP Upload Tempfiles, Private Storage, and Lifecycle.
