A worker is not a controller

All the logic inside execute(), exit code always zero, no lifecycle thinking at all. A controller without a request. I still see this console command in every second codebase. Symfony stopped being only an HTTP framework a long time ago, but habits are slower than releases, and with 8.1 giving console and workers first-class attention the excuse is gone. An HTTP request lives for milliseconds. The kernel builds services, handles, throws everything away. A worker lives for hours. Same container, very different lifecycle. Every service that quietly keeps state, an in-memory cache, an accumulating buffer, an entity manager full of tracked objects, is invisible in HTTP and becomes a memory leak in a worker. If your consumer needs a nightly restart by cron, this is where the night went. ...

May 25, 2026 · 2 min · Murat Useinov

The November upgrade train

PHP 8.5 came out yesterday. Symfony 7.4 LTS and 8.0 land at the end of the month, same as every year. November is upgrade month now. First the cargo, then the timetable. From 8.5 I care about three things. The pipe operator makes transformation chains read left to right: $slug = $title |> trim(...) |> strtolower(...) |> (fn ($s) => preg_replace('/\s+/', '-', $s)); The new URI extension puts standards-correct URL parsing in core. parse_url had opinions instead of a specification, I will not miss it. And #[\NoDiscard] lets a method declare that ignoring its return value is a bug. Made for result-style APIs, where a dropped return is a swallowed error. ...

November 21, 2025 · 2 min · Murat Useinov

Symfony 7.3 and the code I get to delete

Symfony 7.3 was released this week. I opened the release notes with a text file next to them. The file is a list of things to delete. Every project older than three years carries a layer of custom helpers that were reasonable when written. The framework moves, the helpers stay. Nobody reviews them, because they work. Two examples from one project. A base console command class with sugar for arguments and options, written around 2021. Symfony 7.3 has invokable commands, arguments come as parameters with attributes: ...

May 30, 2025 · 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

PHP 8.3 and Symfony 7 in one month

public const int TIMEOUT_SECONDS = 30; One week old, and the one thing I waited for from PHP 8.3. Busy November. 8.3 released last week, and yesterday Symfony shipped 6.4 LTS and 7.0 on the same day. Time to plan, not to jump. Typed class constants are a small thing, but constants were the last untyped island in a class, and a child class could silently redefine one to a different type. Also json_validate(), which checks JSON syntax without building the whole tree in memory. Everyone had a helper that did json_decode and threw the result away. Now the helper can retire. ...

November 30, 2023 · 2 min · Murat Useinov

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

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

Argument resolvers and the HTTP border

Forty lines of $request->get() before the first line of real logic. That was the controller nobody wanted to touch, and the Symfony 6.1 upgrade was my excuse to open it. An action that reads ten request fields by hand does two jobs. It translates HTTP into data, and it runs the use case. The first job is boring and repeated in every action, which is exactly why it should not be written by hand ten times. Symfony has argument resolvers for this since 3.1. People just do not use them for their own types. ...

June 13, 2022 · 2 min · Murat Useinov

Retry is a strategy, not a loop

A failed job that retries every second, one hundred times, against a payment provider that is already down. That is a small DDoS with a queue in front of it. If the provider needs five minutes to recover, hammering it for the first two achieves nothing except log volume. Symfony Messenger has everything needed. You just have to configure it on purpose: framework: messenger: transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy: max_retries: 5 delay: 2000 multiplier: 4 failure_transport: failed Two seconds, eight, thirty two, and so on. Exponential backoff gives the external service room to breathe. I also add jitter in a custom retry strategy, because a hundred messages that failed together will otherwise retry together, in one synchronized wave. Same failure, five times. ...

April 27, 2022 · 2 min · Murat Useinov

Symfony 6.0 is boring, take the compliment

The deprecation counter in the profiler, three digits. That is where one project stands this week, and that number is the whole Symfony 6 migration. Symfony shipped 5.4 and 6.0 together at the end of November, same code, same day. 4.4 and 5.0 worked exactly like this two years ago, so the story is rehearsed. 6.0 is 5.4 with the deprecated code deleted. You never migrate to a major. You migrate to the last minor, and the major is a formality. ...

December 9, 2021 · 2 min · Murat Useinov