<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Messenger on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/messenger/</link><description>Recent content in Messenger on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 10 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/messenger/index.xml" rel="self" type="application/rss+xml"/><item><title>Symfony 7.4 LTS: message signing, PHP config arrays, and the last 7.x</title><link>https://guillaumedelre.github.io/2026/01/10/symfony-7.4-lts-message-signing-php-config-arrays-and-the-last-7.x/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2026/01/10/symfony-7.4-lts-message-signing-php-config-arrays-and-the-last-7.x/</guid><description>Part 10 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 7.4 LTS adds Messenger message signing, PHP array-based configuration, and closes out the 7.x line.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 7.4 landed November 2025, alongside 8.0. It&rsquo;s the last LTS of the 7.x line: PHP 8.2 minimum, three years of bug fixes, four of security. For teams that can&rsquo;t or won&rsquo;t follow 8.0&rsquo;s PHP 8.4 requirement, 7.4 is where you land.</p>
<h2 id="message-signing-in-messenger">Message signing in Messenger</h2>
<p>Transport security in Messenger has always been the application&rsquo;s problem to solve. 7.4 adds message signing: a stamp-based mechanism that signs dispatched messages and validates signatures on reception.</p>
<p>The target use case is multi-tenant or external transport scenarios where you need cryptographic proof that a message wasn&rsquo;t tampered with or injected from outside. Configuration lives at the transport level:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">dsn</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">options</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">signing_key</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_SIGNING_KEY)%&#39;</span>
</span></span></code></pre></div><h2 id="php-array-configuration">PHP array configuration</h2>
<p>Symfony&rsquo;s configuration formats have always been YAML (default), XML, and PHP. The PHP format existed but it was awkward: a fluent builder DSL that required method chaining and gave your IDE nothing useful to work with.</p>
<p>7.4 swaps the fluent format for standard PHP arrays. IDEs can now actually analyze it, <code>config/reference.php</code> is auto-generated as a type-annotated reference, and the result reads like data rather than code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">FrameworkConfig</span> $framework)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>    $framework<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">router</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">strictRequirements</span>(<span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>    $framework<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">session</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">enabled</span>(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>The fluent format is deprecated. Arrays are the future, and honestly it&rsquo;s a better format.</p>
<h2 id="oidc-improvements">OIDC improvements</h2>
<p><code>#[IsSignatureValid]</code> validates signed URLs directly in controllers, cutting out the boilerplate of manual validation. OpenID Connect now supports multiple discovery endpoints, and a new <code>security:oidc-token:generate</code> command makes dev and testing a lot less painful.</p>
<h2 id="the-support-window">The support window</h2>
<p>7.4 LTS: bugs until November 2028, security fixes until November 2029. The path to 8.4 LTS (the next long-term target) goes through 7.4&rsquo;s deprecation notices and the PHP 8.4 upgrade. Fix the deprecations now and the jump to 8.x will be much less painful.</p>
<h2 id="attributes-get-more-precise">Attributes get more precise</h2>
<p><code>#[CurrentUser]</code> now accepts union types, which matters in practice when a route can be reached by more than one user class:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">index</span>(<span style="color:#75715e">#[CurrentUser] AdminUser|Customer $user): Response
</span></span></span></code></pre></div><p><code>#[Route]</code> accepts an array for the <code>env</code> option, so a debug route active only in <code>dev</code> and <code>test</code> no longer needs two separate definitions. <code>#[AsDecorator]</code> is now repeatable, meaning one class can decorate multiple services at once. <code>#[AsEventListener]</code> method signatures accept union event types. <code>#[IsGranted]</code> gets a <code>methods</code> option to scope an authorization check to specific HTTP verbs without duplicating the route.</p>
<h2 id="request-class-stops-doing-too-much">Request class stops doing too much</h2>
<p><code>Request::get()</code> is deprecated, and honestly good riddance. The method searched route attributes, then query parameters, then request body, in that order, silently returning whatever it found first. That ambiguity caused real bugs. It&rsquo;s gone in 8.0; in 7.4 it still works but triggers a deprecation. The replacements are explicit: <code>$request-&gt;attributes-&gt;get()</code>, <code>$request-&gt;query-&gt;get()</code>, <code>$request-&gt;request-&gt;get()</code>.</p>
<p>Body parsing for <code>PUT</code>, <code>PATCH</code>, <code>DELETE</code>, and <code>QUERY</code> requests arrives at the same time. Previously Symfony only parsed <code>application/x-www-form-urlencoded</code> and <code>multipart/form-data</code> for <code>POST</code>. Those same content types now get parsed for the other writable methods too, which kills a common REST API workaround.</p>
<p>HTTP method override for <code>GET</code>, <code>HEAD</code>, <code>CONNECT</code>, and <code>TRACE</code> is deprecated. Overriding a safe method with a header was always semantically broken anyway. You can now explicitly allow only the methods that make sense for your app:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">Request</span><span style="color:#f92672">::</span><span style="color:#a6e22e">setAllowedHttpMethodOverride</span>([<span style="color:#e6db74">&#39;PUT&#39;</span>, <span style="color:#e6db74">&#39;PATCH&#39;</span>, <span style="color:#e6db74">&#39;DELETE&#39;</span>]);
</span></span></code></pre></div><h2 id="workflows-accept-backedenums">Workflows accept BackedEnums</h2>
<p>Workflow places and transitions can now be defined with PHP backed enums, both in YAML (via the <code>!php/enum</code> tag) and in PHP config. The marking store works with enum values directly, so your domain model and your workflow definition finally use the same types:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">workflows</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">blog_publishing</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">initial_marking</span>: !<span style="color:#ae81ff">php/enum App\Status\PostStatus::Draft</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">places</span>: !<span style="color:#ae81ff">php/enum App\Status\PostStatus</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">transitions</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">publish</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">from</span>: !<span style="color:#ae81ff">php/enum App\Status\PostStatus::Review</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">to</span>: !<span style="color:#ae81ff">php/enum App\Status\PostStatus::Published</span>
</span></span></code></pre></div><h2 id="extending-validation-and-serialization-for-third-party-classes">Extending validation and serialization for third-party classes</h2>
<p>Ever needed to add validation or serialization metadata to a class from a bundle you don&rsquo;t own? 7.4 has <code>#[ExtendsValidationFor]</code> and <code>#[ExtendsSerializationFor]</code> for that. You write a companion class with your extra annotations, point the attribute at the target class, and Symfony merges the metadata at container compilation time:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[ExtendsValidationFor(UserRegistration::class)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UserRegistrationValidation</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Assert\NotBlank(groups: [&#39;my_app&#39;])]
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Assert\Length(min: 3, groups: [&#39;my_app&#39;])]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Assert\Email(groups: [&#39;my_app&#39;])]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $email <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Symfony verifies at compile time that the declared properties actually exist on the target class. A rename won&rsquo;t silently break your validation.</p>
<h2 id="dx-the-things-that-dont-headline-but-matter">DX: the things that don&rsquo;t headline but matter</h2>
<p>The Question helper in Console accepts a timeout. Ask the user to confirm something, and if they don&rsquo;t respond in N seconds, the default answer kicks in. Very handy in deployment scripts that can&rsquo;t afford to wait forever for a human.</p>
<p><code>messenger:consume</code> gets <code>--exclude-receivers</code>. Combined with <code>--all</code>, it lets you consume from every transport except specific ones:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>bin/console messenger:consume --all --exclude-receivers<span style="color:#f92672">=</span>low_priority
</span></span></code></pre></div><p>FrankenPHP worker mode is now auto-detected. If the process is running inside FrankenPHP, Symfony switches to worker mode automatically. No extra package needed.</p>
<p>The <code>debug:router</code> command hides the <code>Scheme</code> and <code>Host</code> columns when all routes use <code>ANY</code>, which removes a lot of noise from the default output. HTTP methods are now color-coded too.</p>
<p>Functional tests get <code>$client-&gt;getSession()</code> before the first request. Previously you had to make at least one request to access the session, which was annoying. Now you can pre-seed CSRF tokens or A/B testing flags up front:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$session <span style="color:#f92672">=</span> $client<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getSession</span>();
</span></span><span style="display:flex;"><span>$session<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">set</span>(<span style="color:#e6db74">&#39;_csrf/checkout&#39;</span>, <span style="color:#e6db74">&#39;test-token&#39;</span>);
</span></span><span style="display:flex;"><span>$session<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">save</span>();
</span></span></code></pre></div><h2 id="lock-dynamodb-store">Lock: DynamoDB store</h2>
<p><code>DynamoDbStore</code> lands as a new Lock backend. Useful in AWS-native deployments where Redis isn&rsquo;t in the stack, and it works exactly like any other store:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$store <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">DynamoDbStore</span>(<span style="color:#e6db74">&#39;dynamodb://default/locks&#39;</span>);
</span></span><span style="display:flex;"><span>$factory <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">LockFactory</span>($store);
</span></span></code></pre></div><h2 id="doctrine-bridge-day-and-time-point-types">Doctrine bridge: day and time point types</h2>
<p>Two new Doctrine column types: <code>day_point</code> stores a date-only value (no time component) and <code>time_point</code> stores a time-only value, both mapping to <code>DatePoint</code>. Good when your domain genuinely separates date from time:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[ORM\Column(type: &#39;day_point&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">DatePoint</span> $birthDate;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ORM\Column(type: &#39;time_point&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">DatePoint</span> $openingTime;
</span></span></code></pre></div><h2 id="routing-explicit-query-parameters">Routing: explicit query parameters</h2>
<p>The <code>_query</code> key in URL generation lets you set query parameters explicitly, separate from route parameters. This matters when a route parameter and a query parameter share the same name:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$url <span style="color:#f92672">=</span> $urlGenerator<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">generate</span>(<span style="color:#e6db74">&#39;report&#39;</span>, [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;site&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;fr&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;_query&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;site&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;us&#39;</span>],
</span></span><span style="display:flex;"><span>]);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// /report/fr?site=us
</span></span></span></code></pre></div><h2 id="weblink-parsing-incoming-link-headers">WebLink: parsing incoming Link headers</h2>
<p><code>HttpHeaderParser</code> parses <code>Link</code> response headers into structured objects. Before this, parsing Link headers from API responses meant either pulling in a third-party library or writing regex. The use case is HTTP APIs that advertise related resources or pagination via Link headers, like GitHub&rsquo;s API does.</p>
<h2 id="html5-parsing-gets-faster-on-php-84">HTML5 parsing gets faster on PHP 8.4</h2>
<p>DomCrawler and HtmlSanitizer switch to PHP 8.4&rsquo;s native HTML5 parser when available. No code changes needed on your end. The native parser is faster and more spec-compliant than the previous fallback. On PHP 8.2 or 8.3 nothing changes.</p>
<h2 id="translation-staticmessage">Translation: StaticMessage</h2>
<p><code>StaticMessage</code> implements <code>TranslatableInterface</code> but intentionally doesn&rsquo;t translate. It passes the string through unchanged regardless of locale. The use case is API responses that must stay in a fixed language regardless of the user&rsquo;s locale, or audit log entries where you need to preserve the original text as-is.</p>
]]></content:encoded></item><item><title>Swarrot vs Symfony Messenger: a real-world comparison</title><link>https://guillaumedelre.github.io/2022/01/26/swarrot-vs-symfony-messenger-a-real-world-comparison/</link><pubDate>Wed, 26 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2022/01/26/swarrot-vs-symfony-messenger-a-real-world-comparison/</guid><description>Swarrot and Symfony Messenger both handle RabbitMQ in PHP. Here is why we kept Swarrot after seriously evaluating a migration.</description><content:encoded><![CDATA[<p>We migrated a media microservices platform to Symfony 6 at the start of 2022. Twelve services, most of them consuming messages from RabbitMQ via <a href="https://github.com/swarrot/swarrot" target="_blank" rel="noopener noreferrer">Swarrot</a>. Symfony 6 made <a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Messenger</a> more central than ever, and during the migration planning a developer asked the obvious question: why not switch at the same time?</p>
<p>It ships with the framework. It has retry logic, native AMQP support, first-party documentation. Our setup looked artisanal by comparison.</p>
<p>Fair question. We took it seriously. Here&rsquo;s what we found.</p>
<h2 id="wiring-the-topology-by-hand">Wiring the topology by hand</h2>
<p>Swarrot is a consumer library that wraps the PECL AMQP extension. It reads bytes from a queue, runs them through a chain of processors (their term for middleware), and lets your code decide what to do with the payload. That&rsquo;s really it.</p>
<p>The middleware chain is the interesting part. Processors are nested decorators, each wrapping the next. The outer layers handle infrastructure concerns before the message even reaches your business logic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">middleware_stack</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.signal_handler&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.max_execution_time&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.exception_catcher&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.doctrine_object_manager&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.ack&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;app.processor.retry&#39;</span>
</span></span></code></pre></div><p><code>signal_handler</code> sits at the top because it needs to catch <code>SIGTERM</code> before any other processor sees it. <code>ack</code> sits near the bottom because you only acknowledge the message after processing succeeds. The order is not arbitrary, and it&rsquo;s entirely visible in configuration.</p>
<p>The topology is equally explicit. You declare everything yourself: exchanges, routing keys, retry queues, dead-letter queues:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">messages_types</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_retry</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.retry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_dead</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.dead</span>
</span></span></code></pre></div><p>Three entries per logical message type: main queue, retry queue, dead-letter queue. Everything that exists on the broker is named right here. The config is verbose but honest: no inference, no convention over configuration. If a queue exists in RabbitMQ, you can trace it to a single line of YAML.</p>
<h2 id="when-the-class-name-becomes-the-route">When the class name becomes the route</h2>
<p><a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Symfony Messenger</a> operates one level higher. You define a message class, a handler, and a transport. The library handles serialization, routing, retry, and failure queues automatically.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">IngestContent</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $contentId,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $source,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">dsn</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">retry_strategy</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">max_retries</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#39;App\Message\IngestContent&#39;</span>: <span style="color:#ae81ff">async</span>
</span></span></code></pre></div><p>Messenger serializes the object, puts it on the transport, and deserializes it on the other end into the correct class. No manual topology, no explicit exchange names. The class name is the routing primitive.</p>
<p>That last sentence is exactly where things got complicated for us.</p>
<h2 id="where-typing-becomes-coupling">Where typing becomes coupling</h2>
<p>Messenger assumes that the producer and the consumer share a PHP class definition. That&rsquo;s fine for a single app, or for services that share a dedicated contracts package. In a monorepo of independent Symfony applications, it creates coupling that simply doesn&rsquo;t exist today.</p>
<p>Take a content ingestion message that twelve services consume. With Swarrot, each service reads the raw JSON payload and picks the fields it cares about. Adding a new field means updating the producer. Consumers that don&rsquo;t need the field keep working without any modification.</p>
<p>With Messenger, <code>IngestContent</code> must be defined somewhere that all twelve services can reference. That means either:</p>
<ul>
<li>A shared PHP package, versioned, deployed, and maintained across services. Every schema change becomes a cross-service coordination exercise.</li>
<li>Duplicated classes in each service, which drift silently apart under pressure.</li>
</ul>
<p>Neither is free. The shared package approach inverts the ownership model: the message schema becomes a dependency rather than a contract defined at the boundary. The duplication approach is just the original problem deferred.</p>
<p>The root difference is what a message represents. Messenger is designed for <strong>typed commands</strong>: an object that carries meaning and dispatches to a specific handler. Swarrot treats messages as <strong>opaque data</strong>: bytes that flow through a topology, processed by whatever consumer happens to be listening. If your messages are data, the extra abstraction Messenger adds doesn&rsquo;t help you. It creates friction.</p>
<h2 id="the-blocker">The blocker</h2>
<p>The serialization problem was the decisive one. In a monorepo where services are autonomous, sharing PHP classes between them isn&rsquo;t architecturally neutral: it&rsquo;s a coupling decision that makes future changes harder. We would have been trading a nominally &ldquo;legacy&rdquo; library for a more modern one while introducing exactly the kind of tight coupling we&rsquo;d spent years avoiding.</p>
<p>There were secondary concerns too. The PECL AMQP extension gives direct access to broker features (message priorities, per-queue TTL, headers exchange routing) that Messenger abstracts away. And migrating fifteen consumers without a flag day means running both libraries in parallel, which is a real operational constraint.</p>
<p>But the serialization issue alone would have been enough.</p>
<h2 id="data-or-commands-thats-the-question">Data or commands: that&rsquo;s the question</h2>
<p>The choice isn&rsquo;t about library quality. Messenger is well-maintained, well-documented, and integrates cleanly into the Symfony ecosystem.</p>
<p>The question to ask first is: what are your messages?</p>
<p>If they are typed commands with a known schema and a single authoritative consumer, Messenger is a natural fit. You write a class, a handler, configure a transport, and the infrastructure handles the rest.</p>
<p>If they are data payloads consumed by multiple independent services, each of which owns its own deserialization, the abstraction Messenger adds works against you. Swarrot&rsquo;s explicit topology and raw payload model give you more control where you actually need it.</p>
<p>One real limitation to keep in mind: Swarrot is tied to the PECL AMQP extension, which only implements AMQP 0-9-1. That means RabbitMQ (or a compatible broker) is a hard dependency. If your infrastructure ever moves toward an AMQP 1.0 broker (Azure Service Bus, ActiveMQ Artemis), Swarrot can&rsquo;t follow. Messenger&rsquo;s transport layer abstracts this cleanly: changing brokers means changing a DSN, not rewriting consumers.</p>
<p>If broker portability is a requirement, or likely to become one, that changes the calculus significantly.</p>
<p>Swarrot isn&rsquo;t legacy to migrate away from. For now, it&rsquo;s the right fit: AMQP routing as the primitive, messages as data, RabbitMQ as a long-term infrastructure choice.</p>
<p>That could change. A shared contracts package, a new broker requirement, a greenfield service that doesn&rsquo;t carry the existing topology weight: any of these could tip the balance toward Messenger. The library isn&rsquo;t wrong for this platform. It may just be the right answer for a future version of it.</p>
]]></content:encoded></item><item><title>Symfony 5.4 LTS: enum support, route aliases, and the PHP 8.1 bridge</title><link>https://guillaumedelre.github.io/2022/01/10/symfony-5.4-lts-enum-support-route-aliases-and-the-php-8.1-bridge/</link><pubDate>Mon, 10 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2022/01/10/symfony-5.4-lts-enum-support-route-aliases-and-the-php-8.1-bridge/</guid><description>Part 6 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 5.4 LTS lands native enum support and the full feature set of 6.0, with backward compatibility intact.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 5.4 landed November 29, 2021, same day as Symfony 6.0 and one day after PHP 8.1 was released. Not a coincidence.</p>
<p>5.4 is the LTS, and its job is to carry as much of 6.0&rsquo;s feature set as possible while keeping 5.x compatibility intact. It&rsquo;s also the first Symfony release that actually understands PHP 8.1 features.</p>
<h2 id="enum-support">Enum support</h2>
<p>PHP 8.1 introduced native enums. Symfony 5.4 embraces them immediately:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">enum</span> <span style="color:#a6e22e">Status</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Active</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;active&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Inactive</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;inactive&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>EnumType</code> form type renders enums as select fields, no custom transformers needed. The validator understands backed enums. The serializer maps enum values to their backing type and back. Three components updated in one shot, which meant migrating codebases from pseudo-enum constants to real PHP 8.1 enums was actually pretty smooth.</p>
<h2 id="security-voter-cache">Security voter cache</h2>
<p>The <code>CacheableVoterInterface</code> lets voters that always abstain on a given attribute signal that to the security system, which can then skip them on subsequent checks. For apps with many voters, the gain on permission checks adds up fast. Small change, noticeable in practice.</p>
<h2 id="messenger-matures-further">Messenger matures further</h2>
<p>Messenger batch processing (handling multiple messages in a single transaction instead of one by one) is now stable. Rate limiting per transport. Dead letter queues get better tooling. After years as &ldquo;experimental&rdquo;, Messenger in 5.4 is finally the async foundation you can bet on for serious workloads.</p>
<h2 id="console-grew-a-tab-key">Console grew a tab key</h2>
<p>Symfony 5.4 ships shell autocompletion for all commands. Press Tab and the shell suggests command names, argument values, and option values. For built-in commands this works out of the box. For custom commands, add a <code>complete()</code> method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\Console\Completion\CompletionInput</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\Console\Completion\CompletionSuggestions</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">complete</span>(<span style="color:#a6e22e">CompletionInput</span> $input, <span style="color:#a6e22e">CompletionSuggestions</span> $suggestions)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($input<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">mustSuggestOptionValuesFor</span>(<span style="color:#e6db74">&#39;format&#39;</span>)) {
</span></span><span style="display:flex;"><span>        $suggestions<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">suggestValues</span>([<span style="color:#e6db74">&#39;json&#39;</span>, <span style="color:#e6db74">&#39;xml&#39;</span>, <span style="color:#e6db74">&#39;csv&#39;</span>]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>No interface required, just the method and Symfony picks it up. The community also went through all built-in commands (<code>debug:router</code>, <code>cache:pool:clear</code>, <code>secrets:remove</code>, <code>lint:twig</code>, and a dozen more) to add completions before the release.</p>
<h2 id="routes-can-be-aliases-now">Routes can be aliases now</h2>
<p>The routing component now supports aliasing: one route can point to another. The obvious use case is renaming a route without breaking anything that still generates URLs with the old name.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># config/routes.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">admin_dashboard</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/admin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># legacy name kept during transition</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">dashboard</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">admin_dashboard</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">deprecated</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">package</span>: <span style="color:#e6db74">&#39;acme/my-bundle&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">version</span>: <span style="color:#e6db74">&#39;2.3&#39;</span>
</span></span></code></pre></div><p>Generating a URL with <code>dashboard</code> still works, but fires a deprecation notice. Clean rename paths for bundles that need to maintain public route names while moving on.</p>
<h2 id="exceptions-map-to-http-status-codes-in-config">Exceptions map to HTTP status codes in config</h2>
<p>Before 5.4, mapping an exception class to an HTTP status code meant implementing <code>HttpExceptionInterface</code> or writing a listener. Now it&rsquo;s just a YAML entry:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># config/packages/framework.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">exceptions</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">App\Exception\PaymentRequiredException</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">status_code</span>: <span style="color:#ae81ff">402</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">log_level</span>: <span style="color:#ae81ff">warning</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">App\Exception\MaintenanceException</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">status_code</span>: <span style="color:#ae81ff">503</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">log_level</span>: <span style="color:#ae81ff">info</span>
</span></span></code></pre></div><p>The exception doesn&rsquo;t need to implement anything. The framework reads the map, sets the status code, logs at the configured level. Handy for domain exceptions that have no business knowing about HTTP.</p>
<h2 id="two-new-validator-constraints">Two new validator constraints</h2>
<p>5.4 adds <code>Cidr</code> and <code>CssColor</code> to the Validator component.</p>
<p><code>Cidr</code> validates network notation — IP address plus subnet mask — with control over which IP version to accept and bounds on the mask value:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[Assert\Cidr(version: 4, netmaskMin: 16, netmaskMax: 28)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span> $allowedSubnet;
</span></span></code></pre></div><p><code>CssColor</code> validates that a string is a valid CSS color. Useful for theme editors, CMS config, or any UI that lets users pick colors:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[Assert\CssColor(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">formats</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Assert\CssColor</span><span style="color:#f92672">::</span><span style="color:#a6e22e">HEX_LONG</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">message</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;The accent color must be a 6-digit hex value.&#39;</span>,
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span> $accentColor;
</span></span></code></pre></div><h2 id="nested-php-attributes-for-validation-constraints">Nested PHP attributes for validation constraints</h2>
<p>Symfony 5.2 added validator constraints as PHP attributes, but PHP 8.0 had a hard limit on nested attributes. Complex constraints like <code>All</code>, <code>Collection</code>, or <code>AtLeastOneOf</code> were impossible to express in attribute syntax alone. PHP 8.1 lifted that restriction, and 5.4 makes the most of it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\Validator\Constraints</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">Assert</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CartItem</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Assert\All([
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\NotNull</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\Range</span>(<span style="color:#a6e22e">min</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>),
</span></span><span style="display:flex;"><span>    ])]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">array</span> $quantities;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Assert\AtLeastOneOf(
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">constraints</span><span style="color:#f92672">:</span> [<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\Email</span>(), <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\Url</span>()],
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">message</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Must be a valid email or URL.&#39;</span>,
</span></span><span style="display:flex;"><span>    )]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span> $contact;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>No annotation doc-blocks, no XML mapping. Pure PHP 8.1 attributes all the way down.</p>
<h2 id="dependency-injection-three-things-worth-knowing">Dependency injection: three things worth knowing</h2>
<p>Tagged iterators can now be injected into service locators, which previously only accepted explicit service lists. Union type autowiring works when both sides of the union resolve to the same service, which is common with serializer interfaces:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">NormalizerInterface</span> <span style="color:#f92672">&amp;</span> <span style="color:#a6e22e">DenormalizerInterface</span> $serializer
</span></span><span style="display:flex;"><span>) {}
</span></span></code></pre></div><p><code>#[SubscribedService]</code> replaces the automatic introspection that <code>ServiceSubscriberTrait</code> did implicitly. It&rsquo;s now an explicit attribute on methods, which makes the dependency visible without any magic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Contracts\Service\Attribute\SubscribedService</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SomeService</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ServiceSubscriberInterface</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[SubscribedService]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">router</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">RouterInterface</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">container</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#66d9ef">__METHOD__</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="messenger-attributes-worker-state-and-service-reset">Messenger: attributes, worker state, and service reset</h2>
<p>Messenger handlers can drop the <code>MessageHandlerInterface</code> in favor of <code>#[AsMessageHandler]</code>, which also lets you bind a handler to a specific transport and set its priority, all without touching YAML:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[AsMessageHandler(fromTransport: &#39;async&#39;, priority: 10)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ProcessOrderHandler</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__invoke</span>(<span style="color:#a6e22e">ProcessOrder</span> $message)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Worker state is now inspectable via <code>WorkerMetadata</code> inside event listeners, useful when you have workers on multiple transports and need to know which one fired a given event.</p>
<p>Long-running workers accumulate state across messages: entity manager buffers, in-memory caches, open connections. The new <code>reset_on_message</code> option takes care of resetting all resettable services between messages:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">reset_on_message</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><h2 id="serializer-collect-errors-instead-of-throwing">Serializer: collect errors instead of throwing</h2>
<p>Deserializing external JSON into a typed DTO used to throw on the very first type mismatch. The <code>COLLECT_DENORMALIZATION_ERRORS</code> option changes that: all type errors get collected into a <code>PartialDenormalizationException</code>, so you can return a proper 400 with a full list of field-level problems:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    $dto <span style="color:#f92672">=</span> $serializer<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">deserialize</span>($request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getContent</span>(), <span style="color:#a6e22e">OrderDto</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>, <span style="color:#e6db74">&#39;json&#39;</span>, [
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">DenormalizerInterface</span><span style="color:#f92672">::</span><span style="color:#a6e22e">COLLECT_DENORMALIZATION_ERRORS</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    ]);
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">PartialDenormalizationException</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">json</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">array_map</span>(<span style="color:#a6e22e">fn</span>($err) <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;path&#39;</span> <span style="color:#f92672">=&gt;</span> $err<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getPath</span>(), <span style="color:#e6db74">&#39;expected&#39;</span> <span style="color:#f92672">=&gt;</span> $err<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getExpectedTypes</span>()], $e<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getErrors</span>()),
</span></span><span style="display:flex;"><span>        <span style="color:#ae81ff">400</span>
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The serializer&rsquo;s default context can also be set globally in YAML, so you stop passing the same options on every call.</p>
<h2 id="language-negotiation-out-of-the-box">Language negotiation out of the box</h2>
<p>Two new framework options handle the <code>Accept-Language</code> header without custom listeners:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled_locales</span>: [<span style="color:#e6db74">&#39;en&#39;</span>, <span style="color:#e6db74">&#39;fr&#39;</span>, <span style="color:#e6db74">&#39;de&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">set_locale_from_accept_language</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">set_content_language_from_locale</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>With this in place, Symfony reads the browser&rsquo;s preferred language, picks the best match from <code>enabled_locales</code>, sets the request locale, and adds a <code>Content-Language</code> header to the response. The <code>{_locale}</code> route attribute still takes precedence when present.</p>
<h2 id="translation-extraction-not-update">Translation: extraction, not update</h2>
<p>The <code>translation:update</code> command is renamed to <code>translation:extract</code>. The old name sticks around as deprecated. The distinction matters: the command never writes to a database, it extracts translatable strings from source files. The new name finally says what it does.</p>
<p><code>lint:xliff</code> also gains a <code>--format=github</code> option that outputs errors as GitHub Actions annotations, so translation lint failures show up as PR review comments instead of getting buried in log output.</p>
<h2 id="controller-shortcuts-pruned">Controller shortcuts pruned</h2>
<p>Three <code>AbstractController</code> shortcuts are deprecated: <code>getDoctrine()</code>, <code>dispatchMessage()</code>, and the generic <code>get()</code> method for pulling arbitrary services from the container. The direction is explicit constructor injection. For <code>getDoctrine()</code> specifically:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// before
</span></span></span><span style="display:flex;"><span>$em <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getDoctrine</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getManager</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// after — inject it directly
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">EntityManagerInterface</span> $em) {}
</span></span></code></pre></div><p><code>Request::get()</code> is also deprecated. It searched route attributes, query string, and POST body in an undocumented order, which was a great way to get surprising results. Use <code>$request-&gt;query-&gt;get()</code>, <code>$request-&gt;request-&gt;get()</code>, or <code>$request-&gt;attributes-&gt;get()</code> and be explicit about where the value comes from.</p>
<h2 id="the-path-utility-class">The Path utility class</h2>
<p>The Filesystem component gets a <code>Path</code> class ported from <code>webmozart/path-util</code>. It handles the awkward cases that <code>dirname()</code> and <code>realpath()</code> fumble:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\Filesystem\Path</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Path</span><span style="color:#f92672">::</span><span style="color:#a6e22e">canonicalize</span>(<span style="color:#e6db74">&#39;../config/../config/services.yaml&#39;</span>); <span style="color:#75715e">// &#39;../config/services.yaml&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">Path</span><span style="color:#f92672">::</span><span style="color:#a6e22e">getDirectory</span>(<span style="color:#e6db74">&#39;C:/&#39;</span>);                               <span style="color:#75715e">// &#39;C:/&#39; (dirname() returns &#39;.&#39;)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">Path</span><span style="color:#f92672">::</span><span style="color:#a6e22e">getLongestCommonBasePath</span>([
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;/var/www/project/src/Controller/FooController.php&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;/var/www/project/src/Controller/BarController.php&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;/var/www/project/src/Entity/User.php&#39;</span>,
</span></span><span style="display:flex;"><span>]);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// &#39;/var/www/project/src&#39;
</span></span></span></code></pre></div><p>Useful whenever your code deals with paths that cross OS boundaries or involve relative segments.</p>
<h2 id="smaller-things-that-add-up">Smaller things that add up</h2>
<p><code>debug:dotenv</code> shows which <code>.env</code> files were loaded and what value each variable resolves to. The first thing you reach for when environment-specific behavior is acting up.</p>
<p>The String component adds <code>trimPrefix()</code> and <code>trimSuffix()</code> for removing known prefixes or suffixes without writing a substr calculation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">u</span>(<span style="color:#e6db74">&#39;file-image-0001.png&#39;</span>)<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">trimPrefix</span>(<span style="color:#e6db74">&#39;file-&#39;</span>);    <span style="color:#75715e">// &#39;image-0001.png&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">u</span>(<span style="color:#e6db74">&#39;template.html.twig&#39;</span>)<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">trimSuffix</span>(<span style="color:#e6db74">&#39;.twig&#39;</span>);      <span style="color:#75715e">// &#39;template.html&#39;
</span></span></span></code></pre></div><p>DomCrawler gets <code>innerText()</code>, which returns only the direct text of a node, excluding child elements. <code>text()</code> returns everything including nested text; <code>innerText()</code> returns just the node&rsquo;s own content. Small difference, but it matters when scraping.</p>
<p>The RateLimiter component extends its interval support to <code>perMonth()</code> and <code>perYear()</code>, for apps that need to limit events over longer windows: newsletter sends, API quota resets, annual plan limits.</p>
<p>The Finder component now respects <code>.gitignore</code> files in all subdirectories when you call <code>ignoreVCSIgnored(true)</code>, not just the root. Child directory rules override parent rules, exactly like git itself.</p>
<h2 id="the-lts-window">The LTS window</h2>
<p>5.4 gets bug fixes until November 2024 and security fixes until November 2025. The migration from 5.4 to 6.4 (the next LTS) is intentionally smooth: fix the 5.4 deprecation warnings, and the 6.x jump is mechanical.</p>
<p>The deprecation layer in 5.4 points at everything 6.0 removes: the remaining pieces of the old security system, <code>ContainerAwareTrait</code>, and a handful of legacy form and serializer patterns.</p>
]]></content:encoded></item><item><title>Symfony 4.4 LTS: HttpClient, Mailer, Messenger, and the features that stayed</title><link>https://guillaumedelre.github.io/2020/01/04/symfony-4.4-lts-httpclient-mailer-messenger-and-the-features-that-stayed/</link><pubDate>Sat, 04 Jan 2020 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2020/01/04/symfony-4.4-lts-httpclient-mailer-messenger-and-the-features-that-stayed/</guid><description>Part 4 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 4.4 LTS ships a mature HttpClient and production-ready Messenger — the HTTP and async layers Symfony was missing.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 4.4 and 5.0 both landed November 21, 2019. 4.4 is the LTS: same feature set as 5.0, deprecation layer baked in, and a long support window for teams that can&rsquo;t follow every release.</p>
<p>The feature worth singling out arrived in 4.2 and matured through 4.3 and 4.4: <code>HttpClient</code>.</p>
<h2 id="httpclient">HttpClient</h2>
<p>PHP&rsquo;s built-in HTTP options (<code>file_get_contents</code> with stream contexts, cURL, Guzzle) each have their own model, their own quirks, and their own abstraction cost. Symfony 4.2 introduced <code>HttpClient</code>, a first-party HTTP client with one API over multiple transports.</p>
<p>The interface is clean:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$response <span style="color:#f92672">=</span> $client<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">request</span>(<span style="color:#e6db74">&#39;GET&#39;</span>, <span style="color:#e6db74">&#39;https://api.example.com/users&#39;</span>);
</span></span><span style="display:flex;"><span>$users <span style="color:#f92672">=</span> $response<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">toArray</span>();
</span></span></code></pre></div><p>The implementation is async by default. Responses are lazy: the network request doesn&rsquo;t happen until you actually read the response. Multiple requests can be initiated and resolved as data arrives, no threads or callbacks needed.</p>
<p>The built-in mock transport (<code>MockHttpClient</code>) makes testing HTTP calls painless without spinning up servers or patching global functions.</p>
<h2 id="mailer">Mailer</h2>
<p>Also stabilized in 4.4: the <code>Mailer</code> component, replacing <code>SwiftMailerBundle</code> as the recommended email solution. Transport is configured via DSN:</p>
<pre tabindex="0"><code>MAILER_DSN=smtp://user:pass@smtp.example.com:587
</code></pre><p>The DSN approach means switching providers (Mailgun, Postmark, SES, local SMTP) is a config change, not a code change. Email testing uses a spooler by default in non-production environments.</p>
<h2 id="messenger-matures">Messenger matures</h2>
<p>The Messenger component landed in 3.4 as experimental. By 4.4 it&rsquo;s stable and battle-tested: async message handling with retry logic, failure transport, and adapters for AMQP, Redis, Doctrine, and in-process transports.</p>
<p>The pattern it enables (handle a request synchronously, dispatch work asynchronously, retry on failure) replaces a class of Gearman/RabbitMQ setups that required separate libraries and significant configuration.</p>
<h2 id="the-lts-window">The LTS window</h2>
<p>4.4 is supported for bugs until November 2022 and security fixes until November 2023. If you&rsquo;re on 4.x and want stability, this is a comfortable place to land. The deprecation warnings it introduces point directly at what 5.0 will require.</p>
<h2 id="the-messenger-component-from-experimental-to-production">The Messenger component, from experimental to production</h2>
<p>Messenger arrived in 4.1 as an experiment. The concept was simple: dispatch a message object to a bus, handle it immediately or route it to a transport for async processing. By 4.3 and 4.4, the experiment had become infrastructure.</p>
<p>The 4.3 release added a dedicated failure transport. When a message fails after all retry attempts, it goes somewhere recoverable rather than just disappearing:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">failure_transport</span>: <span style="color:#ae81ff">failed</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">failed</span>: <span style="color:#e6db74">&#39;doctrine://default?queue_name=failed&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">App\Message\SendEmail</span>: <span style="color:#ae81ff">async</span>
</span></span></code></pre></div><p>Messages that land in <code>failed</code> can be inspected and retried manually. Before this, failed messages were a log entry and a headache. After this, they&rsquo;re a queue you can actually work with.</p>
<h2 id="event-dispatching-finally-using-objects-properly">Event dispatching, finally using objects properly</h2>
<p>Since the beginning, Symfony&rsquo;s event system used string event names as the primary identifier. You&rsquo;d define <code>OrderEvents::NEW_ORDER = 'order.new_order'</code>, listen on that string, and pass the event object as a secondary parameter.</p>
<p>4.3 flipped this around. The event object comes first, and the event name becomes optional:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// Before
</span></span></span><span style="display:flex;"><span>$dispatcher<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#a6e22e">OrderEvents</span><span style="color:#f92672">::</span><span style="color:#a6e22e">NEW_ORDER</span>, $event);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 4.3+
</span></span></span><span style="display:flex;"><span>$dispatcher<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($event);
</span></span></code></pre></div><p>Omit the name and Symfony uses the class name as the identifier. Listeners and subscribers can now reference the class directly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getSubscribedEvents</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">array</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">OrderPlacedEvent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;onOrderPlaced&#39;</span>,
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The HttpKernel events were renamed accordingly: <code>GetResponseEvent</code> became <code>RequestEvent</code>, <code>FilterResponseEvent</code> became <code>ResponseEvent</code>. The old names stayed as aliases through 4.x.</p>
<h2 id="vardumper-gets-a-server">VarDumper gets a server</h2>
<p><code>dump()</code> in a controller that returns JSON means your debug output gets injected straight into the response body. For API development, that&rsquo;s annoying enough to make people disable dumping entirely.</p>
<p>4.1 added a VarDumper server that captures dumps separately:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>bin/console server:dump
</span></span></code></pre></div><p>Configure the dump destination in <code>config/packages/dev/debug.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">debug</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dump_destination</span>: <span style="color:#e6db74">&#34;tcp://%env(VAR_DUMPER_SERVER)%&#34;</span>
</span></span></code></pre></div><p>Now <code>dump()</code> in your API controller sends data to the server&rsquo;s console instead of polluting the response. The server shows the dump alongside its source file, the HTTP request that triggered it, and the timestamp.</p>
<h2 id="varexporter-for-when-var_export-fails-you">VarExporter, for when <code>var_export()</code> fails you</h2>
<p><code>var_export()</code> has two problems: it ignores serialization semantics and its output isn&rsquo;t PSR-2 compliant. The 4.2 VarExporter component fixes both.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$exported <span style="color:#f92672">=</span> <span style="color:#a6e22e">VarExporter</span><span style="color:#f92672">::</span><span style="color:#a6e22e">export</span>([<span style="color:#ae81ff">123</span>, [<span style="color:#e6db74">&#39;abc&#39;</span>, <span style="color:#66d9ef">true</span>]]);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Returns:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//     123,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//     [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//         &#39;abc&#39;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//         true,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//     ],
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// ]
</span></span></span></code></pre></div><p>More importantly, it correctly handles objects implementing <code>Serializable</code>, <code>__sleep</code>, and <code>__wakeup</code>. Where <code>var_export()</code> silently drops serialization methods and exports raw properties, VarExporter produces code that calls the same hooks <code>unserialize()</code> would. The practical use case is cache warming: generating PHP files that can be loaded by OPcache without re-executing expensive computations.</p>
<h2 id="passwords-that-check-against-breach-databases">Passwords that check against breach databases</h2>
<p>The <code>NotCompromisedPassword</code> constraint arrived in 4.3. It checks submitted passwords against haveibeenpwned.com&rsquo;s breach database without sending the actual password anywhere.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\Validator\Constraints</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">Assert</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Assert\NotCompromisedPassword]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $plainPassword;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The implementation uses k-anonymity: SHA-1 hash the password, send only the first five characters to the API, get back all matching hashes, check locally. The password never leaves your server. For registration forms, adding this constraint is one line and a genuinely useful security signal.</p>
<h2 id="workflow-gets-context">Workflow gets context</h2>
<p>The Workflow component existed before 4.x, but 4.3 added context propagation: the ability to pass arbitrary data through a transition and access it in listeners.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$workflow<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">apply</span>($article, <span style="color:#e6db74">&#39;publish&#39;</span>, [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;user&#39;</span> <span style="color:#f92672">=&gt;</span> $user<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getUsername</span>(),
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;reason&#39;</span> <span style="color:#f92672">=&gt;</span> $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">request</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;reason&#39;</span>),
</span></span><span style="display:flex;"><span>]);
</span></span></code></pre></div><p>The context arrives in <code>TransitionEvent</code> and gets stored alongside the marking. For audit trails, this is the difference between knowing a transition happened and knowing who triggered it and why. You can also inject context from a subscriber without touching every <code>apply()</code> call, which is handy for cross-cutting concerns like timestamps or current user.</p>
<h2 id="the-autowiring-got-smarter">The autowiring got smarter</h2>
<p>4.2 added binding by type and name together. Before, you could bind by type (<code>LoggerInterface</code>) or by name (<code>$logger</code>), but not both at once. That caused problems when a service needs two different implementations of the same interface:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">_defaults</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">bind</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">Psr\Log\LoggerInterface $orderLogger</span>: <span style="color:#e6db74">&#39;@monolog.logger.orders&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">Psr\Log\LoggerInterface $paymentLogger</span>: <span style="color:#e6db74">&#39;@monolog.logger.payments&#39;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">LoggerInterface</span> $orderLogger,   <span style="color:#75715e">// gets monolog.logger.orders
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">LoggerInterface</span> $paymentLogger, <span style="color:#75715e">// gets monolog.logger.payments
</span></span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The match requires both type and argument name to align, so there&rsquo;s no risk of accidentally injecting the wrong logger.</p>
<h2 id="errorhandler-replaces-the-debug-component">ErrorHandler replaces the Debug component</h2>
<p>The <code>Debug</code> component, unchanged since 2013, had an awkward dependency on TwigBundle even for API-only apps. Any uncaught exception in a JSON API would render an HTML error page unless you wrote custom exception listeners.</p>
<p>4.4 extracts this into a dedicated <code>ErrorHandler</code> component. For non-HTML requests, error responses now follow RFC 7807 out of the box:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;title&#34;</span>: <span style="color:#e6db74">&#34;Not Found&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#ae81ff">404</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;detail&#34;</span>: <span style="color:#e6db74">&#34;Sorry, the page you are looking for could not be found&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>No Twig required. The format follows the <code>Accept</code> header: JSON for JSON requests, XML for XML requests. To customize further, you provide a normalizer via the Serializer component rather than a Twig template.</p>
<h2 id="php-74-preloading-wired-in-automatically">PHP 7.4 preloading, wired in automatically</h2>
<p>PHP 7.4 introduced OPcache preloading: load files into shared memory before any requests arrive, so they&rsquo;re available as compiled opcodes from the very first request. The practical gain is 30-50% faster response times with no code changes.</p>
<p>The catch is configuration: you need to specify exactly which files to preload in <code>php.ini</code>. Symfony 4.4 generates that file automatically in the cache directory:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e">; php.ini</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">opcache.preload</span><span style="color:#f92672">=</span><span style="color:#e6db74">/path/to/project/var/cache/prod/App_KernelProdContainer.preload.php</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">opcache.preload_user</span><span style="color:#f92672">=</span><span style="color:#e6db74">www-data</span>
</span></span></code></pre></div><p>Run <code>cache:warmup</code> in production and point OPcache at the generated file. Symfony preloads the container, compiled routes, and Twig templates: the files that are read on every request and never change between deploys.</p>
<h2 id="console-return-codes-and-no_color">Console: return codes and NO_COLOR</h2>
<p>Two small things in 4.4 that honestly should have existed earlier. Commands that don&rsquo;t return an integer from <code>execute()</code> now trigger a deprecation warning. In 5.0, the return type becomes mandatory. Returning <code>0</code> for success, non-zero for failure: standard Unix behavior, and it makes integration with process supervisors and CI pipelines unambiguous.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">execute</span>(<span style="color:#a6e22e">InputInterface</span> $input, <span style="color:#a6e22e">OutputInterface</span> $output)<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Command</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SUCCESS</span>; <span style="color:#75715e">// = 0
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The second: <code>NO_COLOR</code> environment variable support, following the convention from no-color.org. Set it and every Symfony console command drops ANSI escape codes regardless of what the terminal claims to support. Useful for CI environments that capture output as text and then choke on color codes embedded in logs.</p>
]]></content:encoded></item></channel></rss>