<?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>Dependency-Injection on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/dependency-injection/</link><description>Recent content in Dependency-Injection on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Thu, 04 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/dependency-injection/index.xml" rel="self" type="application/rss+xml"/><item><title>Symfony 8.1 : kernel sans HTTP, rate limiting déclaratif et batch fetching Messenger</title><link>https://guillaumedelre.github.io/fr/2026/06/04/symfony-8.1-kernel-sans-http-rate-limiting-d%C3%A9claratif-et-batch-fetching-messenger/</link><pubDate>Thu, 04 Jun 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/06/04/symfony-8.1-kernel-sans-http-rate-limiting-d%C3%A9claratif-et-batch-fetching-messenger/</guid><description>Part 12 of 12 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 8.1 découple le kernel d&amp;#39;HttpKernel, ajoute un attribut #[RateLimit] et livre des améliorations concrètes dans Messenger, Console et HttpClient.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 8.1 est sorti le 29 mai 2026. PHP 8.4 reste le minimum requis et aucun changement cassant n&rsquo;est introduit. L&rsquo;ajout principal est architectural : le kernel n&rsquo;est plus couplé à <code>HttpKernel</code>. Le reste est incrémental mais genuinement utile.</p>
<h2 id="une-application-sans-http">Une application sans HTTP</h2>
<p>Depuis les débuts de Symfony, chaque application embarque un kernel basé sur <code>HttpKernel</code>, même quand elle ne sert aucun trafic HTTP. Un worker Messenger qui consomme depuis SQS traînait malgré lui toute la machinerie HTTP. 8.1 règle ça à la racine.</p>
<p><code>AbstractKernel</code> et <code>KernelTrait</code> vivent maintenant dans le composant <code>DependencyInjection</code>. <code>HttpKernel\Kernel</code> étend <code>AbstractKernel</code>, donc les applications existantes sont entièrement compatibles. Ce qui est nouveau, c&rsquo;est la possibilité d&rsquo;écrire un kernel qui étend <code>AbstractKernel</code> directement, sans la couche HTTP.</p>
<p>Deux nouveaux bundles rendent ça utile immédiatement :</p>
<ul>
<li><code>ServicesBundle</code> câble le DI, l&rsquo;event dispatcher et le clock, sans dépendance HTTP.</li>
<li><code>ConsoleBundle</code> s&rsquo;appuie dessus et ajoute le résolveur de commandes.</li>
</ul>
<p>Pour une application CLI uniquement :</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">// config/bundles.php
</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">Symfony\Component\Console\ConsoleBundle</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;all&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>],
</span></span><span style="display:flex;"><span>];
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> <span style="color:#a6e22e">App</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\DependencyInjection\Kernel\AbstractKernel</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\DependencyInjection\Kernel\KernelTrait</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">Kernel</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractKernel</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#a6e22e">KernelTrait</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Les auteurs de bundles héritent d&rsquo;un attribut <code>#[RequiredBundle]</code> pour déclarer explicitement les dépendances entre bundles, avec un flag <code>ignoreOnInvalid</code> pour les dépendances optionnelles :</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">RequiredBundle</span>(<span style="color:#a6e22e">AcmeCoreBundle</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>)]
</span></span><span style="display:flex;"><span>#[<span style="color:#a6e22e">RequiredBundle</span>(<span style="color:#a6e22e">AcmeUtilBundle</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>, <span style="color:#a6e22e">ignoreOnInvalid</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AcmeBlogBundle</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractBundle</span> {}
</span></span></code></pre></div><h2 id="rate-limiting-déclaratif">Rate limiting déclaratif</h2>
<p>8.1 ajoute un attribut <code>#[RateLimit]</code> pour les controllers. Posé sur une action, il applique la limite configurée dans <code>framework.rate_limiter</code> et renvoie automatiquement un 429 avec <code>Retry-After</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">Symfony\Component\HttpKernel\Attribute\RateLimit</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">ApiController</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractController</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    #[<span style="color:#a6e22e">RateLimit</span>(<span style="color:#e6db74">&#39;api&#39;</span>)]
</span></span><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:#f92672">:</span> <span style="color:#a6e22e">JsonResponse</span> { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    #[<span style="color:#a6e22e">RateLimit</span>(<span style="color:#e6db74">&#39;api&#39;</span>, <span style="color:#a6e22e">methods</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;POST&#39;</span>, <span style="color:#e6db74">&#39;PUT&#39;</span>])]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">edit</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">JsonResponse</span> { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    #[<span style="color:#a6e22e">RateLimit</span>(<span style="color:#e6db74">&#39;api&#39;</span>, <span style="color:#a6e22e">tokens</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">5</span>)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">export</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">JsonResponse</span> { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;attribut est répétable, il est donc possible d&rsquo;empiler deux politiques sur la même action. Par défaut, la clé de bucket combine l&rsquo;IP client, la méthode HTTP et le path. Elle peut être remplacée par une expression pour des buckets par utilisateur :</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\ExpressionLanguage\Expression</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>#[<span style="color:#a6e22e">RateLimit</span>(<span style="color:#e6db74">&#39;per_account&#39;</span>, <span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Expression</span>(<span style="color:#e6db74">&#39;request.request.get(&#34;email&#34;)&#39;</span>))]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">public</span> <span style="color:#a6e22e">function</span> <span style="color:#a6e22e">resetPassword</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span> { <span style="color:#75715e">/* ... */</span> }
</span></span></code></pre></div><p>La politique <code>fixed_window</code> gagne <code>anchor_at</code>, qui aligne les resets sur un moment calendaire plutôt que sur la première requête. Pratique pour les quotas de facturation mensuelle :</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">rate_limiter</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">api_quota</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">policy</span>: <span style="color:#e6db74">&#39;fixed_window&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">limit</span>: <span style="color:#ae81ff">10000</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">interval</span>: <span style="color:#e6db74">&#39;1 month&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">anchor_at</span>: <span style="color:#e6db74">&#39;2026-01-05 00:00:00 UTC&#39;</span>
</span></span></code></pre></div><h2 id="injection-de-dépendances">Injection de dépendances</h2>
<p>Plusieurs améliorations DI arrivent dans 8.1.</p>
<p><strong>Env vars en <code>Closure</code> ou <code>Stringable</code>.</strong> Les workers longue durée ont parfois besoin de rafraîchir des variables d&rsquo;environnement entre les itérations. Autowirer une env var en <code>Closure</code> fournit une factory plutôt qu&rsquo;une valeur figée :</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">Worker</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:#a6e22e">Autowire</span>(<span style="color:#a6e22e">env</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;DB_URL&#39;</span>)]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">\Closure</span> $dbUrl,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        #[<span style="color:#a6e22e">Autowire</span>(<span style="color:#a6e22e">env</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;APP_NAME&#39;</span>)]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span><span style="color:#f92672">|</span><span style="color:#a6e22e">\Stringable</span> $appName <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;default&#39;</span>,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong><code>#[AsTagDecorator]</code>.</strong> Décorer tous les services portant un tag donné en posant un seul attribut sur la classe décorateur :</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">AsTagDecorator</span>(<span style="color:#e6db74">&#39;app.handler&#39;</span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LoggingHandler</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">object</span> $inner) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Env vars avec des points dans le nom.</strong> Les noms comme <code>DATABASE.PRIMARY.URL</code> sont désormais valides, ce qui compte quand on consomme des variables structurées par des plateformes cloud.</p>
<h2 id="messenger">Messenger</h2>
<p>Messenger reçoit plusieurs ajouts concrets dans 8.1.</p>
<p><strong>Batch fetching.</strong> La nouvelle option <code>--fetch-size</code> de <code>messenger:consume</code> réduit le nombre d&rsquo;aller-retours vers le transport. Avec SQS (qui autorise jusqu&rsquo;à 10 messages par appel), ça coupe une bonne partie de l&rsquo;overhead à fort débit :</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>php bin/console messenger:consume async --fetch-size<span style="color:#f92672">=</span><span style="color:#ae81ff">8</span>
</span></span></code></pre></div><p><strong>Nom de type sérialisé personnalisé.</strong> Quand un consommateur non-Symfony identifie un message par une chaîne stable plutôt que par un nom de classe PHP, <code>#[AsMessage]</code> couvre ce cas :</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">AsMessage</span>(<span style="color:#a6e22e">serializedTypeName</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;crawler.vectorization_finished&#39;</span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#a6e22e">readonly</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">VectorizationFinished</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">public</span> <span style="color:#a6e22e">string</span> $crawlId) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Priorité AMQP par message.</strong> <code>AmqpPriorityStamp</code> définit la priorité sur un dispatch individuel plutôt qu&rsquo;au niveau de la queue :</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>$bus<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($message, [<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">AmqpPriorityStamp</span>(<span style="color:#ae81ff">5</span>)]);
</span></span></code></pre></div><p><strong>Contrôle du reset.</strong> <code>--no-reset=100</code> exécute le reset des services tous les 100 messages plutôt qu&rsquo;après chacun, ce qui réduit l&rsquo;overhead des workers longue durée à grande échelle.</p>
<p><strong>Idle timeout pour <code>BatchHandler</code>.</strong> Les batches partiels sont maintenant flushés après une période d&rsquo;inactivité configurable, pas seulement quand le batch est plein.</p>
<p><strong>Redis receiver listable.</strong> Le receiver Redis expose maintenant <code>all()</code> et <code>find()</code>, ce qui permet d&rsquo;inspecter les messages en attente par programmation.</p>
<h2 id="console">Console</h2>
<p><strong>Commandes méthodes.</strong> Plusieurs commandes peuvent cohabiter dans une même classe comme méthodes distinctes. Moins de boilerplate quand une fonctionnalité génère un groupe de commandes liées.</p>
<p><strong><code>#[AskChoice]</code>.</strong> Déclarer un prompt de choix interactif directement dans la signature de l&rsquo;argument, avec support des enums :</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">AsCommand</span>(<span style="color:#e6db74">&#39;app:create-user&#39;</span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CreateUserCommand</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></span><span style="display:flex;"><span>        #[<span style="color:#a6e22e">Argument</span>, <span style="color:#a6e22e">AskChoice</span>(<span style="color:#e6db74">&#39;Select a role&#39;</span>, [<span style="color:#e6db74">&#39;admin&#39;</span>, <span style="color:#e6db74">&#39;editor&#39;</span>, <span style="color:#e6db74">&#39;viewer&#39;</span>])]
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">string</span> $role,
</span></span><span style="display:flex;"><span>    )<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span> { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Validation sur <code>#[Ask]</code>.</strong> Les prompts interactifs acceptent maintenant des contraintes de validation :</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">__invoke</span>(
</span></span><span style="display:flex;"><span>    #[<span style="color:#a6e22e">Argument</span>, <span style="color:#a6e22e">Ask</span>(<span style="color:#e6db74">&#39;Enter your email:&#39;</span>, <span style="color:#a6e22e">constraints</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\NotBlank</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\Email</span>(),
</span></span><span style="display:flex;"><span>    ])]
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">string</span> $email,
</span></span><span style="display:flex;"><span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span> { <span style="color:#75715e">/* ... */</span> }
</span></span></code></pre></div><p><strong><code>#[MapInput]</code>.</strong> Mappe automatiquement les arguments et options d&rsquo;une commande dans un DTO validé.</p>
<p><strong><code>RawInputInterface</code>.</strong> Donne accès aux tokens bruts de l&rsquo;input, utile pour forwarder des arguments à un sous-processus sans les re-parser.</p>
<h2 id="httpclient">HttpClient</h2>
<p><strong>Connexions cURL persistantes</strong> (PHP 8.5+) : réutilisation du cache DNS et des sessions SSL entre les requêtes, ce qui réduit la latence pour les clients à haute fréquence.</p>
<p><strong><code>GuzzleHttpHandler</code>.</strong> Symfony HttpClient peut maintenant servir de transport Guzzle, ce qui permet au code existant utilisant Guzzle de bénéficier du retry, du mock et du scopage Symfony sans migration complète :</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\HttpClient\GuzzleHttpHandler</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$guzzle <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\GuzzleHttp\Client</span>([<span style="color:#e6db74">&#39;handler&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GuzzleHttpHandler</span>()]);
</span></span></code></pre></div><p><strong>Allowlist SSRF pour <code>NoPrivateNetworkHttpClient</code>.</strong> Passer une IP ou un range spécifique à autoriser à travers le blocage réseau privé :</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>$client <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">NoPrivateNetworkHttpClient</span>(<span style="color:#a6e22e">HttpClient</span><span style="color:#f92672">::</span><span style="color:#a6e22e">create</span>(), <span style="color:#66d9ef">null</span>, <span style="color:#e6db74">&#39;10.0.0.42&#39;</span>);
</span></span></code></pre></div><p><strong><code>max_connect_duration</code>.</strong> Un timeout limité à la phase de connexion uniquement, pour un contrôle plus fin sur les DNS lents et les handshakes TLS.</p>
<h2 id="mapping-du-payload-de-requête">Mapping du payload de requête</h2>
<p><code>#[MapRequestPayload]</code> gère maintenant le <code>multipart/form-data</code>, ce qui permet d&rsquo;avoir des propriétés <code>UploadedFile</code> dans les DTOs mappés :</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">ProductDto</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span> $name <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">UploadedFile</span> $image <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</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">upload</span>(#[<span style="color:#a6e22e">MapRequestPayload</span>] <span style="color:#a6e22e">ProductDto</span> $data)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span> { <span style="color:#75715e">/* ... */</span> }
</span></span></code></pre></div><p>Les arguments variadiques permettent de mapper un tableau JSON directement en une série d&rsquo;objets typés :</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">createPrices</span>(#[<span style="color:#a6e22e">MapRequestPayload</span>] <span style="color:#a6e22e">Price</span> <span style="color:#f92672">...</span>$prices)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span> { <span style="color:#75715e">/* ... */</span> }
</span></span></code></pre></div><p><code>validationGroups</code> accepte maintenant une <code>Expression</code> ou une <code>Closure</code> pour une sélection de groupes dynamique. <code>mapWhenEmpty: true</code> déclenche la dénormalisation même sur un payload vide.</p>
<h2 id="formulaires">Formulaires</h2>
<p>Le thème daisyUI 5 est maintenant inclus, ce qui couvre les frontends Tailwind sans thème personnalisé :</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">twig</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">form_themes</span>: [<span style="color:#e6db74">&#39;daisyui_5_layout.html.twig&#39;</span>]
</span></span></code></pre></div><p><code>DateType</code> en mode <code>choice</code> accepte une option <code>labels</code> pour renommer les selects année, mois et jour sans template personnalisé. Les checkboxes non cochées sont maintenant soumises automatiquement comme <code>false</code> plutôt qu&rsquo;être absentes du payload, ce qui corrige une incohérence de longue date.</p>
<h2 id="sérialisation-automatique-des-réponses">Sérialisation automatique des réponses</h2>
<p>L&rsquo;attribut <code>#[Serialize]</code> posé sur une méthode de controller sérialise automatiquement la valeur de retour dans une <code>Response</code>, en choisissant le format (JSON, XML) depuis le format de la requête. Fini les <code>$this-&gt;json()</code> manuels pour les endpoints API simples :</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\HttpKernel\Attribute\Serialize</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\Serializer\Normalizer\DateTimeNormalizer</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#a6e22e">readonly</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CreateProductController</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    #[<span style="color:#a6e22e">Serialize</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">code</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">201</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;X-Custom-Header&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;abc&#39;</span>],
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">context</span><span style="color:#f92672">:</span> [<span style="color:#a6e22e">DateTimeNormalizer</span><span style="color:#f92672">::</span><span style="color:#a6e22e">FORMAT_KEY</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;d.m.Y H:i:s&#39;</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:#f92672">:</span> <span style="color:#a6e22e">ProductCreated</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ProductCreated</span>(<span style="color:#ae81ff">101</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Une route avec <code>{_format}</code> retourne du JSON pour <code>/products/42.json</code> et du XML pour <code>/products/42.xml</code>. Les formats non supportés donnent un 415.</p>
<h2 id="deepcloner">DeepCloner</h2>
<p><code>DeepCloner</code> arrive dans le composant <code>VarExporter</code> comme remplacement de <code>Instantiator</code> et <code>Hydrator</code> (tous deux dépréciés en 8.1). Il clone des graphes d&rsquo;objets PHP complexes directement, sans le round-trip <code>unserialize(serialize())</code>, en préservant la sémantique copy-on-write pour les strings et les tableaux.</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\VarExporter\DeepCloner</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// clone one-shot
</span></span></span><span style="display:flex;"><span>$clone <span style="color:#f92672">=</span> <span style="color:#a6e22e">DeepCloner</span><span style="color:#f92672">::</span><span style="color:#a6e22e">deepClone</span>($originalObject);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// cloner réutilisable sur le même prototype
</span></span></span><span style="display:flex;"><span>$cloner <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">DeepCloner</span>($prototype);
</span></span><span style="display:flex;"><span>$clone1 <span style="color:#f92672">=</span> $cloner<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">clone</span>();
</span></span><span style="display:flex;"><span>$clone2 <span style="color:#f92672">=</span> $cloner<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">clone</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// clone vers une sous-classe compatible
</span></span></span><span style="display:flex;"><span>$childDefinition <span style="color:#f92672">=</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">DeepCloner</span>($definition))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">cloneAs</span>(<span style="color:#a6e22e">ChildDefinition</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>);
</span></span></code></pre></div><p>Une fonction <code>deepclone_hydrate()</code> remplace le <code>Hydrator</code> déprécié :</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>$user <span style="color:#f92672">=</span> <span style="color:#a6e22e">deepclone_hydrate</span>(<span style="color:#a6e22e">User</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>, [<span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Alice&#39;</span>]);
</span></span></code></pre></div><p>DI, FrameworkBundle, Form et Cache (ArrayAdapter) l&rsquo;utilisent tous en interne dans 8.1. Une extension C optionnelle (<code>symfony/php-ext-deepclone</code>) est disponible pour des performances natives.</p>
<h2 id="attribut-cache-amélioré">Attribut #[Cache] amélioré</h2>
<p>L&rsquo;attribut <code>#[Cache]</code> sur les méthodes de controller gagne trois choses en 8.1.</p>
<p><strong>Variables d&rsquo;expression nommées.</strong> <code>lastModified</code> et <code>etag</code> exposent maintenant <code>request</code> (l&rsquo;objet <code>Request</code>) et <code>args</code> (les arguments du controller sous forme de tableau), en remplacement de la fusion plate précédente qui causait des collisions de noms :</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">Cache</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">etag</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;args[&#39;article&#39;].computeETag()&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">lastModified</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;args[&#39;article&#39;].getUpdatedAt()&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span><span style="color:#f92672">:</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">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">show</span>(<span style="color:#a6e22e">Article</span> $article)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span> { <span style="color:#f92672">...</span> }
</span></span></code></pre></div><p><strong>Support des closures</strong> pour <code>lastModified</code> et <code>etag</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:#a6e22e">Cache</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">lastModified</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> (<span style="color:#66d9ef">array</span> $args, <span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#a6e22e">\DateTimeInterface</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">return</span> $args[<span style="color:#e6db74">&#39;post&#39;</span>]<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getUpdatedAt</span>();
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">etag</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> (<span style="color:#66d9ef">array</span> $args, <span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> (<span style="color:#a6e22e">string</span>) $args[<span style="color:#e6db74">&#39;post&#39;</span>]<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getId</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">show</span>(<span style="color:#a6e22e">Post</span> $post)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span> { <span style="color:#f92672">...</span> }
</span></span></code></pre></div><p><strong>Application conditionnelle</strong> via <code>if</code> (expression string ou closure retournant un bool). Mettre en cache une réponse uniquement quand la requête ne porte pas de paramètre <code>preview</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:#a6e22e">Cache</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">maxage</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">3600</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">static</span> <span style="color:#a6e22e">fn</span> (<span style="color:#66d9ef">array</span> $args, <span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#a6e22e">bool</span> <span style="color:#f92672">=&gt;</span> <span style="color:#f92672">!</span>$request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">query</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">has</span>(<span style="color:#e6db74">&#39;preview&#39;</span>),
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">public</span> <span style="color:#a6e22e">function</span> <span style="color:#a6e22e">show</span>(<span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span> { <span style="color:#f92672">...</span> }
</span></span></code></pre></div><p>L&rsquo;attribut est aussi répétable, ce qui permet d&rsquo;empiler plusieurs politiques avec des conditions différentes sur la même méthode.</p>
<h2 id="json-streaming-et-jsonpath">JSON streaming et JsonPath</h2>
<p><strong>Value objects dans JsonStreamer.</strong> Une interface <code>ValueObjectTransformerInterface</code> couvre les types qui se sérialisent en scalaire et vice-versa. Il suffit de l&rsquo;implémenter et de taguer le 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><span style="color:#e6db74">/** @implements ValueObjectTransformerInterface&lt;Money, string&gt; */</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MoneyTransformer</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ValueObjectTransformerInterface</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">transform</span>(<span style="color:#a6e22e">object</span> $object, <span style="color:#66d9ef">array</span> $options <span style="color:#f92672">=</span> [])<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $object<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">amount</span><span style="color:#f92672">.</span><span style="color:#e6db74">&#39; &#39;</span><span style="color:#f92672">.</span>$object<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">currency</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">reverseTransform</span>(<span style="color:#a6e22e">string</span> $scalar, <span style="color:#66d9ef">array</span> $options <span style="color:#f92672">=</span> [])<span style="color:#f92672">:</span> <span style="color:#a6e22e">Money</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        [$amount, $currency] <span style="color:#f92672">=</span> <span style="color:#a6e22e">explode</span>(<span style="color:#e6db74">&#39; &#39;</span>, $scalar);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Money</span>((<span style="color:#a6e22e">int</span>) $amount, $currency);
</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">static</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getStreamValueType</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">BuiltinType</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Type</span><span style="color:#f92672">::</span><span style="color:#a6e22e">string</span>(); }
</span></span><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">getValueObjectClassName</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Money</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Types date.</strong> <code>DateInterval</code> se sérialise en ISO 8601 (<code>P2Y6M1DT12H30M5S</code>), <code>DateTimeZone</code> en nom de timezone. L&rsquo;option <code>date_time_timezone</code> gère la conversion à l&rsquo;encode/decode.</p>
<p><strong>Options par défaut via config :</strong></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">json_streamer</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default_options</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">include_null_properties</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p><strong>Fonctions JsonPath personnalisées</strong> via <code>#[AsJsonPathFunction]</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">Symfony\Component\JsonPath\Attribute\AsJsonPathFunction</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>#[<span style="color:#a6e22e">AsJsonPathFunction</span>(<span style="color:#e6db74">&#39;upper&#39;</span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UppercaseFunction</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">mixed</span> $value)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">\is_string</span>($value) <span style="color:#f92672">?</span> <span style="color:#a6e22e">strtoupper</span>($value) <span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$result <span style="color:#f92672">=</span> $crawler<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">find</span>(<span style="color:#e6db74">&#39;$.items[?upper(@.title) == &#34;HELLO&#34;]&#39;</span>);
</span></span></code></pre></div><h2 id="validator">Validator</h2>
<p><strong>Support Clock dans les contraintes de comparaison.</strong> <code>GreaterThan</code>, <code>GreaterThanOrEqual</code>, <code>LessThan</code>, <code>LessThanOrEqual</code> et <code>Range</code> acceptent une <code>ClockInterface</code>. Avec <code>MockClock</code>, les expressions de date relatives comme <code>-10 days</code> se résolvent par rapport à un point fixe dans le temps, rendant la validation temporelle déterministe dans les tests.</p>
<p><strong>Validators réentrants.</strong> Le pattern stateful avec <code>initialize($context)</code> posait des problèmes lors d&rsquo;appels récursifs. Une nouvelle méthode <code>validateInContext()</code> remplace <code>validate()</code> + <code>initialize()</code> sur <code>ConstraintValidatorInterface</code>. Les validators qui étendent la classe abstraite <code>ConstraintValidator</code> n&rsquo;ont rien à changer.</p>
<p><strong>Contrainte <code>Xml</code>.</strong> Valide qu&rsquo;une chaîne est du XML bien formé, avec validation optionnelle via schéma XSD qui reporte les erreurs individuelles avec numéros de ligne.</p>
<p><strong>Vérification d&rsquo;existence de propriété.</strong> <code>ValidatorBuilder::enablePropertyMetadataExistenceCheck()</code> lève une <code>ValidatorException</code> quand une contrainte cible une propriété inexistante, ce qui attrape les typos au warmup.</p>
<h2 id="en-résumé">En résumé</h2>
<p>8.1 est une version dense en fonctionnalités. Le kernel sans HTTP est la pièce architecturalement significative : elle fait de Symfony un choix crédible pour les applications CLI et worker purs, sans le poids de la couche HTTP. Tout le reste gravite autour de l&rsquo;ergonomie : moins de boilerplate sur les controllers (<code>#[Serialize]</code>, <code>#[RateLimit]</code>, <code>#[Cache]</code> amélioré), moins de boilerplate sur les commandes (attributs ask, commandes méthodes), moins de friction dans Messenger (taille de batch, noms de types, priorité AMQP), un clonage profond plus rapide dans VarExporter, et un Validator qui comprend enfin le temps. Beaucoup d&rsquo;irritants de longue date traités en une seule version.</p>
]]></content:encoded></item></channel></rss>