Scheduler component, or cron as messages

crontab -l under a login nobody remembers creating. That is where the real business logic of one server lived: a nightly cleanup, an export, a retry script. Symfony 6.3 came out yesterday with a Scheduler component. Periodic tasks defined in PHP, executed as Messenger messages. Experimental, but the idea deserves a note. #[AsSchedule('default')] final class MainSchedule implements ScheduleProviderInterface { public function getSchedule(): Schedule { return (new Schedule())->add( RecurringMessage::every('10 minutes', new CleanupExpiredCarts()), ); } } Then messenger:consume scheduler_default, and a worker fires the messages. ...

May 31, 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

DI attributes: config moves into the class

Three lines of YAML in a file two directories away, for one integer that one service reads. That was the first thing I converted when I started moving a project’s service config from YAML into attributes, to see where the line is. public function __construct( #[Autowire('%env(int:IMPORT_BATCH_SIZE)%')] private int $batchSize, ) {} Now the class tells you everything about itself. Same with #[TaggedIterator] for collecting all implementations of an interface, and #[AsDecorator] for wrapping a service. These are local facts. A local fact belongs next to the code it describes. When I open the class, I want to stop searching. ...

March 27, 2023 · 2 min · Murat Useinov

Laravel 10 is a quiet major

Process::run('gzip -k dump.sql'). Out of the whole Laravel 10 release this week, that is the line I will actually use. The rest is quiet. Skeleton and framework code got native type declarations instead of docblocks, PHP 8.1 is the floor, and there is the Process facade. That is roughly the whole story. Some people are disappointed. I am not. A major release that is mostly maintenance means the framework is an adult. ...

February 16, 2023 · 2 min · Murat Useinov

Before Laravel 10: raise the baseline first

composer why-not php 8.1. One command, and it tells you who holds you back. Run it before making any plans. Laravel 10 comes next month and requires PHP 8.1. One project I help with is on Laravel 9 and PHP 8.0. The temptation is one heroic branch: new PHP, new framework, new package versions. When that branch breaks in production, you will not know which of the three changes broke it. ...

January 7, 2023 · 2 min · Murat Useinov

PHP 8.2: stricter by default

PHP 8.2 was released on December 8. My checklist for it looks like every other December, which is the whole point of a yearly cadence. On the surface a quiet release. Readonly classes, DNF types, standalone true, false and null types, constants in traits. Underneath, the more important half: another round of cleaning old dynamic behavior. Dynamic properties deprecated, ${var} string interpolation deprecated, utf8_encode deprecated. The language keeps trading looseness for predictability, and I keep voting for the trade. ...

December 16, 2022 · 2 min · Murat Useinov

Readonly classes for value objects

Two readonly keywords for two properties in a two-property class. That is what an immutable Money looks like in PHP 8.1: final class Money { public function __construct( public readonly int $amount, public readonly string $currency, ) {} } PHP 8.2 is at release candidate stage, and the feature I am waiting for is this one. The keyword moves up and says it once: final readonly class Money { public function __construct( public int $amount, public string $currency, ) {} } Every property is readonly, and the class refuses dynamic properties on top. Cosmetics, yes. But cosmetics that make the right thing the short thing, and that changes what people actually write. ...

November 8, 2022 · 2 min · Murat Useinov

MERGE arrives in PostgreSQL 15

A supplier price list, loaded into staging_prices, then reconciled against prices. Since PostgreSQL 15 came out two weeks ago, that is one statement: MERGE INTO prices p USING staging_prices s ON p.sku = s.sku WHEN MATCHED AND s.price IS NULL THEN DELETE WHEN MATCHED AND p.price <> s.price THEN UPDATE SET price = s.price, updated_at = now() WHEN NOT MATCHED THEN INSERT (sku, price) VALUES (s.sku, s.price); Before 15 this was three statements in a transaction, or a stored procedure, or a loop in PHP. Now it is one statement that says what it does. Update changed rows, delete withdrawn ones, insert new ones. People coming from Oracle and SQL Server waited a decade for this. The conditional WHEN MATCHED AND ... branches are the real value. ON CONFLICT cannot express “delete when the source says so” at all. ...

October 27, 2022 · 2 min · Murat Useinov

Dynamic properties are leaving

$invoice->totall = 250; One extra letter. No error, no warning. A second property appears, the real total stays null, and the bug surfaces three screens later as “total is empty sometimes”. I have hunted this exact bug. Twice. Both times it took hours, because the write looks correct and the read looks correct. They just disagree on one letter. PHP 8.2 lands in December, and the deprecation that will touch the most code is this one. Writing to an undeclared property becomes deprecated. In some future major it becomes an error. For fifteen years it was legal: ...

September 3, 2022 · 2 min · Murat Useinov

Fast query, slow endpoint

The profiler said the query takes 20 ms. The export endpoint took 900 ms and a quarter of a gigabyte of memory. The missing 880 ms was Doctrine turning five thousand rows into five thousand entities. Hydration is not free. For every row the ORM builds an object, fills properties through reflection, registers it in the unit of work, creates proxies for relations. Per row it is nothing. Times five thousand, it is the endpoint. The profiler shows SQL because SQL is easy to show. ...

August 23, 2022 · 2 min · Murat Useinov