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

The fastest request never reaches PHP

One header, and the FPM load graph fell off a cliff. Cache-Control: public, s-maxage=60 A public catalog page on one project renders the same HTML for every anonymous visitor. Same queries, same JSON from the search service, same template, thousands of times per hour. We profiled it, we tuned it, and only then asked the obvious question: why is PHP involved in the second request at all. HTTP had the answer before my career started. public says a shared cache may store the response. s-maxage gives the CDN or reverse proxy its own lifetime, separate from browser max-age. With Varnish in front, the request path splits in two. MISS: full stack, FPM, database, sixty milliseconds. HIT: the proxy answers from memory, the PHP process never starts, the database never hears about it. Nothing in the application got faster. There was simply less application running. ...

December 3, 2024 · 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

PostgreSQL 17 and why VACUUM is an API concern

A reporting transaction open for hours, and n_dead_tup on the hot table climbing all day while autovacuum ran non-stop and changed nothing. That was one project. PostgreSQL 17 came out yesterday, and the headline work is exactly this plumbing: vacuum got a new memory structure for tracking dead tuples, far less memory and no longer capped the old way, plus better throughput for high-concurrency writes and bulk loads. Maintenance internals. Which is why backend people should care. ...

September 27, 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

WebSockets change your operational model

Every client reconnects at once. That is what a deploy means now on one project of mine, since Reverb made WebSockets a first-party Laravel feature this spring and order updates go to the browser instead of polling. The code was the easy part. A classic PHP app is stateless between requests. An FPM worker takes a request, answers, forgets. Capacity planning is requests per second. A WebSocket server is the opposite animal: thousands of open connections that mostly do nothing, but each one holds memory and a file descriptor, and each one is state that dies with the process. Your quiet realtime feature has a thundering herd built in. Plan for reconnect storms, raise descriptor limits, and make the client reconnect with jitter, never on a fixed timer. ...

July 3, 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

Symfony 7.1 and the value of boring releases

composer update, the changelog over coffee, two deprecation notices fixed, done before lunch. That was Symfony 7.1 on one project this morning. Nothing in the release will change your architecture, and I mean this as a compliment. There are nice small things. An attribute that maps an uploaded file straight into a controller argument, one more piece of request plumbing gone. A new experimental component for type introspection that libraries will quietly benefit from. A layer of deprecations preparing the next major. That is the whole show. ...

May 31, 2024 · 2 min · Murat Useinov

MySQL 8.4 gives us a place to stand

Half the industry is sitting on MySQL 5.7 right now, unsupported since last autumn, and calling it stability. MySQL 8.4 came out today, and the important word in the announcement is not a feature. It is LTS. First long-term release of the new model: innovation releases every quarter for the brave, an LTS every couple of years for production, bugfixes only after that. People underestimate how much pain the old model caused. 8.0 was a rolling target. A “minor” 8.0.x could change optimizer behavior or deprecate something you used. Staying current meant re-testing, staying behind meant missing security fixes. ...

April 30, 2024 · 2 min · Murat Useinov