<?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>Configuration on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/configuration/</link><description>Recent content in Configuration 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/configuration/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></channel></rss>