PHP 8.4 after the hype

A private field, a getter, a setter with one line of normalization. Three members for one idea, repeated through every entity folder I have ever opened. PHP 8.4 is two months old, the conference demos are done, and the honest question is which features earn a place in a working codebase. My filter is simple. A feature is good when it deletes code. Property hooks pass: class User { public string $email { set => strtolower(trim($value)); } } The class is shorter and the normalization cannot be bypassed. But keep hooks boring. A hook that talks to the database is a magic getter from 2008 in new syntax. ...

January 15, 2025 · 2 min · Murat Useinov

Lazy objects move into the engine

Fifty ghosts initialized in a loop are still fifty one queries. PHP 8.4 is out since Thursday. Property hooks get the headlines, but the feature I keep coming back to is native lazy objects. It legalizes a trick ORMs have done with generated code for fifteen years. The trick: you load an order, $order->customer should be a Customer, and you do not want a query until someone actually touches it. Doctrine generates a proxy class at build time, a subclass that overrides every method with “initialize first, then call parent”. It works. It is also a pile of magic: generated files, edge cases with final classes and private properties, strange things in var_dump. ...

November 23, 2024 · 2 min · Murat Useinov

Property hooks, before the hype settles

Six lines of getter and setter ceremony around one strtolower. PHP 8.4 is in release candidates, and property hooks delete exactly that: class Customer { public string $email { set => strtolower(trim($value)); } } Assignment stays assignment, $customer->email = $input, and normalization happens on the way in. For this exact case, one value, no dependencies, I am convinced. The property remains a property. Where I get careful is everything past normalization. A setter method is ugly, but it announces itself. setEmail() in a call stack tells you code ran. $obj->email = $x looks free, and with hooks it is not. Put validation that throws into a hook, and every plain assignment becomes a possible exception, invisible at the call site. Put a side effect in, and you have built magic the next person discovers through a debugger at midnight. We spent years removing __get and __set from codebases for this exact crime. Hooks are their respectable cousins, better typed, analyzable, and the temptation is identical. ...

October 14, 2024 · 2 min · Murat Useinov

Pick the library with a profiler, not a chart

Two hundred thousand ops per second against eighty. That was the bar chart, and the team was ready to pick a serializer from it. I asked for one day. The day went like this. Take a representative workload. Not a synthetic three-field object, but a hundred real payloads from production logs, with the nested structures, the nullable mess, the one field that is sometimes a list and sometimes a map. Anonymize them, save as fixtures. This step is the whole method. The bar chart was measured on someone else’s data with someone else’s configuration, and a serializer is exactly the kind of code whose cost depends on the shape of the input. ...

August 7, 2024 · 2 min · Murat Useinov

A faster sprintf will not fix your p99

Three times faster. That is the number in the benchmark posts about PHP 8.4, where simple sprintf() calls with basic placeholders get turned into plain string operations at compile time. The function call disappears. for ($i = 0; $i < 1000000; $i++) { $s = sprintf('%s-%d', $prefix, $i); } I like this work. I do not like how people read it. A microbenchmark measures one function in a loop, alone, with a warm cache and nothing else to do. Now profile a real request from the same application. In every backend I have profiled the picture has the same shape: most of the wall time is waiting. SQL queries, Redis round trips, an HTTP call to some partner API. The CPU spent inside PHP is a modest slice, and sprintf inside that slice is a rounding error. Three times faster rounding error is still a rounding error. ...

June 18, 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

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

Persistent PHP workers need discipline

A static array with the comment “cache, cleared per request”. It went to production on RoadRunner last month, and the memory graph turned into a staircase. PHP had one superpower nobody advertised: the request died. Every leak, every forgotten static, every open transaction was erased when the process shut down. Shared-nothing was not architecture. It was amnesia, and amnesia forgave us everything. RoadRunner and Swoole take it away. The worker lives for thousands of requests, and the class of bugs changes on the first day. That “per request” cache became a cache per worker lifetime, and the supervisor killed the process when it ran out of memory. A logger kept the request id in a property, so entries from user B carried the id of user A. And the best one: an exception in the middle of a Doctrine transaction left the connection with the transaction open, and the next request on that worker silently joined it. ...

April 3, 2023 · 2 min · Murat Useinov