Static state outlives the job

One static array, from one of our projects: class Settings { private static $cache = []; public static function get($tenantId, $key) { if (!isset(self::$cache[$tenantId])) { self::$cache[$tenantId] = self::load($tenantId); } return self::$cache[$tenantId][$key] ?? null; } } A reasonable per-request cache. Under PHP-FPM it died with the process. In a queue worker it lives forever. PHP had one great architectural feature nobody put in the manual: the process died after every request. Leak memory, cache nonsense in a static, forget to close things. Did not matter. The dying process forgave everything. ...

December 22, 2019 · 2 min · Murat Useinov

PHP 7.4: typed properties first, preloading later

Two lines of a class, and half of my docblocks can be deleted: class Money { private int $amount; private string $currency; } PHP 7.4 came out yesterday, and this is the part I start using on Monday. Those docblocks existed only to say @var int. Now the language says it, and unlike the docblock, it checks. Assign a string, get a TypeError at the assignment, not a strange bug three layers later. There is one new state to learn. A typed property without a default is uninitialized, and reading it before the first write throws. This is the feature. “Object exists but is not filled yet” finally fails loudly instead of pretending to be null. ...

November 29, 2019 · 2 min · Murat Useinov

200 OK is not health

Route /health, returns ok, everyone happy. That is the first version of every health endpoint on the project we are moving to Kubernetes. Then somebody pastes the same path into livenessProbe and readinessProbe, and a thirty second database hiccup becomes a long evening. The two probes ask different questions. Readiness asks: should this pod get traffic right now. Here it is correct to check dependencies. Database unreachable, cache cold, migrations still running: answer no. Kubernetes takes the pod out of the Service, traffic goes to the others, the pod returns when the world improves. Failing readiness is cheap and reversible. A polite “not now”. ...

August 7, 2019 · 2 min · Murat Useinov

One use case, one class

Four hundred lines in a checkout action, half of them inside one try. That is one way a project dies. The other is OrderManager: thirty methods, six injected services, and nobody can say what the class is for, because it is for everything about orders. Both diseases have the same cure and it is embarrassingly simple. One use case, one class, one public method. final class PlaceOrder { public function __construct( OrderRepository $orders, PaymentGateway $payments, EventDispatcher $events ) { ... } public function handle(PlaceOrderCommand $command): OrderId { // the whole story, top to bottom } } The shape answers three questions by itself. What comes in: one command object, a thing you can log, validate, put in a queue. What it needs: the constructor lists dependencies of this use case, not of orders in general, so when PlaceOrder suddenly needs the mailer you see it and can ask why. What comes out: one result. Reading handle() from top to bottom tells the whole story of placing an order. No chapter hidden in a base class or a trait. ...

June 26, 2019 · 2 min · Murat Useinov

PHP 7.3 and the value of small releases

A trailing comma after the last argument of a function call. PHP 7.3 came out at the start of the month, and this is the feature I noticed first, because I meet the missing comma in every second diff: $this->logger->info( 'order created', ['order_id' => $order->id], ); A tiny thing that kills a whole class of noisy diffs, the ones where adding an argument touches the previous line too. Arrays got this years ago. Calls only now. ...

December 27, 2018 · 2 min · Murat Useinov

Rector: refactoring by machine

Search for ->fetch( in a project with old Kohana code and count the hits. A method call on a model, a comment, a line in a test fixture, an unrelated class that happens to have a method with the same name. That is what regex refactoring looks like. Regex does not know a method call from a string literal. Rector does. A young tool I found this month: it parses PHP into an AST with nikic/php-parser, applies transformation rules, prints the code back. Rename a class across the whole project. Change a method call, add an argument everywhere. The AST sees that this fetch is called on that type and touches only those places. Mechanical change becomes exact. ...

February 8, 2018 · 2 min · Murat Useinov

Sodium in PHP 7.2 core

PHP 7.2 came out on the last day of November. Mcrypt is out of core, libsodium is in. That trade alone makes it a good release. Search any forum for “php encrypt” and you find the same folk recipe: openssl_encrypt with AES-256-CBC, an IV made from who knows what, no authentication of the ciphertext. Every choice in that recipe is a place to be wrong, and CBC without a MAC is wrong in a way that has published attacks. The developer is not careless. The API hands an application developer decisions that belong to a cryptographer. ...

December 10, 2017 · 2 min · Murat Useinov

First run of PHPStan on legacy code

Level 0, a legacy codebase, a few hundred errors on the first run. That was my week with PHPStan. Most of the output was noise about magic the tool cannot see. But in the first hour of reading I found three real bugs, live in production for months. One: a repository method returns an entity or null, and a caller chains a method right on the result. The not-found branch was never written. It survived because that path needs a deleted record, and deleted records are rare. Rare is not never. ...

September 9, 2017 · 2 min · Murat Useinov

PHP 7.1: nullable is honest

// returns User or false, see wiki public function findByEmail($email) Every legacy codebase has this method. The comment is the type system. Half the callers check for false, some check for null because a sister method returns null, one caller checks nothing and works by luck. PHP 7.1 came out last week. Nullable types, void, iterable, multi-catch. Small features, but together they continue the direction 7.0 started: less implicit agreement, more signature. The one I care about is ?Type: ...

December 9, 2016 · 2 min · Murat Useinov

Doctrine and the 100 000 row import

Row sixty thousand. Allowed memory size exhausted. The import script on one project died there, and the code was the obvious loop: read a row, persist() an entity, next row, flush() at the end. On a hundred test rows it worked perfectly. It was my loop. The reason is the Unit of Work. Doctrine keeps every managed entity in memory, plus a snapshot of its original data for change tracking. Persist a hundred thousand entities and you hold a hundred thousand objects twice. This is not a bug. It is the price of the ORM’s main feature, and on a normal web request the price is invisible because the request dies young. ...

June 19, 2016 · 2 min · Murat Useinov