Laravel 11 removes files, not decisions

The diff of a Laravel 11 upgrade on one mid-size API this week is mostly deletions. Almost empty app/, configuration in one fluent bootstrap file, the slim skeleton I wrote about in January now real. It feels great. Now look at what survived untouched. The use case class that wraps an order state change in a transaction. The listener that must fire only after commit, because it queues an email about a row that must exist. The cache invalidation that follows every write to a heavily read table. The decision that validation lives in a request class and business rules one layer deeper. None of this is skeleton. None of it got smaller. ...

March 25, 2024 · 2 min · Murat Useinov

One trace through HTTP, queue and database

A request creates a job. The job calls an external API and writes to the database. Something in this chain is slow, and the logs tell four disconnected stories. Grepping a request id across services is our folklore. OpenTelemetry finally makes the boring standard version practical in PHP. The core is small. One trace id for the whole causal chain, a span id for every operation, and one rule: pass the context along. Over HTTP it is a single header: ...

February 8, 2024 · 2 min · Murat Useinov

The shrinking Laravel skeleton

No app/Http/Kernel.php. That is the first thing you notice in the Laravel 11 preview, and the first thing every comment thread asks: where did my middleware go. The skeleton is the loudest change this quarter, louder than any feature. An app/ directory that is almost empty, no folder of middleware stubs, a pile of service providers collapsed into one, config files trimmed. The middleware did not go anywhere. It moved into the framework. The mechanism exists exactly as before, only the file does not, and bootstrap/app.php gets a small fluent API to reconfigure the stack when you actually need to. ...

January 6, 2024 · 2 min · Murat Useinov

AI assistants do not know your production

A DTO with a dozen fields, a framework migration, a slow query incident. Three tasks from the last few months, all done with Copilot and ChatGPT open. Here is where the line runs for me. Task one, boilerplate. The DTO, its validation rules, a test with fixtures. The assistant writes this faster than I type, and the result needs only a quick read. Same for converting an array-shaped legacy structure into typed classes. This is honest time saved, maybe an hour a day. The code is the kind where being generic is correct. ...

December 23, 2023 · 2 min · Murat Useinov

PHP 8.3 and Symfony 7 in one month

public const int TIMEOUT_SECONDS = 30; One week old, and the one thing I waited for from PHP 8.3. Busy November. 8.3 released last week, and yesterday Symfony shipped 6.4 LTS and 7.0 on the same day. Time to plan, not to jump. Typed class constants are a small thing, but constants were the last untyped island in a class, and a child class could silently redefine one to a different type. Also json_validate(), which checks JSON syntax without building the whole tree in memory. Everyone had a helper that did json_decode and threw the result away. Now the helper can retire. ...

November 30, 2023 · 2 min · Murat Useinov

FrankenPHP: the server absorbs PHP

One binary, my code, a Caddyfile. That is the whole deployment of a small internal service I moved to FrankenPHP in staging this week. The classic PHP deployment is a couple: Nginx in front, PHP-FPM behind, FastCGI between them. Two configs, two processes to supervise, one socket to misconfigure. It works, we all know it by heart, and knowing it by heart is the only reason we call it simple. ...

October 27, 2023 · 2 min · Murat Useinov

PostgreSQL 16, read through one workload

A nightly import, a few hundred million rows through COPY, then reporting queries in the morning. That is the only workload I care about, so that is how I read the PostgreSQL 16 release notes: with one pipeline in mind and everything else skipped. Three items survived the filter. COPY got faster, noticeably when several loads run at once. Our import is exactly this shape, parallel COPY streams into partitioned tables. I will take the free speed. But the old lesson stays: if your import still does row-by-row INSERT from PHP, no Postgres release will save you. Batch into COPY first, then talk about versions. ...

September 16, 2023 · 2 min · Murat Useinov

UUID primary keys and the B-tree bill

The same table, the same rows, about 1.5 times the disk. That was the measurement after someone made UUIDv4 the primary key in InnoDB, and that is before counting secondary indexes. Every new API wants UUIDs. Distributed generation, no coordination, nothing leaks about row counts. All true. Then the database quietly starts paying. InnoDB clusters the table by primary key. Auto-increment inserts always land in the rightmost page, warm and predictable. Random UUIDs land anywhere in the tree: page splits everywhere, half-empty pages, and the set of hot pages becomes the whole index. Secondary indexes store the primary key in every entry, so each of them inherits the full 16 bytes too. In PostgreSQL the heap does not cluster, so the pain is smaller, but index bloat and lost locality are still there. ...

August 3, 2023 · 2 min · Murat Useinov

MySQL now has two speeds

MySQL 8.1 came out this week, and its support window is measured in months. That is by design. Oracle changed the release model: two tracks now. Innovation releases every quarter or so, 8.1 is the first, short support, new features fast. And an LTS line with years of support, promised for next year. 8.0 stays on bugfixes until its end of life in 2026. For years the question was “when do we finally move to 8.0”. Now the question has a second dimension: which track are you even on. That is a product decision, and it is worth making explicitly instead of by inertia. ...

July 20, 2023 · 2 min · Murat Useinov

Outbox: events that do not lie

An email confirming an order that does not exist. A colleague showed me this one last week, together with the mirror bug: an order that exists and never got its email. The code was the obvious kind. Commit the Doctrine transaction, then dispatch to the AMQP transport. Two systems, two writes, no shared transaction. Publish after commit and the process can die between them, event lost. Publish inside the transaction and the broker gets an event for data that may still roll back. Both orders are wrong, and under real load this fires weekly. ...

June 7, 2023 · 2 min · Murat Useinov