<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Messenger on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/messenger/</link><description>Recent content in Messenger on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Sat, 10 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/messenger/index.xml" rel="self" type="application/rss+xml"/><item><title>Symfony 7.4 LTS : signature de messages, tableaux PHP en config et le dernier 7.x</title><link>https://guillaumedelre.github.io/fr/2026/01/10/symfony-7.4-lts-signature-de-messages-tableaux-php-en-config-et-le-dernier-7.x/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/01/10/symfony-7.4-lts-signature-de-messages-tableaux-php-en-config-et-le-dernier-7.x/</guid><description>Part 10 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 7.4 LTS ajoute la signature de messages Messenger, une configuration en tableaux PHP, et clôt la ligne 7.x.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 7.4 est sorti en novembre 2025, aux côtés de 8.0. C&rsquo;est la dernière LTS de la ligne 7.x : PHP 8.2 minimum, trois ans de corrections de bugs, quatre de sécurité. Pour les équipes qui ne peuvent pas ou ne veulent pas suivre l&rsquo;exigence PHP 8.4 de 8.0, 7.4 est l&rsquo;endroit où atterrir.</p>
<h2 id="la-signature-de-messages-dans-messenger">La signature de messages dans Messenger</h2>
<p>La sécurité des transports dans Messenger a toujours été le problème de l&rsquo;application à résoudre. 7.4 ajoute la signature de messages : un mécanisme basé sur des stamps qui signe les messages dispatchés et valide les signatures à la réception.</p>
<p>Le cas d&rsquo;usage cible est les scénarios multi-tenant ou de transport externe où on a besoin d&rsquo;une preuve cryptographique qu&rsquo;un message n&rsquo;a pas été altéré ni injecté depuis l&rsquo;extérieur. La configuration vit au niveau du transport :</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="configuration-en-tableaux-php">Configuration en tableaux PHP</h2>
<p>Les formats de configuration de Symfony ont toujours été YAML (défaut), XML et PHP. Le format PHP existait mais était maladroit : un DSL builder fluent qui nécessitait du chaînage de méthodes et ne donnait rien d&rsquo;utile à l&rsquo;IDE.</p>
<p>7.4 remplace le format fluent par des tableaux PHP standard. Les IDEs peuvent maintenant vraiment l&rsquo;analyser, <code>config/reference.php</code> est auto-généré comme référence avec annotations de types, et le résultat ressemble à des données plutôt qu&rsquo;à du 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>Le format fluent est déprécié. Les tableaux sont l&rsquo;avenir, et honnêtement c&rsquo;est un meilleur format.</p>
<h2 id="améliorations-oidc">Améliorations OIDC</h2>
<p><code>#[IsSignatureValid]</code> valide les URLs signées directement dans les contrôleurs, supprimant le boilerplate de la validation manuelle. OpenID Connect supporte maintenant plusieurs endpoints de découverte, et une nouvelle commande <code>security:oidc-token:generate</code> rend le dev et les tests beaucoup moins pénibles.</p>
<h2 id="la-fenêtre-de-support">La fenêtre de support</h2>
<p>7.4 LTS : bugs jusqu&rsquo;en novembre 2028, correctifs de sécurité jusqu&rsquo;en novembre 2029. Le chemin vers 8.4 LTS (la prochaine cible long terme) passe par les notices de dépréciation de 7.4 et la mise à jour PHP 8.4. Corriger les dépréciations maintenant et le saut vers 8.x sera beaucoup moins douloureux.</p>
<h2 id="les-attributs-deviennent-plus-précis">Les attributs deviennent plus précis</h2>
<p><code>#[CurrentUser]</code> accepte maintenant les types union, ce qui compte en pratique quand une route est accessible par plus d&rsquo;une classe 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">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> accepte un tableau pour l&rsquo;option <code>env</code>, donc une route de debug active seulement en <code>dev</code> et <code>test</code> n&rsquo;a plus besoin de deux définitions séparées. <code>#[AsDecorator]</code> est maintenant répétable, ce qui signifie qu&rsquo;une classe peut décorer plusieurs services à la fois. Les signatures de méthode <code>#[AsEventListener]</code> acceptent les types d&rsquo;événements union. <code>#[IsGranted]</code> reçoit une option <code>methods</code> pour limiter une vérification d&rsquo;autorisation à des verbes HTTP spécifiques sans dupliquer la route.</p>
<h2 id="la-classe-request-arrête-den-faire-trop">La classe Request arrête d&rsquo;en faire trop</h2>
<p><code>Request::get()</code> est dépréciée, et franchement bonne débarrassance. La méthode cherchait dans les attributs de route, puis les paramètres de query, puis le corps de la requête, dans cet ordre, retournant silencieusement ce qu&rsquo;elle trouvait en premier. Cette ambiguïté causait de vrais bugs. Elle est supprimée dans 8.0 ; dans 7.4 elle fonctionne encore mais déclenche une dépréciation. Les remplacements sont explicites : <code>$request-&gt;attributes-&gt;get()</code>, <code>$request-&gt;query-&gt;get()</code>, <code>$request-&gt;request-&gt;get()</code>.</p>
<p>Le parsing du corps pour les requêtes <code>PUT</code>, <code>PATCH</code>, <code>DELETE</code> et <code>QUERY</code> arrive en même temps. Auparavant Symfony ne parsait <code>application/x-www-form-urlencoded</code> et <code>multipart/form-data</code> que pour <code>POST</code>. Ces mêmes types de contenu sont maintenant parsés pour les autres méthodes accessibles en écriture aussi, ce qui tue un contournement REST API courant.</p>
<p>La surcharge de méthode HTTP pour <code>GET</code>, <code>HEAD</code>, <code>CONNECT</code> et <code>TRACE</code> est dépréciée. Surcharger une méthode sûre avec un header était de toute façon toujours sémantiquement cassé. On peut maintenant autoriser explicitement seulement les méthodes qui ont du sens pour son application :</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="les-workflows-acceptent-les-backedenums">Les Workflows acceptent les BackedEnums</h2>
<p>Les places et transitions de Workflow peuvent maintenant être définies avec des backed enums PHP, à la fois en YAML (via le tag <code>!php/enum</code>) et en config PHP. Le marking store fonctionne avec les valeurs d&rsquo;enum directement, donc le modèle de domaine et la définition du workflow utilisent enfin les mêmes 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="étendre-la-validation-et-la-sérialisation-pour-les-classes-tierces">Étendre la validation et la sérialisation pour les classes tierces</h2>
<p>Besoin d&rsquo;ajouter des métadonnées de validation ou de sérialisation à une classe d&rsquo;un bundle qu&rsquo;on ne possède pas ? 7.4 a <code>#[ExtendsValidationFor]</code> et <code>#[ExtendsSerializationFor]</code> pour ça. On écrit une classe compagnon avec ses annotations supplémentaires, on pointe l&rsquo;attribut vers la classe cible, et Symfony fusionne les métadonnées à la compilation du conteneur :</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 vérifie à la compilation que les propriétés déclarées existent réellement sur la classe cible. Un renommage ne cassera pas silencieusement la validation.</p>
<h2 id="dx--ce-qui-ne-fait-pas-la-une-mais-compte">DX : ce qui ne fait pas la une mais compte</h2>
<p>Le helper Question dans Console accepte un timeout. Demander à l&rsquo;utilisateur de confirmer quelque chose, et s&rsquo;il ne répond pas en N secondes, la réponse par défaut s&rsquo;applique. Très pratique dans les scripts de déploiement qui ne peuvent pas se permettre d&rsquo;attendre éternellement un humain.</p>
<p><code>messenger:consume</code> reçoit <code>--exclude-receivers</code>. Combiné avec <code>--all</code>, il permet de consommer depuis tous les transports sauf des spécifiques :</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>Le mode worker FrankenPHP est maintenant auto-détecté. Si le processus tourne dans FrankenPHP, Symfony bascule en mode worker automatiquement. Pas de package supplémentaire nécessaire.</p>
<p>La commande <code>debug:router</code> cache les colonnes <code>Scheme</code> et <code>Host</code> quand toutes les routes utilisent <code>ANY</code>, ce qui supprime beaucoup de bruit de la sortie par défaut. Les méthodes HTTP sont maintenant aussi colorées.</p>
<p>Les tests fonctionnels reçoivent <code>$client-&gt;getSession()</code> avant la première requête. Auparavant il fallait faire au moins une requête pour accéder à la session, ce qui était agaçant. Maintenant on peut pré-remplir les tokens CSRF ou les flags A/B en amont :</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--store-dynamodb">Lock : store DynamoDB</h2>
<p><code>DynamoDbStore</code> arrive comme nouveau backend de Lock. Utile dans les déploiements AWS-natifs où Redis n&rsquo;est pas dans la stack, et ça fonctionne exactement comme n&rsquo;importe quel autre 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="bridge-doctrine--types-day-point-et-time-point">Bridge Doctrine : types day point et time point</h2>
<p>Deux nouveaux types de colonnes Doctrine : <code>day_point</code> stocke une valeur date uniquement (sans composant heure) et <code>time_point</code> stocke une valeur heure uniquement, tous deux mappant vers <code>DatePoint</code>. Bien quand le domaine sépare genuinement la date de l&rsquo;heure :</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--paramètres-de-query-explicites">Routing : paramètres de query explicites</h2>
<p>La clé <code>_query</code> dans la génération d&rsquo;URL permet de définir les paramètres de query explicitement, séparément des paramètres de route. Ça compte quand un paramètre de route et un paramètre de query partagent le même nom :</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-des-en-têtes-link-entrants">WebLink : parsing des en-têtes Link entrants</h2>
<p><code>HttpHeaderParser</code> parse les en-têtes de réponse <code>Link</code> en objets structurés. Avant ça, parser des en-têtes Link depuis des réponses d&rsquo;API nécessitait soit d&rsquo;importer une bibliothèque tierce, soit d&rsquo;écrire des regex. Le cas d&rsquo;usage : les APIs HTTP qui annoncent des ressources liées ou la pagination via des en-têtes Link, comme le fait l&rsquo;API GitHub.</p>
<h2 id="le-parsing-html5-est-plus-rapide-sur-php-84">Le parsing HTML5 est plus rapide sur PHP 8.4</h2>
<p>DomCrawler et HtmlSanitizer basculent vers le parser HTML5 natif de PHP 8.4 quand il est disponible. Pas de changements de code nécessaires de votre côté. Le parser natif est plus rapide et plus conforme à la spec que le fallback précédent. Sur PHP 8.2 ou 8.3, rien ne change.</p>
<h2 id="translation--staticmessage">Translation : StaticMessage</h2>
<p><code>StaticMessage</code> implémente <code>TranslatableInterface</code> mais ne traduit intentionnellement pas. Elle passe la string inchangée quelle que soit la locale. Le cas d&rsquo;usage : les réponses d&rsquo;API qui doivent rester dans une langue fixe quelle que soit la locale de l&rsquo;utilisateur, ou les entrées de log d&rsquo;audit où on doit préserver le texte original tel quel.</p>
]]></content:encoded></item><item><title>Swarrot vs Symfony Messenger : une comparaison en conditions réelles</title><link>https://guillaumedelre.github.io/fr/2022/01/26/swarrot-vs-symfony-messenger-une-comparaison-en-conditions-r%C3%A9elles/</link><pubDate>Wed, 26 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/01/26/swarrot-vs-symfony-messenger-une-comparaison-en-conditions-r%C3%A9elles/</guid><description>Swarrot et Symfony Messenger gèrent tous deux RabbitMQ en PHP. Voici pourquoi on a gardé Swarrot après avoir sérieusement évalué une migration.</description><content:encoded><![CDATA[<p>On a migré une plateforme de microservices médias vers Symfony 6 début 2022. Douze services, la plupart consommant des messages depuis RabbitMQ via <a href="https://github.com/swarrot/swarrot" target="_blank" rel="noopener noreferrer">Swarrot</a>. Symfony 6 a rendu <a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Messenger</a> plus central que jamais, et pendant la planification de la migration un développeur a posé la question évidente : pourquoi ne pas migrer en même temps ?</p>
<p>Ça vient avec le framework. Ça a de la logique de retry, du support AMQP natif, de la documentation first-party. Notre setup ressemblait à de l&rsquo;artisanat par comparaison.</p>
<p>Question légitime. On l&rsquo;a prise au sérieux. Voilà ce qu&rsquo;on a trouvé.</p>
<h2 id="câbler-la-topologie-à-la-main">Câbler la topologie à la main</h2>
<p>Swarrot est une bibliothèque consumer qui enveloppe l&rsquo;extension PECL AMQP. Elle lit des octets depuis une queue, les fait passer à travers une chaîne de processors (leur terme pour middleware), et laisse votre code décider quoi faire avec le payload. C&rsquo;est vraiment tout.</p>
<p>La chaîne de middleware est la partie intéressante. Les processors sont des décorateurs imbriqués, chacun enveloppant le suivant. Les couches extérieures gèrent les préoccupations d&rsquo;infrastructure avant même que le message n&rsquo;atteigne la logique métier :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">middleware_stack</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.signal_handler&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.max_execution_time&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.exception_catcher&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.doctrine_object_manager&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.ack&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;app.processor.retry&#39;</span>
</span></span></code></pre></div><p><code>signal_handler</code> est en tête parce qu&rsquo;il doit intercepter <code>SIGTERM</code> avant que tout autre processor ne le voie. <code>ack</code> est près du bas parce qu&rsquo;on n&rsquo;acquitte le message qu&rsquo;après que le traitement réussit. L&rsquo;ordre n&rsquo;est pas arbitraire, et il est entièrement visible dans la configuration.</p>
<p>La topologie est tout aussi explicite. On déclare tout soi-même : exchanges, routing keys, queues de retry, queues de lettres mortes :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">messages_types</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_retry</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.retry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_dead</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.dead</span>
</span></span></code></pre></div><p>Trois entrées par type de message logique : queue principale, queue de retry, queue de lettres mortes. Tout ce qui existe sur le broker est nommé ici. La config est verbeuse mais honnête : pas d&rsquo;inférence, pas de convention plutôt que configuration. Si une queue existe dans RabbitMQ, on peut la tracer jusqu&rsquo;à une seule ligne de YAML.</p>
<h2 id="quand-le-nom-de-classe-devient-la-route">Quand le nom de classe devient la route</h2>
<p><a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Symfony Messenger</a> opère un niveau plus haut. On définit une classe de message, un handler, et un transport. La bibliothèque gère la sérialisation, le routing, le retry et les queues d&rsquo;échec automatiquement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">IngestContent</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $contentId,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $source,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">dsn</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">retry_strategy</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">max_retries</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#39;App\Message\IngestContent&#39;</span>: <span style="color:#ae81ff">async</span>
</span></span></code></pre></div><p>Messenger sérialise l&rsquo;objet, le met sur le transport, et le désérialise de l&rsquo;autre côté dans la classe correcte. Pas de topologie manuelle, pas de noms d&rsquo;exchange explicites. Le nom de classe est la primitive de routing.</p>
<p>Cette dernière phrase est exactement là où les choses se sont compliquées pour nous.</p>
<h2 id="quand-le-typage-devient-du-couplage">Quand le typage devient du couplage</h2>
<p>Messenger suppose que le producteur et le consumer partagent une définition de classe PHP. C&rsquo;est bien pour une seule application, ou pour des services qui partagent un package de contrats dédié. Dans un monorepo d&rsquo;applications Symfony indépendantes, ça crée un couplage qui n&rsquo;existe tout simplement pas aujourd&rsquo;hui.</p>
<p>Prenez un message d&rsquo;ingestion de contenu que douze services consomment. Avec Swarrot, chaque service lit le payload JSON brut et prend les champs qui l&rsquo;intéressent. Ajouter un nouveau champ signifie mettre à jour le producteur. Les consumers qui n&rsquo;ont pas besoin du champ continuent de fonctionner sans modification.</p>
<p>Avec Messenger, <code>IngestContent</code> doit être définie quelque part que les douze services peuvent référencer. Ça signifie soit :</p>
<ul>
<li>Un package PHP partagé, versionné, déployé et maintenu à travers les services. Chaque changement de schéma devient un exercice de coordination inter-services.</li>
<li>Des classes dupliquées dans chaque service, qui divergent silencieusement sous la pression.</li>
</ul>
<p>Ni l&rsquo;une ni l&rsquo;autre n&rsquo;est gratuite. L&rsquo;approche package partagé inverse le modèle de propriété : le schéma de message devient une dépendance plutôt qu&rsquo;un contrat défini à la frontière. L&rsquo;approche duplication est juste le problème original différé.</p>
<p>La différence fondamentale est ce que représente un message. Messenger est conçu pour des <strong>commandes typées</strong> : un objet qui porte du sens et se distribue à un handler spécifique. Swarrot traite les messages comme des <strong>données opaques</strong> : des octets qui coulent à travers une topologie, traités par le consumer qui écoute. Si vos messages sont des données, l&rsquo;abstraction supplémentaire qu&rsquo;ajoute Messenger ne vous aide pas. Elle crée de la friction.</p>
<h2 id="le-bloquant">Le bloquant</h2>
<p>Le problème de sérialisation était le décisif. Dans un monorepo où les services sont autonomes, partager des classes PHP entre eux n&rsquo;est pas architecturalement neutre : c&rsquo;est une décision de couplage qui rend les changements futurs plus difficiles. On aurait échangé une bibliothèque nominalement &ldquo;legacy&rdquo; pour une plus moderne tout en introduisant exactement le genre de couplage fort qu&rsquo;on avait passé des années à éviter.</p>
<p>Il y avait des préoccupations secondaires aussi. L&rsquo;extension PECL AMQP donne un accès direct aux fonctionnalités du broker (priorités de messages, TTL par queue, routing par headers exchange) que Messenger abstrait. Et migrer quinze consumers sans jour J signifie faire tourner les deux bibliothèques en parallèle, ce qui est une vraie contrainte opérationnelle.</p>
<p>Mais le problème de sérialisation seul aurait suffi.</p>
<h2 id="données-ou-commandes--voilà-la-question">Données ou commandes : voilà la question</h2>
<p>Le choix ne concerne pas la qualité des bibliothèques. Messenger est bien maintenu, bien documenté, et s&rsquo;intègre proprement dans l&rsquo;écosystème Symfony.</p>
<p>La question à se poser en premier est : que sont vos messages ?</p>
<p>Si ce sont des commandes typées avec un schéma connu et un seul consumer faisant autorité, Messenger est un choix naturel. On écrit une classe, un handler, on configure un transport, et l&rsquo;infrastructure gère le reste.</p>
<p>Si ce sont des payloads de données consommés par plusieurs services indépendants, chacun possédant sa propre désérialisation, l&rsquo;abstraction qu&rsquo;ajoute Messenger joue contre vous. La topologie explicite de Swarrot et son modèle de payload brut donnent plus de contrôle là où on en a vraiment besoin.</p>
<p>Une vraie limitation à garder à l&rsquo;esprit : Swarrot est lié à l&rsquo;extension PECL AMQP, qui n&rsquo;implémente qu&rsquo;AMQP 0-9-1. Ce qui signifie que RabbitMQ (ou un broker compatible) est une dépendance dure. Si l&rsquo;infrastructure migre un jour vers un broker AMQP 1.0 (Azure Service Bus, ActiveMQ Artemis), Swarrot ne peut pas suivre. La couche transport de Messenger abstrait ça proprement : changer de broker signifie changer un DSN, pas réécrire les consumers.</p>
<p>Si la portabilité de broker est une exigence, ou susceptible de le devenir, ça change significativement le calcul.</p>
<p>Swarrot n&rsquo;est pas du legacy à migrer. Pour l&rsquo;instant, c&rsquo;est le bon choix : le routing AMQP comme primitive, les messages comme données, RabbitMQ comme choix d&rsquo;infrastructure long terme.</p>
<p>Ça pourrait changer. Un package de contrats partagé, une nouvelle exigence de broker, un service greenfield qui ne porte pas le poids de la topologie existante : n&rsquo;importe lequel de ces éléments pourrait faire pencher la balance vers Messenger. La bibliothèque n&rsquo;est pas inadaptée à cette plateforme. Elle est peut-être juste la bonne réponse pour une version future de celle-ci.</p>
]]></content:encoded></item><item><title>Symfony 5.4 LTS : support des enums, alias de routes, et le pont vers PHP 8.1</title><link>https://guillaumedelre.github.io/fr/2022/01/10/symfony-5.4-lts-support-des-enums-alias-de-routes-et-le-pont-vers-php-8.1/</link><pubDate>Mon, 10 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/01/10/symfony-5.4-lts-support-des-enums-alias-de-routes-et-le-pont-vers-php-8.1/</guid><description>Part 6 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 5.4 LTS intègre le support natif des enums et l&amp;#39;essentiel des fonctionnalités de 6.0, tout en préservant la compatibilité ascendante.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 5.4 est sorti le 29 novembre 2021, le même jour que Symfony 6.0 et un jour après PHP 8.1. Pas un hasard.</p>
<p>5.4 est la version LTS, et son rôle est de porter autant que possible le jeu de fonctionnalités de 6.0 tout en conservant la compatibilité 5.x. C&rsquo;est aussi la première version de Symfony qui comprend réellement les fonctionnalités de PHP 8.1.</p>
<h2 id="support-des-enums">Support des enums</h2>
<p>PHP 8.1 a introduit les enums natifs. Symfony 5.4 les embrasse immédiatement :</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>Le type de formulaire <code>EnumType</code> rend les enums sous forme de listes déroulantes, sans transformateurs personnalisés. Le validateur comprend les backed enums. Le sérialiseur mappe les valeurs d&rsquo;enum sur leur type de backing et inversement. Trois composants mis à jour d&rsquo;un coup, ce qui a rendu la migration des bases de code des pseudo-constantes enum vers les vrais enums PHP 8.1 étonnamment fluide.</p>
<h2 id="cache-des-voters-de-sécurité">Cache des voters de sécurité</h2>
<p>La <code>CacheableVoterInterface</code> permet aux voters qui s&rsquo;abstiennent toujours sur un attribut donné de le signaler au système de sécurité, qui peut alors les ignorer lors des vérifications suivantes. Pour les applications avec de nombreux voters, le gain sur les vérifications de permissions s&rsquo;accumule vite. Petit changement, perceptible en pratique.</p>
<h2 id="messenger-continue-de-mûrir">Messenger continue de mûrir</h2>
<p>Le traitement par batch de Messenger (gérer plusieurs messages en une seule transaction au lieu d&rsquo;un par un) est maintenant stable. Rate limiting par transport. Les dead letter queues bénéficient de meilleurs outils. Après des années en mode « expérimental », Messenger en 5.4 est enfin la fondation async sur laquelle on peut s&rsquo;appuyer pour des charges sérieuses.</p>
<h2 id="la-console-a-eu-sa-touche-tab">La Console a eu sa touche Tab</h2>
<p>Symfony 5.4 embarque l&rsquo;autocomplétion shell pour toutes les commandes. Appuyez sur Tab et le shell suggère les noms de commandes, les valeurs d&rsquo;arguments et les valeurs d&rsquo;options. Pour les commandes intégrées, ça fonctionne sans configuration. Pour les commandes personnalisées, ajoutez une méthode <code>complete()</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\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>Pas d&rsquo;interface requise, juste la méthode et Symfony s&rsquo;en charge. La communauté a aussi passé en revue toutes les commandes intégrées (<code>debug:router</code>, <code>cache:pool:clear</code>, <code>secrets:remove</code>, <code>lint:twig</code>, et une dizaine d&rsquo;autres) pour ajouter les compléments avant la sortie.</p>
<h2 id="les-routes-peuvent-être-des-alias-maintenant">Les routes peuvent être des alias maintenant</h2>
<p>Le composant de routing supporte désormais les alias : une route peut pointer vers une autre. Le cas d&rsquo;usage évident, c&rsquo;est renommer une route sans casser tout ce qui génère encore des URLs avec l&rsquo;ancien nom.</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"># ancien nom conservé pendant la 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>Générer une URL avec <code>dashboard</code> fonctionne toujours, mais déclenche un avertissement de dépréciation. Des chemins de renommage propres pour les bundles qui doivent maintenir des noms de routes publics tout en avançant.</p>
<h2 id="les-exceptions-sont-mappées-aux-codes-http-dans-la-config">Les exceptions sont mappées aux codes HTTP dans la config</h2>
<p>Avant 5.4, mapper une classe d&rsquo;exception à un code HTTP signifiait implémenter <code>HttpExceptionInterface</code> ou écrire un listener. Maintenant c&rsquo;est juste une entrée 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-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>L&rsquo;exception n&rsquo;a rien à implémenter. Le framework lit la map, définit le code de statut, loggue au niveau configuré. Pratique pour les exceptions métier qui n&rsquo;ont aucune raison de connaître HTTP.</p>
<h2 id="deux-nouvelles-contraintes-de-validation">Deux nouvelles contraintes de validation</h2>
<p>5.4 ajoute <code>Cidr</code> et <code>CssColor</code> au composant Validator.</p>
<p><code>Cidr</code> valide la notation réseau (adresse IP plus masque de sous-réseau) avec un contrôle sur la version IP acceptée et les bornes de la valeur du masque :</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> valide qu&rsquo;une chaîne est une couleur CSS valide. Utile pour les éditeurs de thème, la config CMS, ou toute interface qui laisse les utilisateurs choisir des couleurs :</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">&#34;La couleur d&#39;accentuation doit être une valeur hex à 6 chiffres.&#34;</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="attributs-php-imbriqués-pour-les-contraintes-de-validation">Attributs PHP imbriqués pour les contraintes de validation</h2>
<p>Symfony 5.2 avait ajouté les contraintes de validation en attributs PHP, mais PHP 8.0 avait une limitation sur les attributs imbriqués. Les contraintes complexes comme <code>All</code>, <code>Collection</code>, ou <code>AtLeastOneOf</code> étaient impossibles à exprimer avec la syntaxe d&rsquo;attribut seule. PHP 8.1 a levé cette restriction, et 5.4 en tire le meilleur parti :</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;Doit être un email ou une URL valide.&#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>Pas de docblocks d&rsquo;annotations, pas de mapping XML. Des attributs PHP 8.1 purs, de bout en bout.</p>
<h2 id="injection-de-dépendances--trois-choses-à-savoir">Injection de dépendances : trois choses à savoir</h2>
<p>Les itérateurs taggués peuvent maintenant être injectés dans des service locators, qui n&rsquo;acceptaient auparavant que des listes de services explicites. L&rsquo;autowiring des types union fonctionne quand les deux côtés de l&rsquo;union résolvent vers le même service, ce qui est courant avec les interfaces du serializer :</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> remplace l&rsquo;introspection automatique que <code>ServiceSubscriberTrait</code> faisait implicitement. C&rsquo;est maintenant un attribut explicite sur les méthodes, ce qui rend la dépendance visible sans aucune magie :</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--attributs-état-des-workers-et-reset-de-services">Messenger : attributs, état des workers et reset de services</h2>
<p>Les handlers Messenger peuvent abandonner <code>MessageHandlerInterface</code> en faveur de <code>#[AsMessageHandler]</code>, qui permet aussi de lier un handler à un transport spécifique et de définir sa priorité, sans toucher au 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>L&rsquo;état des workers est maintenant inspectable via <code>WorkerMetadata</code> dans les event listeners, utile quand vous avez des workers sur plusieurs transports et avez besoin de savoir lequel a déclenché un événement donné.</p>
<p>Les workers longue durée accumulent de l&rsquo;état entre les messages : buffers de l&rsquo;entity manager, caches en mémoire, connexions ouvertes. La nouvelle option <code>reset_on_message</code> prend en charge la réinitialisation de tous les services réinitialisables entre les 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--collecter-les-erreurs-plutôt-que-lever">Serializer : collecter les erreurs plutôt que lever</h2>
<p>Désérialiser du JSON externe dans un DTO typé levait une exception dès la première discordance de type. L&rsquo;option <code>COLLECT_DENORMALIZATION_ERRORS</code> change ça : toutes les erreurs de type sont collectées dans une <code>PartialDenormalizationException</code>, pour que vous puissiez retourner un 400 propre avec la liste complète des problèmes par champ :</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>Le contexte par défaut du serializer peut aussi être défini globalement en YAML, pour ne plus passer les mêmes options à chaque appel.</p>
<h2 id="négociation-de-langue-intégrée">Négociation de langue intégrée</h2>
<p>Deux nouvelles options du framework gèrent l&rsquo;en-tête <code>Accept-Language</code> sans listeners personnalisé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-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>Avec ça en place, Symfony lit la langue préférée du navigateur, choisit la meilleure correspondance parmi <code>enabled_locales</code>, définit la locale de la requête, et ajoute un en-tête <code>Content-Language</code> à la réponse. L&rsquo;attribut de route <code>{_locale}</code> a toujours la priorité quand il est présent.</p>
<h2 id="traduction--extraction-pas-mise-à-jour">Traduction : extraction, pas mise à jour</h2>
<p>La commande <code>translation:update</code> est renommée en <code>translation:extract</code>. L&rsquo;ancien nom reste comme déprécié. La distinction compte : la commande n&rsquo;écrit jamais dans une base de données, elle extrait les chaînes traduisibles des fichiers source. Le nouveau nom dit enfin ce qu&rsquo;elle fait.</p>
<p><code>lint:xliff</code> gagne aussi une option <code>--format=github</code> qui sort les erreurs en annotations GitHub Actions, pour que les échecs de lint de traduction apparaissent en commentaires de revue de PR plutôt que de se noyer dans les logs.</p>
<h2 id="raccourcis-du-contrôleur-élagués">Raccourcis du contrôleur élagués</h2>
<p>Trois raccourcis d&rsquo;<code>AbstractController</code> sont dépréciés : <code>getDoctrine()</code>, <code>dispatchMessage()</code>, et la méthode générique <code>get()</code> pour récupérer des services arbitraires du container. La direction, c&rsquo;est l&rsquo;injection par constructeur explicite. Pour <code>getDoctrine()</code> en particulier :</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">// avant
</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">// après — injecter directement
</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> est aussi déprécié. Il cherchait dans les attributs de route, la query string et le corps POST dans un ordre non documenté, ce qui était une excellente façon d&rsquo;obtenir des résultats surprenants. Utilisez <code>$request-&gt;query-&gt;get()</code>, <code>$request-&gt;request-&gt;get()</code>, ou <code>$request-&gt;attributes-&gt;get()</code> et soyez explicite sur la provenance de la valeur.</p>
<h2 id="la-classe-utilitaire-path">La classe utilitaire Path</h2>
<p>Le composant Filesystem reçoit une classe <code>Path</code> portée depuis <code>webmozart/path-util</code>. Elle gère les cas tordus que <code>dirname()</code> et <code>realpath()</code> ratent :</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() retourne &#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>Utile dès que votre code manipule des chemins qui traversent les frontières des OS ou qui contiennent des segments relatifs.</p>
<h2 id="les-petites-choses-qui-saccumulent">Les petites choses qui s&rsquo;accumulent</h2>
<p><code>debug:dotenv</code> montre quels fichiers <code>.env</code> ont été chargés et quelle valeur chaque variable résout. La première chose qu&rsquo;on cherche quand un comportement spécifique à un environnement déraille.</p>
<p>Le composant String ajoute <code>trimPrefix()</code> et <code>trimSuffix()</code> pour retirer des préfixes ou suffixes connus sans écrire un calcul de substr :</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 reçoit <code>innerText()</code>, qui retourne uniquement le texte direct d&rsquo;un nœud, à l&rsquo;exclusion des éléments enfants. <code>text()</code> retourne tout y compris le texte imbriqué ; <code>innerText()</code> retourne uniquement le contenu propre du nœud. Petite différence, mais ça compte quand on fait du scraping.</p>
<p>Le composant RateLimiter étend son support des intervalles avec <code>perMonth()</code> et <code>perYear()</code>, pour les applications qui ont besoin de limiter des événements sur des fenêtres plus longues : envois de newsletters, remises à zéro de quotas API, limites de forfaits annuels.</p>
<p>Le composant Finder respecte maintenant les fichiers <code>.gitignore</code> dans tous les sous-répertoires quand vous appelez <code>ignoreVCSIgnored(true)</code>, pas seulement à la racine. Les règles des répertoires enfants supplantent celles des parents, exactement comme git lui-même.</p>
<h2 id="la-fenêtre-lts">La fenêtre LTS</h2>
<p>5.4 reçoit des corrections de bugs jusqu&rsquo;en novembre 2024 et des correctifs de sécurité jusqu&rsquo;en novembre 2025. La migration de 5.4 vers 6.4 (la prochaine LTS) est intentionnellement fluide : corrigez les avertissements de dépréciation de 5.4, et le saut vers 6.x devient mécanique.</p>
<p>La couche de dépréciation en 5.4 pointe vers tout ce que 6.0 supprime : les derniers morceaux de l&rsquo;ancien système de sécurité, <code>ContainerAwareTrait</code>, et quelques patterns legacy de formulaires et de serializer.</p>
]]></content:encoded></item><item><title>Symfony 4.4 LTS : HttpClient, Mailer, Messenger et les fonctionnalités qui ont tenu bon</title><link>https://guillaumedelre.github.io/fr/2020/01/04/symfony-4.4-lts-httpclient-mailer-messenger-et-les-fonctionnalit%C3%A9s-qui-ont-tenu-bon/</link><pubDate>Sat, 04 Jan 2020 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2020/01/04/symfony-4.4-lts-httpclient-mailer-messenger-et-les-fonctionnalit%C3%A9s-qui-ont-tenu-bon/</guid><description>Part 4 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 4.4 LTS embarque un HttpClient mature et un Messenger prêt pour la production — les couches HTTP et asynchrone qui manquaient à Symfony.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 4.4 et 5.0 sont tous les deux sortis le 21 novembre 2019. La 4.4 est la LTS : même ensemble de fonctionnalités que la 5.0, couche de dépréciation intégrée, et une longue fenêtre de support pour les équipes qui ne peuvent pas suivre chaque release.</p>
<p>La fonctionnalité qui mérite d&rsquo;être mise en avant est arrivée en 4.2 et a mûri tout au long des 4.3 et 4.4 : <code>HttpClient</code>.</p>
<h2 id="httpclient">HttpClient</h2>
<p>Les options HTTP natives de PHP (<code>file_get_contents</code> avec des contextes de flux, cURL, Guzzle) ont chacune leur propre modèle, leurs propres bizarreries et leur propre coût d&rsquo;abstraction. Symfony 4.2 a introduit <code>HttpClient</code>, un client HTTP first-party avec une seule API pour plusieurs transports.</p>
<p>L&rsquo;interface est claire :</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>L&rsquo;implémentation est asynchrone par défaut. Les réponses sont paresseuses : la requête réseau n&rsquo;a pas lieu tant qu&rsquo;on ne lit pas réellement la réponse. Plusieurs requêtes peuvent être initiées et résolues au fil de l&rsquo;arrivée des données, sans threads ni callbacks.</p>
<p>Le transport mock intégré (<code>MockHttpClient</code>) rend les tests d&rsquo;appels HTTP indolores sans avoir à démarrer des serveurs ou patcher des fonctions globales.</p>
<h2 id="mailer">Mailer</h2>
<p>Également stabilisé en 4.4 : le composant <code>Mailer</code>, qui remplace <code>SwiftMailerBundle</code> comme solution d&rsquo;email recommandée. Le transport se configure via DSN :</p>
<pre tabindex="0"><code>MAILER_DSN=smtp://user:pass@smtp.example.com:587
</code></pre><p>L&rsquo;approche DSN signifie que changer de fournisseur (Mailgun, Postmark, SES, SMTP local) est un changement de config, pas un changement de code. Les tests d&rsquo;emails utilisent un spooler par défaut dans les environnements hors production.</p>
<h2 id="messenger-se-mâture">Messenger se mâture</h2>
<p>Le composant Messenger a atterri en 3.4 à titre expérimental. En 4.4, il est stable et éprouvé en production : gestion de messages asynchrones avec logique de retry, transport d&rsquo;échec, et adaptateurs pour AMQP, Redis, Doctrine, et les transports in-process.</p>
<p>Le pattern qu&rsquo;il permet (traiter une requête de façon synchrone, dispatcher du travail de façon asynchrone, réessayer en cas d&rsquo;échec) remplace toute une classe de setups Gearman/RabbitMQ qui nécessitaient des bibliothèques tierces et une configuration conséquente.</p>
<h2 id="la-fenêtre-lts">La fenêtre LTS</h2>
<p>La 4.4 est supportée pour les bugs jusqu&rsquo;en novembre 2022 et pour les correctifs de sécurité jusqu&rsquo;en novembre 2023. Si vous êtes sur la 4.x et recherchez la stabilité, c&rsquo;est un endroit confortable où rester. Les avertissements de dépréciation qu&rsquo;elle introduit pointent directement vers ce que la 5.0 exigera.</p>
<h2 id="le-composant-messenger-de-lexpérimental-à-la-production">Le composant Messenger, de l&rsquo;expérimental à la production</h2>
<p>Messenger est arrivé en 4.1 comme une expérience. Le concept était simple : dispatcher un objet message vers un bus, le traiter immédiatement ou le router vers un transport pour un traitement asynchrone. En 4.3 et 4.4, l&rsquo;expérience était devenue de l&rsquo;infrastructure.</p>
<p>La release 4.3 a ajouté un transport d&rsquo;échec dédié. Quand un message échoue après toutes les tentatives de retry, il va quelque part de récupérable plutôt que de simplement disparaître :</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>Les messages qui atterrissent dans <code>failed</code> peuvent être inspectés et retentés manuellement. Avant ça, les messages en échec étaient une entrée de log et un mal de tête. Après, c&rsquo;est une file qu&rsquo;on peut vraiment travailler.</p>
<h2 id="le-dispatching-dévénements-avec-les-objets-en-première-place">Le dispatching d&rsquo;événements, avec les objets en première place</h2>
<p>Depuis le début, le système d&rsquo;événements de Symfony utilisait des noms d&rsquo;événements en chaîne comme identifiant principal. On définissait <code>OrderEvents::NEW_ORDER = 'order.new_order'</code>, on écoutait cette chaîne, et on passait l&rsquo;objet événement comme paramètre secondaire.</p>
<p>La 4.3 a inversé ça. L&rsquo;objet événement passe en premier, et le nom de l&rsquo;événement devient optionnel :</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">// Avant
</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>Omettez le nom et Symfony utilise le nom de classe comme identifiant. Les listeners et subscribers peuvent maintenant référencer la classe directement :</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>Les événements HttpKernel ont été renommés en conséquence : <code>GetResponseEvent</code> est devenu <code>RequestEvent</code>, <code>FilterResponseEvent</code> est devenu <code>ResponseEvent</code>. Les anciens noms sont restés comme alias pendant toute la 4.x.</p>
<h2 id="vardumper-obtient-un-serveur">VarDumper obtient un serveur</h2>
<p>Un <code>dump()</code> dans un contrôleur qui retourne du JSON, et votre sortie de debug se retrouve injectée directement dans le corps de la réponse. Pour le développement d&rsquo;API, c&rsquo;est suffisamment agaçant pour que les gens désactivent le dumping complètement.</p>
<p>La 4.1 a ajouté un serveur VarDumper qui capture les dumps séparément :</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>Configurez la destination du dump dans <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>Maintenant, <code>dump()</code> dans votre contrôleur API envoie les données vers la console du serveur au lieu de polluer la réponse. Le serveur affiche le dump avec le fichier source, la requête HTTP qui l&rsquo;a déclenché, et le timestamp.</p>
<h2 id="varexporter-pour-quand-var_export-vous-déçoit">VarExporter, pour quand <code>var_export()</code> vous déçoit</h2>
<p><code>var_export()</code> a deux problèmes : il ignore la sémantique de sérialisation et sa sortie n&rsquo;est pas conforme à PSR-2. Le composant VarExporter de la 4.2 corrige les deux.</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">// Retourne :
</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>Plus important encore, il gère correctement les objets implémentant <code>Serializable</code>, <code>__sleep</code>, et <code>__wakeup</code>. Là où <code>var_export()</code> abandonne silencieusement les méthodes de sérialisation et exporte les propriétés brutes, VarExporter produit du code qui appelle les mêmes hooks qu&rsquo;<code>unserialize()</code> utiliserait. Le cas d&rsquo;usage pratique est le préchauffage du cache : générer des fichiers PHP que OPcache peut charger sans ré-exécuter des calculs coûteux.</p>
<h2 id="des-mots-de-passe-vérifiés-contre-les-bases-de-données-de-violations">Des mots de passe vérifiés contre les bases de données de violations</h2>
<p>La contrainte <code>NotCompromisedPassword</code> est arrivée en 4.3. Elle vérifie les mots de passe soumis contre la base de données de violations d&rsquo;haveibeenpwned.com sans envoyer le vrai mot de passe nulle part.</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>L&rsquo;implémentation utilise la k-anonymité : on hash le mot de passe en SHA-1, on envoie seulement les cinq premiers caractères à l&rsquo;API, on récupère tous les hashs correspondants, on vérifie localement. Le mot de passe ne quitte jamais votre serveur. Pour les formulaires d&rsquo;inscription, ajouter cette contrainte c&rsquo;est une ligne et un signal de sécurité vraiment utile.</p>
<h2 id="workflow-obtient-du-contexte">Workflow obtient du contexte</h2>
<p>Le composant Workflow existait avant la 4.x, mais la 4.3 a ajouté la propagation de contexte : la possibilité de passer des données arbitraires à travers une transition et d&rsquo;y accéder dans les 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>Le contexte arrive dans <code>TransitionEvent</code> et est stocké aux côtés du marquage. Pour les pistes d&rsquo;audit, c&rsquo;est la différence entre savoir qu&rsquo;une transition s&rsquo;est produite et savoir qui l&rsquo;a déclenchée et pourquoi. On peut aussi injecter du contexte depuis un subscriber sans toucher à chaque appel <code>apply()</code>, ce qui est pratique pour les préoccupations transversales comme les timestamps ou l&rsquo;utilisateur courant.</p>
<h2 id="lautowiring-est-devenu-plus-intelligent">L&rsquo;autowiring est devenu plus intelligent</h2>
<p>La 4.2 a ajouté la liaison par type et par nom simultanément. Avant, on pouvait lier par type (<code>LoggerInterface</code>) ou par nom (<code>$logger</code>), mais pas les deux en même temps. Ça posait problème quand un service a besoin de deux implémentations différentes de la même 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>La correspondance exige que le type et le nom de l&rsquo;argument s&rsquo;alignent, donc pas de risque d&rsquo;injecter accidentellement le mauvais logger.</p>
<h2 id="errorhandler-remplace-le-composant-debug">ErrorHandler remplace le composant Debug</h2>
<p>Le composant <code>Debug</code>, inchangé depuis 2013, avait une dépendance maladroite sur TwigBundle même pour les apps API-only. Toute exception non attrapée dans une API JSON rendait une page d&rsquo;erreur HTML à moins d&rsquo;écrire des listeners d&rsquo;exception personnalisés.</p>
<p>La 4.4 extrait ça dans un composant <code>ErrorHandler</code> dédié. Pour les requêtes non-HTML, les réponses d&rsquo;erreur suivent désormais RFC 7807 nativement :</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>Pas besoin de Twig. Le format suit l&rsquo;en-tête <code>Accept</code> : JSON pour les requêtes JSON, XML pour les requêtes XML. Pour personnaliser davantage, on fournit un normalizer via le composant Serializer plutôt qu&rsquo;un template Twig.</p>
<h2 id="le-préchargement-php-74-câblé-automatiquement">Le préchargement PHP 7.4, câblé automatiquement</h2>
<p>PHP 7.4 a introduit le préchargement OPcache : charger des fichiers en mémoire partagée avant l&rsquo;arrivée de toute requête, pour qu&rsquo;ils soient disponibles sous forme d&rsquo;opcodes compilés dès la toute première requête. Le gain pratique est de 30 à 50 % de temps de réponse en moins sans changer une ligne de code.</p>
<p>Le bémol c&rsquo;est la configuration : il faut spécifier exactement quels fichiers précharger dans <code>php.ini</code>. Symfony 4.4 génère ce fichier automatiquement dans le répertoire de cache :</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>Lancez <code>cache:warmup</code> en production et pointez OPcache vers le fichier généré. Symfony précharge le container, les routes compilées et les templates Twig : les fichiers lus à chaque requête et qui ne changent jamais entre les déploiements.</p>
<h2 id="console--codes-de-retour-et-no_color">Console : codes de retour et NO_COLOR</h2>
<p>Deux petites choses en 4.4 qui auraient honnêtement dû exister plus tôt. Les commandes qui ne retournent pas d&rsquo;entier depuis <code>execute()</code> déclenchent maintenant un avertissement de dépréciation. En 5.0, le type de retour devient obligatoire. Retourner <code>0</code> pour le succès, non-zéro pour l&rsquo;échec : comportement Unix standard, et ça rend l&rsquo;intégration avec les superviseurs de processus et les pipelines CI sans ambiguïté.</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>Le deuxième point : le support de la variable d&rsquo;environnement <code>NO_COLOR</code>, suivant la convention de no-color.org. Activez-la et toutes les commandes console de Symfony abandonnent les codes d&rsquo;échappement ANSI quelle que soit la capacité déclarée par le terminal. Utile pour les environnements CI qui capturent la sortie en texte et qui s&rsquo;étranglent sur les codes couleur intégrés dans les logs.</p>
]]></content:encoded></item></channel></rss>