July 31, 2026
#php
A line in square brackets sits above your job classes and runs nothing at all. Delete an attribute's import and PHP builds the object anyway; write a string where an int is declared, hang the attribute on a method, or misspell its name by two letters, and the language stays silent every time. Where the shout lives (newInstance), why a typo ships a maxTries=null payload with no warning anywhere, and how getJobTries climbs the parent classes on foot because PHP will not inherit an attribute for it.
July 25, 2026
· Open the box · part 8 of 8
delete() sends one keyed DELETE, flips exists back to false, and leaves a ghost in your hand: the object keeps every attribute after the row is gone, id included. Then the ghost saves itself back, and the INSERT ships the old id as an explicit column, in the exact order the copy holds. Why the model never delegates death, the silent return where increment once hit all 103 rows, and a resurrection that is not a soft delete.
July 24, 2026
· Open the box · part 7 of 8
new Order, exists false, one save: the INSERT ships with returning "id" and the model learns its own name in the same round trip, no second query. How a row is born: performInsert, both timestamps stamped for free, insertGetId and the Postgres RETURNING clause, Builder::create as new-fill-save in one line, and the mass assignment shield that drops unknown keys in silence.
July 23, 2026
· Open the box · part 6 of 8
class_basename says Carbon, the copies from the last episode hold a plain string, and getCasts() does not even list created_at. Laravel's cast and accessor machine from the inside: the three doors of transformModelValue, the getDates gift that comes with timestamps, why every access builds a fresh Carbon (no cache, unlike relations), and the trap where ->addDay() on the way out changes nothing.
July 22, 2026
· Open the box · part 5 of 8
Assign 999, ask getDirty, and the model answers without touching the database: every Eloquent model carries a second copy of its row. Dirty tracking from the inside: where $original is born, why save() on a clean model sends zero queries, why a naive touched-flag resave costs 103 pointless UPDATEs, and the strcmp scar that makes "160.0" dirty while "160" is not.
July 21, 2026
· Open the box · part 4 of 8
Two tinker sessions race the same order: the read-modify-write update loses ten dollars on screen, a classic lost update. Why Eloquent's increment survives the race condition: an atomic SQL formula built in incrementEach, and why the model never forwards the call to the builder. The proof is one byte: a formula without a key updates all 103 orders at once.
July 20, 2026
· Open the box · part 3 of 8
Order::where() runs while method_exists says false, and the last episode left the question of who answers. A shared static builder dies of dirty state, a hand-rolled __callStatic almost passes, and then the vendor chain opens: a fresh instance per static call, __call, and a fresh Eloquent Builder from newQuery. The scars: a BadMethodCallException relayed and rewritten until it names the class you actually called, and query(), the honest door beside the hatch.
July 19, 2026
· Open the box · part 2 of 8
with('customer') folds an N+1 loop back to two queries and asks for Alice once. The obvious fix, a shared cache, prints the same numbers on three orders and dies at a hundred: 102 queries, because a cache answers one miss at a time. The real move collects every customer_id before the first question to customers exists, asks once with the list, and stitches the answers back through a dictionary. Built by hand, then found in the vendor, verb for verb.
July 18, 2026
· Open the box · part 1 of 8
$order->customer and $order->customer() come from one line on the model and behave nothing alike: the method hands back an open query and fires nothing, the property runs it once and caches. Build the naive version where a relation is a plain property and a single find becomes five queries, while the real thing stays one. Laziness is the default, eagerness a lever. Counted straight from the query log.
July 11, 2026
· The mail room · part 9 of 9
Two workers on one table never collide: the reserve query ends in FOR UPDATE SKIP LOCKED, read straight from the Postgres log. Then the fleet: a reservation is a lease whose term lives in the reader, a lease cannot tell dead from slow (one job, two live pids), the timeout alarm kills before the lease frees, and Supervisor refills the slot. Four clocks, byte-exact.
July 10, 2026
· The mail room · part 8 of 9
Chain three jobs and the queue holds one row - the other two are serialized inside the first, the catch() closure riding with them. A chain carries the group in its payload; a batch keeps it in a job_batches row every worker locks with SELECT ... FOR UPDATE. Two ways to remember a group, two currencies. Read from the payload and the ledger.
July 9, 2026
· The mail room · part 7 of 9
Two syncs for the same product, two workers, two DONE - and a job still in the queue. Job middleware wraps handle() in a pipeline: WithoutOverlapping is a lock, RateLimited a budget, ThrottlesExceptions a breaker, and each can end a job without ever running it. Read straight from cache_locks and failed_jobs.
July 8, 2026
· The mail room · part 6 of 9
A job can hang, throw, or give up on its own terms, and Laravel keeps a separate limit for each. #[Timeout] lives on the daemon worker, retryUntil() is a deadline not a count, release() ends in DONE and not the morgue, #[MaxExceptions] and $this->fail() cut it short. Six limits, read straight from the tables.
July 7, 2026
· The mail room · part 5 of 9
Flip ShouldQueue on a listener and the spinner drops from 2004 ms to 5.3 ms. Then open the payload: CallQueuedListener carries the listener by name, the event by value, the model as a claim tag - and fire() → call() → handle() finishes the route. A queued listener is a job the framework wrote for you.
July 6, 2026
· The mail room · part 4 of 9
A job throws - one attempt, then the morgue: failed_jobs keeps the uuid, the cause of death and the payload intact. queue:retry raises the same job into today's code, #[Tries] and #[Backoff] buy it patience, and failed() speaks the last word. Every step read straight from the tables.
July 5, 2026
· The mail room · part 3 of 9
Five problems from real development, five jobs built for them: a reminder that waits, an order that respects its transaction, an invoice that refuses to exist twice, a card number nobody else can read, a checkout with a fast lane - every switch verified with a SELECT.
July 4, 2026
· The mail room · part 2 of 9
Pass an Eloquent model to a job and the payload keeps a claim tag - class, id, relation names, connection. The worker trades it for fresh rows: ships 999 when you dispatched 250, throws when the row is gone. Snapshots, DeleteWhenMissingModels, WithoutRelations - every corner verified live.
July 3, 2026
· The mail room · part 1 of 9
What a job actually is - built from the empty stub up: dispatch it, catch the empty-queue surprise, open the payload with a SELECT, hire the worker, and measure what the mailbox buys. The chapter the queue docs never wrote.