Symfony 4: the framework as recipes

A kernel, an index.php, a config folder with almost nothing in it. That is a Symfony 4 project on day one, and 4.0 came out today together with 3.4 LTS. The code changes are the smaller story. How a project starts is the bigger one. The Standard Edition is gone. You start from the empty skeleton and ask for what you need. composer require orm pulls Doctrine, and the recipe writes the config files, registers the bundle in bundles.php, adds the variables to .env. You watch your own repo grow file by file as you add dependencies. ...

November 30, 2017 · 2 min · Murat Useinov

PostgreSQL 10: partitions I can finally use

DROP TABLE events_2017_10. That is how you delete October now, and it is the reason I am reading PostgreSQL 10 release notes on a Thursday evening instead of waiting a year like usual. Partitioning existed before, through inheritance, CHECK constraints and an insert trigger you wrote yourself and hoped was right. Now the database owns it: CREATE TABLE events ( id bigserial NOT NULL, created_at timestamptz NOT NULL, payload jsonb ) PARTITION BY RANGE (created_at); CREATE TABLE events_2017_10 PARTITION OF events FOR VALUES FROM ('2017-10-01') TO ('2017-11-01'); The win is the data lifecycle. An events table grows forever, and deleting a year of history with DELETE is a night of I/O plus a bloated table in the morning. With partitions, retiring a month is one DROP. Instant. And a query that filters by created_at visits only the partitions in range, the planner skips the rest. ...

October 26, 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

Laravel 5.5 and package auto-discovery

Laravel 5.5 came out yesterday, an LTS: two years of bug fixes, three of security fixes. Good news for projects that outlive the hype cycle, which is most of them. The feature in every screenshot is package auto-discovery. A package declares its service provider in its own composer.json, and after composer require it is registered. No editing of config/app.php, no provider line copied from the readme. Convenient. Now look at what got deleted. Installing a package used to be two steps, and the second step was consent: a line in my own config saying this code runs inside my application at boot. Now composer require is the whole ceremony. The list of code that runs at boot lives in vendor/, spread over other people’s files. php artisan package:discover shows it, and dont-discover in composer.json switches it off per package. But the default flipped from explicit to implicit. ...

August 31, 2017 · 2 min · Murat Useinov

Multi-stage builds for PHP images

720 MB. That was our production PHP image, and most of it was composer, git, unzip, build headers and a pile of apt cache. None of it runs in production. It was there because composer install needs it, and a Dockerfile was one linear script. People worked around this with two Dockerfiles and a shell script gluing them. Ugly, and everyone’s glue was different. Docker 17.05 brought multi-stage builds. The first Docker feature in a while that fixes a problem I actually had. Now it is one file: ...

July 7, 2017 · 2 min · Murat Useinov

Keyset pagination instead of OFFSET

LIMIT 50 OFFSET 500000. Page 10001 of an events table, from a paginator someone wrote in an afternoon. Postgres has no shortcut to row 500000. It walks the index through half a million entries, fetches them, throws them away, and returns fifty. Page one is fast. Page ten thousand is slow, and every page after it is slower. Run EXPLAIN ANALYZE on both: the plans are identical, the numbers are not. ...

June 19, 2017 · 2 min · Murat Useinov

Private services: the container steps back

$container->get('app.mailer') stopped working this week. Symfony 3.3 is out, and in the new configuration services are private by default. The container refuses the call. Everyone is writing about autowiring. This is the change I like more. That call was always a smell. A class that pulls dependencies from the container by string id is a class with secrets. Its constructor says nothing, and to test it you boot half the framework, because it may ask for anything at any moment. ...

May 30, 2017 · 2 min · Murat Useinov

ALTER TABLE is part of the deploy

Forty million rows, three application instances behind a balancer, one migration that renames a column. The deploy script runs migrations first, then rolls the code instance by instance. For a few minutes old code was writing to a column that no longer existed. The error rate graph looked like a wall. I had approved that migration. It was one line. Migration tools trained us to think a schema change is a code change. Write a file, run it, done. A schema change on a big table takes time, takes locks, and while it runs the previous version of the application is still serving traffic. Both versions must survive both schemas. That is the whole discipline, and it has a name: expand and contract. ...

April 24, 2017 · 2 min · Murat Useinov

Autowiring is coming to Symfony 3.3

Our services file repeats every class name three times. Once as the id, once as the class, and again in the arguments of every service that needs it. Add a constructor parameter, edit YAML, clear cache, repeat. Symfony 3.3 lands in May and promises to delete most of that file: services: _defaults: autowire: true autoconfigure: true AppBundle\: resource: '../../src/AppBundle/*' Autowiring and autoconfiguration on by default, the whole source folder registered in five lines. I have mixed feelings. Sorted, they look like this. ...

March 8, 2017 · 2 min · Murat Useinov

Cache stampede: the hot key problem

Cache hit rate 98 percent. The database still fell over. Both facts are true, and the second one does not care about the first. What happened. One key holds the result of a heavy query for the main page. TTL one minute. At some second the key expires. In that second a few hundred requests all get a miss, and all of them go to recompute the same heavy query. Redis is fine. MySQL is not. This is a cache stampede, and hit rate will not warn you, because hit rate is an average. The stampede lives in the worst second. ...

February 7, 2017 · 2 min · Murat Useinov