<?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>Lts on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/lts/</link><description>Recent content in Lts 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/lts/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>Symfony 6.4 LTS: AssetMapper, Scheduler, Webhook, and the long-term release</title><link>https://guillaumedelre.github.io/2024/01/10/symfony-6.4-lts-assetmapper-scheduler-webhook-and-the-long-term-release/</link><pubDate>Wed, 10 Jan 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2024/01/10/symfony-6.4-lts-assetmapper-scheduler-webhook-and-the-long-term-release/</guid><description>Part 8 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 6.4 LTS stabilizes AssetMapper — a bundler-free frontend approach — alongside the Scheduler and Webhook components.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 6.4 landed November 29, 2023. It&rsquo;s an LTS with a story: four components that shipped as experimental in earlier releases are now stable. The biggest deal is AssetMapper.</p>
<h2 id="assetmapper">AssetMapper</h2>
<p>Modern frontend tooling in Symfony meant Webpack Encore. Encore works: it handles transpilation, bundling, versioning, hot reload. It also requires Node.js, a separate build step, and a non-trivial amount of configuration for what is often a pretty modest frontend.</p>
<p>AssetMapper takes a different position. Modern browsers support ES modules natively. Instead of bundling, ship the files as-is, let the browser resolve imports through an importmap, and manage vendor dependencies through downloaded files rather than npm packages.</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>composer require symfony/asset-mapper
</span></span><span style="display:flex;"><span>php bin/console importmap:require lodash
</span></span></code></pre></div><p>No Node.js. No npm. No build step. JavaScript and CSS files are versioned and served directly, with a digest in the URL for cache busting. For apps where the frontend is not the primary engineering concern, this removes an entire toolchain from the equation.</p>
<p>6.4 adds CSS files to the importmap, automatic CSS preloading via WebLink, and commands to audit and update vendor dependencies. The package.json experience, minus npm.</p>
<h2 id="scheduler">Scheduler</h2>
<p>The Scheduler component (periodic and cron-style task scheduling without an external job runner) exits experimental and becomes stable. The API uses attributes:</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">#[AsCronTask(&#39;0 * * * *&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HourlyReport</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ScheduledTaskInterface</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">run</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> { <span style="color:#f92672">...</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Backed by Messenger transports, tasks run in any environment where a worker is running. For many use cases, this replaces the classic <code>cron</code> entry + console command pattern.</p>
<h2 id="webhook-and-remoteevent">Webhook and RemoteEvent</h2>
<p>Also graduating from experimental: the Webhook component handles incoming webhooks from external services. Instead of writing raw controllers that parse payloads and dispatch events by hand, you configure parsers for known services (Stripe, GitHub, Mailgun) and get typed events.</p>
<h2 id="clock3-datepoint">:clock3: DatePoint</h2>
<p>A new <code>DatePoint</code> class in the Clock component: an immutable <code>DateTime</code> wrapper that throws exceptions on invalid modifiers instead of silently returning <code>false</code>. Small thing, but meaningful for code that manipulates dates and actually wants to know when something goes wrong.</p>
<h2 id="the-support-window">The support window</h2>
<p>6.4 LTS gets bug fixes until November 2026 and security fixes until November 2027. The path from 6.4 to 7.4 (the next LTS) runs through the 6.4 deprecation notices, as usual.</p>
<h2 id="routes-without-magic-strings">Routes without magic strings</h2>
<p>FQCN-based route aliases are now generated automatically. If a controller method has a single route, Symfony creates an alias using its fully qualified class 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><span style="color:#75715e">// Previously: only &#39;blog_index&#39; worked
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Now: both work identically
</span></span></span><span style="display:flex;"><span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">urlGenerator</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">generate</span>(<span style="color:#e6db74">&#39;blog_index&#39;</span>);
</span></span><span style="display:flex;"><span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">urlGenerator</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">generate</span>(<span style="color:#a6e22e">BlogController</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span><span style="color:#f92672">.</span><span style="color:#e6db74">&#39;::index&#39;</span>);
</span></span></code></pre></div><p>For invokable controllers, the alias is just the class name. The practical benefit is IDE navigation and refactoring safety: you&rsquo;re referencing a class constant, not a string that can silently drift.</p>
<h2 id="two-new-di-attributes">Two new DI attributes</h2>
<p><code>#[AutowireLocator]</code> and <code>#[AutowireIterator]</code> join the DI attribute family. Instead of configuring service locators and tagged iterables in YAML, you just declare them on constructor parameters:</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:#75715e">#[AutowireLocator([FooHandler::class, BarHandler::class])]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">ContainerInterface</span> $handlers,
</span></span><span style="display:flex;"><span>) {}
</span></span></code></pre></div><p>Aliases, optional services (prefixed with <code>?</code>), and parameter injection via <code>SubscribedService</code> are all supported. The locator lazy-loads, so only the handlers you actually call get instantiated.</p>
<h2 id="messenger-gets-built-in-handlers">Messenger gets built-in handlers</h2>
<p>Three new message classes cover common tasks that previously required custom handlers.</p>
<p><code>RunProcessMessage</code> dispatches a <code>Process</code> command through the bus. <code>RunCommandMessage</code> does the same for console commands. Both return a context object with the exit code and output. <code>PingWebhookMessage</code> pings a URL, which is useful for monitoring scheduled tasks without spinning up a dedicated health-check service:</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>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">bus</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">RunCommandMessage</span>(<span style="color:#e6db74">&#39;cache:clear&#39;</span>));
</span></span><span style="display:flex;"><span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">bus</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PingWebhookMessage</span>(<span style="color:#e6db74">&#39;GET&#39;</span>, <span style="color:#e6db74">&#39;https://healthchecks.io/ping/abc123&#39;</span>));
</span></span></code></pre></div><p>The subprocess inheritance problem also got addressed with <code>PhpSubprocess</code>. When you run PHP with a custom memory limit (<code>-d memory_limit=-1</code>), child processes launched with <code>Process</code> don&rsquo;t inherit it. <code>PhpSubprocess</code> does:</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>$sub <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PhpSubprocess</span>([<span style="color:#e6db74">&#39;bin/console&#39;</span>, <span style="color:#e6db74">&#39;app:heavy-import&#39;</span>]);
</span></span></code></pre></div><h2 id="security-three-fixes-for-real-situations">Security: three fixes for real situations</h2>
<p>The profiler now shows how security badges were resolved during authentication: which ones passed, which failed, and why. Before, you had to add debug output manually when a custom authenticator wasn&rsquo;t behaving.</p>
<p>Login throttling via RateLimiter now hashes PII in logs automatically. IP addresses and usernames get hashed with the kernel secret before they&rsquo;re written. No config needed, no regex on log lines.</p>
<p>Firewall patterns now accept arrays:</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">firewalls</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">no_security</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;^/register$&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;^/api/webhooks/&#34;</span>
</span></span></code></pre></div><p>No more regex gymnastics for multi-path exclusions.</p>
<h2 id="logout-without-a-dummy-controller">Logout without a dummy controller</h2>
<p>The logout route used to require a controller that did nothing but throw an exception, with a comment explaining that yes, this is intentional. 6.4 eliminates that:</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/security.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">_security_logout</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resource</span>: <span style="color:#ae81ff">security.route_loader.logout</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">service</span>
</span></span></code></pre></div><p>The route loader handles it. The dummy controller is gone. Flex updates the recipe.</p>
<h2 id="the-serializer-in-better-shape">The serializer in better shape</h2>
<p>Three serializer improvements that each solve a real problem.</p>
<p>Class-level <code>#[Groups]</code> attribute: apply a group to the entire class, then override per property. Useful when a resource has a default serialization group and a few fields that need finer control.</p>
<p>Translatable objects now have a dedicated normalizer. Translatable strings (wrapping Doctrine&rsquo;s <code>TranslatableInterface</code>) get translated to the locale passed via <code>NORMALIZATION_LOCALE_KEY</code> during normalization. Before this, you had to write a custom normalizer.</p>
<p>In debug mode, JSON decoding errors now use <code>seld/jsonlint</code> for better messages. Instead of &ldquo;Syntax error&rdquo;, you get the line and what actually went wrong:</p>
<pre tabindex="0"><code>Parse error on line 1: {&#39;foo&#39;: &#39;bar&#39;}
           ^ Invalid string, used single quotes instead of double quotes
</code></pre><h2 id="profilers-for-the-things-that-werent-http-requests">Profilers for the things that weren&rsquo;t HTTP requests</h2>
<p>The command profiler extends the existing profiler to console commands. Add <code>--profile</code> to any command and get a full profiler entry: input/output, execution time, memory, database queries, log messages. Commands that used to need <code>--verbose</code> plus manual timing now have the same debugging experience as HTTP requests.</p>
<p>The workflow profiler does the same for state machines. A new panel shows a graphical representation of your workflows and which transitions fired during the request. Zero configuration.</p>
<h2 id="the-dx-accumulation">The DX accumulation</h2>
<p>Several smaller additions that compound.</p>
<p><code>renderBlock()</code> and <code>renderBlockView()</code> on <code>AbstractController</code> let you render a named Twig block and return it as a <code>Response</code> or string. Handy for Turbo Stream responses where you want to update a fragment without a full controller action.</p>
<p>The <code>defined</code> env var processor returns a boolean rather than the value: <code>true</code> if the variable exists and is non-empty, <code>false</code> otherwise. Useful for feature flags driven by environment variables:</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">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">is_feature_enabled</span>: <span style="color:#e6db74">&#39;%env(defined:FEATURE_FLAG_KEY)%&#39;</span>
</span></span></code></pre></div><p><code>HttpClient</code> now accepts <code>max_retries</code> per request, overriding the global retry strategy. The Finder component&rsquo;s <code>filter()</code> method accepts a second argument to prune entire directories early, which matters when you&rsquo;re searching large trees.</p>
<p>The <code>BrowserKit</code> <code>click()</code> method now accepts server parameters as extra headers, useful in functional tests that need to simulate authenticated API calls while following links.</p>
<h2 id="impersonation-becomes-usable-in-templates">Impersonation becomes usable in templates</h2>
<p>Two new Twig helpers: <code>impersonation_path()</code> and <code>impersonation_url()</code>. They generate the correct URLs including the switch-user query parameter, which is configurable and has no business being hardcoded in templates. Pair them with the existing <code>impersonation_exit_path()</code> for the full admin impersonation flow.</p>
<h2 id="locale-control-everywhere-it-was-missing">Locale control, everywhere it was missing</h2>
<p>Three gaps filled. <code>TemplatedEmail</code> now has a <code>locale()</code> method for rendering emails in the recipient&rsquo;s language. The locale switcher&rsquo;s <code>runWithLocale()</code> now passes the locale as an argument to the callback, so you don&rsquo;t have to capture it from the outer scope. And <code>app.enabledLocales</code> is available in Twig, so you can build language switchers without hardcoding locale lists.</p>
<h2 id="deploying-to-read-only-filesystems">Deploying to read-only filesystems</h2>
<p><code>APP_BUILD_DIR</code> is now an environment variable recognized by the kernel. Set it to redirect compiled artifacts (router cache, Doctrine proxies, preloaded translations) to a directory that exists, even when the default cache directory doesn&rsquo;t. The <code>MicroKernelTrait</code> uses it automatically. The <code>WarmableInterface</code> gained a <code>$buildDir</code> parameter to support this separation: custom cache warmers that write read-only artifacts should update accordingly.</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><item><title>Symfony 3.4 LTS: the bridge you actually want to cross</title><link>https://guillaumedelre.github.io/2018/01/12/symfony-3.4-lts-the-bridge-you-actually-want-to-cross/</link><pubDate>Fri, 12 Jan 2018 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2018/01/12/symfony-3.4-lts-the-bridge-you-actually-want-to-cross/</guid><description>Part 2 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 3.4 LTS is the migration bridge: same features as 3.3 plus every deprecation warning that 4.0 will enforce.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 3.4 and 4.0 were released the same day: November 30, 2017. That&rsquo;s not a coincidence, it&rsquo;s the strategy.</p>
<p>3.4 is not a feature release. It ships with exactly the same features as 3.3, plus every deprecation warning that 4.0 will enforce. Its whole purpose is to be the migration tool: upgrade from 3.3 to 3.4, fix what&rsquo;s in your logs, then step to 4.0 cleanly.</p>
<h2 id="why-lts-releases-matter-in-symfonys-model">Why LTS releases matter in Symfony&rsquo;s model</h2>
<p>Symfony releases a new minor version every six months. That pace would be brutal for production apps to follow, so the project designates every fourth minor as an LTS: three years of bug fixes, four of security fixes. Which means teams can target 3.4 and mostly stop thinking about upgrades for a while.</p>
<p>3.4 is the last LTS of the 3.x line. If you&rsquo;re still on 2.x or early 3.x, this is your landing zone.</p>
<h2 id="the-deprecation-layer">The deprecation layer</h2>
<p>Every feature that 4.0 removes is deprecated in 3.4. Run your app on 3.4 with deprecation notices enabled and your logs become a to-do list. The common ones:</p>
<ul>
<li>Services without explicit visibility (public/private) generate warnings — 4.0 makes all services private by default</li>
<li><code>ControllerTrait</code> is deprecated in favor of <code>AbstractController</code></li>
<li>The old security authenticator interfaces are marked for removal</li>
<li>YAML-only service configuration without autowiring annotations triggers warnings</li>
</ul>
<p>The intended workflow: upgrade to 3.4, run the test suite with deprecation notices as errors (<code>SYMFONY_DEPRECATIONS_HELPER=max[self]=0</code> in PHPUnit), fix everything that fails. After that, the upgrade to 4.0 is basically mechanical.</p>
<h2 id="the-support-window">The support window</h2>
<p>3.4 LTS receives bug fixes until November 2020 and security fixes until November 2021. That&rsquo;s a comfortable runway for apps that can&rsquo;t follow every release. The cost: staying on the 3.x architecture, with no Flex, no micro-framework structure, no zero-config autowiring by default.</p>
<p>The bridge is there. Whether and when you cross it is a business decision, not a technical one.</p>
<h2 id="services-go-private">Services go private</h2>
<p>3.4 flipped the default visibility of services from public to private. Before this, <code>$container-&gt;get('app.my_service')</code> was perfectly normal code. After this, it&rsquo;s an anti-pattern that generates a deprecation warning in 3.4 and breaks entirely in 4.0.</p>
<p>The reasoning is simple: fetching services directly from the container hides dependencies and defeats static analysis. If you inject through the constructor, the container can optimize the graph, tree-shake unused services, and catch mistakes at compile time. If you pull them at runtime, it can&rsquo;t.</p>
<p>For apps already using autowiring, the migration is usually small. The sticky point is controllers that extend <code>Controller</code> and call <code>$this-&gt;get('something')</code>. The fix is switching to <code>AbstractController</code>, which provides the same shortcuts but through lazy service locators instead of raw container access.</p>
<p>For services that genuinely need to be public (accessed from legacy code or functional tests), mark them explicitly:</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">App\Service\LegacyAdapter</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">public</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><h2 id="binding-scalar-arguments-once">Binding scalar arguments once</h2>
<p>A classic friction point with autowiring: scalar constructor arguments. If ten services all need <code>$projectDir</code>, you had to configure each one individually. The <code>bind</code> key under <code>_defaults</code> fixes that:</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">autowire</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">autoconfigure</span>: <span style="color:#66d9ef">true</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">$projectDir</span>: <span style="color:#e6db74">&#39;%kernel.project_dir%&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">$mailerDsn</span>: <span style="color:#e6db74">&#39;%env(MAILER_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">Psr\Log\LoggerInterface $auditLogger</span>: <span style="color:#e6db74">&#39;@monolog.logger.audit&#39;</span>
</span></span></code></pre></div><p>Any service with a constructor parameter named <code>$projectDir</code> gets the bound value automatically. You can also bind by type-hint, which handles the common case where multiple logger channels exist and you need a specific one. Bindings in <code>_defaults</code> apply to all services in the file; you can override per-service if needed.</p>
<h2 id="injecting-tagged-services-without-a-compiler-pass">Injecting tagged services without a compiler pass</h2>
<p>Before 3.4, collecting all services with a given tag meant writing a compiler pass. Now there&rsquo;s a YAML shorthand:</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">App\Chain\TransformerChain</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">arguments</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">$transformers</span>: !<span style="color:#ae81ff">tagged app.transformer</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">TransformerChain</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 style="color:#66d9ef">private</span> <span style="color:#a6e22e">iterable</span> $transformers) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>!tagged</code> notation creates an <code>IteratorArgument</code>: services are lazily instantiated as you iterate, so unused transformers never get built. For ordering, add a <code>priority</code> attribute to the tag definition on each service.</p>
<h2 id="a-logger-that-ships-with-the-framework">A logger that ships with the framework</h2>
<p>No Monolog? No problem. Symfony 3.4 includes a PSR-3 logger that writes to <code>php://stderr</code> by default. Autowire it with <code>Psr\Log\LoggerInterface</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">use</span> <span style="color:#a6e22e">Psr\Log\LoggerInterface</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">MyService</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 style="color:#66d9ef">private</span> <span style="color:#a6e22e">LoggerInterface</span> $logger) {}
</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">doSomething</span>()<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>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">logger</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">warning</span>(<span style="color:#e6db74">&#39;Something questionable happened&#39;</span>, [<span style="color:#e6db74">&#39;context&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;here&#39;</span>]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The default minimum level is <code>warning</code>. The target is container and Kubernetes workloads where stderr is the natural log sink. It&rsquo;s deliberately minimal: no handlers, no processors, no channels. When you need those, install Monolog.</p>
<h2 id="guard-authenticators-got-a-supports-method">Guard authenticators got a supports() method</h2>
<p>The Guard component&rsquo;s <code>getCredentials()</code> method was pulling double duty: deciding whether the authenticator should handle the request, and extracting the credentials. Returning <code>null</code> was the signal to skip. That made the contract messy.</p>
<p>3.4 added <code>supports()</code> to separate those concerns:</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">ApiTokenAuthenticator</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractGuardAuthenticator</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">supports</span>(<span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#a6e22e">bool</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">headers</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">has</span>(<span style="color:#e6db74">&#39;X-API-TOKEN&#39;</span>);
</span></span><span style="display:flex;"><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">getCredentials</span>(<span style="color:#a6e22e">Request</span> $request)<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:#75715e">// Only called when supports() returns true.
</span></span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Must always return credentials now.
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> [<span style="color:#e6db74">&#39;token&#39;</span> <span style="color:#f92672">=&gt;</span> $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">headers</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;X-API-TOKEN&#39;</span>)];
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The old <code>GuardAuthenticatorInterface</code> is deprecated. The practical benefit: base classes can implement shared <code>getUser()</code> and <code>checkCredentials()</code> logic, while subclasses only override <code>supports()</code> and <code>getCredentials()</code>. One responsibility each.</p>
<h2 id="two-new-debug-commands">Two new debug commands</h2>
<p><code>debug:autowiring</code> replaces the old <code>debug:container --types</code> for discovering which type-hints work with autowiring:</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 debug:autowiring log
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Autowirable Services
</span></span><span style="display:flex;"><span><span style="color:#f92672">====================</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  Psr<span style="color:#ae81ff">\L</span>og<span style="color:#ae81ff">\L</span>oggerInterface
</span></span><span style="display:flex;"><span>      alias to monolog.logger
</span></span><span style="display:flex;"><span>  Psr<span style="color:#ae81ff">\L</span>og<span style="color:#ae81ff">\L</span>oggerInterface $auditLogger
</span></span><span style="display:flex;"><span>      alias to monolog.logger.audit
</span></span></code></pre></div><p>Pass a keyword to filter. No more guessing whether it&rsquo;s <code>LoggerInterface</code> or <code>Logger</code>.</p>
<p><code>debug:form</code> gives you the same introspection capability for form 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-bash" data-lang="bash"><span style="display:flex;"><span>$ bin/console debug:form App<span style="color:#ae81ff">\F</span>orm<span style="color:#ae81ff">\O</span>rderType label_attr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Option: label_attr
</span></span><span style="display:flex;"><span>  Required: false
</span></span><span style="display:flex;"><span>  Default: <span style="color:#f92672">[]</span>
</span></span><span style="display:flex;"><span>  Allowed types: array
</span></span></code></pre></div><p>Without arguments it lists all registered form types, extensions, and guessers. With a type name and option name it shows every constraint on that option. Before this, you either read the source or trial-and-errored your way through.</p>
<h2 id="sessions-got-stricter-by-default">Sessions got stricter by default</h2>
<p>3.4 implements PHP 7.0&rsquo;s <code>SessionUpdateTimestampHandlerInterface</code>, which brings two things: lazy session writes (only written when data actually changed) and strict session ID validation (IDs that don&rsquo;t exist in the store are rejected rather than silently created, which blocks a class of session fixation attacks).</p>
<p>The old <code>WriteCheckSessionHandler</code>, <code>NativeSessionHandler</code>, and <code>NativeProxy</code> classes are deprecated. The <code>MemcacheSessionHandler</code> (note: not Memcached) is gone too, since the underlying PECL extension stopped receiving PHP 7 updates.</p>
<h2 id="twig-form-themes-can-now-be-scoped">Twig form themes can now be scoped</h2>
<p>Global form themes apply to every form in the app. If one form needs a completely different look, you had no clean way to opt out. The <code>only</code> keyword handles that:</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-twig" data-lang="twig"><span style="display:flex;"><span><span style="color:#75715e">{%</span> <span style="color:#66d9ef">raw</span> <span style="color:#75715e">%}</span>{% form_theme orderForm with [&#39;form/order_layout.html.twig&#39;] only %}<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endraw</span> <span style="color:#75715e">%}</span>
</span></span></code></pre></div><p>The <code>only</code> keyword disables all global themes for that form, including the base <code>form_div_layout.html.twig</code>. Your custom theme then needs to either provide all the blocks it uses, or explicitly pull them in with <code>{% raw %}{% use 'form_div_layout.html.twig' %}{% endraw %}</code>.</p>
<h2 id="overriding-bundle-templates-without-infinite-loops">Overriding bundle templates without infinite loops</h2>
<p>Overriding a bundle template that you also need to extend used to cause a circular reference error. Override <code>@TwigBundle/Exception/error404.html.twig</code> and also try to inherit from it? The old namespace resolution would follow your override and loop forever.</p>
<p>3.4 introduced the <code>@!</code> prefix to explicitly reference the original bundle template, bypassing any overrides:</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-twig" data-lang="twig"><span style="display:flex;"><span><span style="color:#75715e">{%</span> <span style="color:#66d9ef">raw</span> <span style="color:#75715e">%}</span>{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
</span></span><span style="display:flex;"><span>{% extends &#39;@!Twig/Exception/error404.html.twig&#39; %}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{% block title %}Page not found{% endblock %}<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endraw</span> <span style="color:#75715e">%}</span>
</span></span></code></pre></div><p><code>@TwigBundle</code> resolves to your override if one exists. <code>@!TwigBundle</code> always resolves to the original. Override-and-extend, without the gymnastics.</p>
]]></content:encoded></item></channel></rss>