Generics we already have

A template calls getEmail() on an Order at three in the morning. The method returned array, the docblock said “array of User”, and eight months later someone put an Order in it. Everything was fine until it was not. PHP still has no generics and will not get them soon. Meanwhile PHPStan and Psalm shipped them anyway, in docblocks. Checked at analysis, not at runtime. For everyday backend work that turns out to be most of the value. ...

October 9, 2021 · 2 min · Murat Useinov

Attributes in Symfony 5.3

#[Route('/orders/{id}')] right above the method, and nothing in config/routes. Symfony 5.3 came out last week, and with PHP 8 attributes configuration finally found its place. Routes, autowiring hints, some validation, native syntax next to the code: #[Route('/orders/{id}', methods: ['GET'])] public function show(int $id): Response { // ... } I spent years defending YAML routing. The argument was separation: code is code, wiring is wiring, one file shows the whole URL map. The argument was never wrong. It lost to practice. In every real project the first thing you do with a route is jump to the controller, and the first thing you do with a controller is wonder which route hits it. Two files, one mental join, forever. Attributes remove the join. Rename a method, the metadata moves with it. Delete the class, no orphaned YAML block stays behind to confuse the next person. ...

June 4, 2021 · 2 min · Murat Useinov

Symfony Runtime: front controller as a callable

public/index.php, unchanged since I learned Symfony. Create the request from globals, run the kernel, send the response, terminate. Symfony 5.3 arrives next month, and the Runtime component rewrites this file. It looks like a small refactoring. It is a statement about where PHP is going. Baked into those few lines is one big assumption: one process, one request, then we die. FPM made the assumption true for fifteen years, so nobody saw it as an assumption. ...

April 16, 2021 · 2 min · Murat Useinov

PHP 8 in production: dependencies first

composer why-not php 8.0. That is the first command of the migration, and it has nothing to do with syntax. PHP 8.0 is six weeks old. Twitter is full of match expressions and constructor promotion. Meanwhile the real upgrade of a working project is a dependency problem. Your code is maybe twenty percent of what runs in production. The rest is the framework, thirty packages and a dozen extensions, and any one of them can be the blocker. ...

January 8, 2021 · 2 min · Murat Useinov

PHP 8.0: ignore the JIT, take the rest

JIT on, JIT off, the same Symfony endpoint. Difference within noise. Same for a Laravel endpoint. PHP 8.0 came out yesterday and this was the first thing I checked, because every headline is about the JIT. Not a scandal. A web request spends its time in I/O, in MySQL, in framework code full of method calls the JIT cannot do much with. JIT is for tight numeric loops, and your controller has none. If you compute fractals in PHP, congratulations. The rest of us can leave it off. ...

November 27, 2020 · 2 min · Murat Useinov

Average response time explains nothing

Average response time: 180 ms. Ticket from support: “the API is slow”. Both true. The average is a diplomat. It offends nobody and tells you nothing. Latency is a distribution. Our 180 ms hides a p50 of 90 ms and a p99 above four seconds. One request in a hundred is terrible, and with thirty requests per page load, most users hit that unlucky one regularly. The people complaining are not imagining things. They live in the tail, and the average never visits there. ...

August 4, 2020 · 2 min · Murat Useinov

PHP 8 will retire some of our workarounds

@param int|string $id. I grepped one service this morning: dozens of those, and every one is a wish. The docblock says what the signature could not. First alpha of PHP 8 came out last week, and native union types turn the wish into a contract, checked at runtime and by tooling. Half of my phpdoc can go. Everyone writes about the JIT. I keep thinking about the less shiny thing: how many of our daily patterns exist only to compensate for a missing language feature, and are about to become legacy. ...

July 3, 2020 · 2 min · Murat Useinov

Types are for reading, not for the compiler

OrderRepository in a constructor. Click. I am there. That is how I explore a big Symfony project now, and I noticed my main reason for types has changed. Not bug catching anymore. Navigation. Symfony 5.1 is out, PHP 7.4 is everywhere I work, and the IDE knows every caller and every implementation. Compare with the array-passing style we all wrote for years: public function register($data) { // what is in $data? read three call sites to find out } versus ...

June 27, 2020 · 2 min · Murat Useinov

Waiting for three APIs, one at a time

Three external calls on one page: prices, stock, delivery estimate. Each answers in about 300 ms. The page waits a full second, because we call them one after another. PHP is synchronous, what can you do. Turns out, something. Symfony HttpClient is lazy. request() sends and returns immediately. The waiting happens when you read the response. So start all three, read later: $prices = $client->request('GET', $pricesUrl); $stock = $client->request('GET', $stockUrl); $delivery = $client->request('GET', $deliveryUrl); $data = [ 'prices' => $prices->toArray(), 'stock' => $stock->toArray(), 'delivery' => $delivery->toArray(), ]; Under the hood it is curl multi. Three requests fly at once, wall time is the slowest one instead of the sum. Our second became 350 ms. No swoole, no reactphp, no async rewrite. Same boring controller. ...

April 21, 2020 · 2 min · Murat Useinov

A Redis lock is a promise you cannot fully keep

Two workers, one order, processed twice. Every project gets this day. Someone writes SETNX, calls it a distributed lock, closes the ticket. I want to slow down here, because the ticket is not closed. The small bugs first. A lock needs a TTL, or a crashed worker holds it forever. A lock needs an owner token, or worker A releases the lock of worker B: $token = bin2hex(random_bytes(16)); $ok = $redis->set('lock:order:'.$orderId, $token, ['nx', 'ex' => 30]); And the release must be atomic: compare the token and delete in one Lua script. Check in PHP, delete in a second command, and there is a gap. Something will land in that gap. ...

February 26, 2020 · 2 min · Murat Useinov