<?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>Sorties Symfony on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/series/symfony-releases/</link><description>Recent content in Sorties Symfony on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Mon, 12 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/series/symfony-releases/index.xml" rel="self" type="application/rss+xml"/><item><title>Symfony 8.0 : PHP 8.4 minimum, objets paresseux natifs et FormFlow</title><link>https://guillaumedelre.github.io/fr/2026/01/12/symfony-8.0-php-8.4-minimum-objets-paresseux-natifs-et-formflow/</link><pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/01/12/symfony-8.0-php-8.4-minimum-objets-paresseux-natifs-et-formflow/</guid><description>Part 11 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 8.0 exige PHP 8.4, remplace son générateur de code proxy par des objets paresseux natifs et introduit FormFlow.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 8.0 est sorti le 27 novembre 2025, le même jour que 7.4. Il exige PHP 8.4 et abandonne tout ce qui était déprécié dans 7.4. Les deux changements les plus intéressants sont ce qu&rsquo;il arrête de faire et ce qu&rsquo;il commence à faire avec PHP 8.4.</p>
<h2 id="les-objets-paresseux-natifs">Les objets paresseux natifs</h2>
<p>Le système de proxy de Symfony, utilisé pour l&rsquo;initialisation paresseuse des services et les proxies d&rsquo;entités de Doctrine, a historiquement reposé sur la génération de code. Les classes proxy étaient générées au cache warmup, stockées sous forme de fichiers, et chargées à la demande. Ça fonctionnait, mais ça ajoutait une vraie complexité : des fichiers générés à gérer, un cache à invalider, du code qui ne ressemblait en rien à la classe qu&rsquo;il proxyifiait.</p>
<p>PHP 8.4 a ajouté des objets paresseux natifs. Symfony 8.0 les utilise. Le <code>LazyGhostTrait</code> et le <code>LazyProxyTrait</code> qui alimentaient l&rsquo;ancien système sont supprimés. La création de proxy est maintenant une opération à l&rsquo;exécution soutenue par le moteur lui-même, pas une étape de génération de code.</p>
<p>Pour les développeurs d&rsquo;applications, le changement est essentiellement invisible : les services paresseux fonctionnent toujours. Pour les auteurs de frameworks et bibliothèques, une surface significative de complexité vient de disparaître.</p>
<h2 id="formflow">FormFlow</h2>
<p>Les formulaires multi-étapes ont toujours été un exercice DIY dans Symfony. Gestion de session, suivi des étapes, validation partielle, navigation entre les étapes : chaque projet roulait sa propre solution ou importait un bundle tiers.</p>
<p>8.0 introduit FormFlow : un mécanisme intégré pour les wizards de formulaires multi-étapes. Les étapes sont définies comme une séquence de types de formulaires, la validation partielle est scopée à l&rsquo;étape courante, et la gestion de session est gérée 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:#75715e">#[AsFormFlow]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CheckoutFlow</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractFormFlow</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">defineSteps</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Steps</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Steps</span><span style="color:#f92672">::</span><span style="color:#a6e22e">create</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;shipping&#39;</span>, <span style="color:#a6e22e">ShippingType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;payment&#39;</span>, <span style="color:#a6e22e">PaymentType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#a6e22e">ReviewType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="la-config-xml-et-php-fluent-supprimées">La config XML et PHP fluent supprimées</h2>
<p>La dépréciation de 7.4 du format de configuration PHP fluent devient une suppression définitive dans 8.0. La configuration XML sort aussi comme format de première classe. Les formats supportés pour la configuration applicative sont maintenant YAML et tableaux PHP. L&rsquo;empreinte rétrécit, mais ce qui reste est genuinement meilleur.</p>
<h2 id="ce-qui-est-supprimé-dautre">Ce qui est supprimé d&rsquo;autre</h2>
<ul>
<li>Le support PHP 8.2 et 8.3 (8.4 minimum)</li>
<li><code>ContainerAwareInterface</code> et <code>ContainerAwareTrait</code></li>
<li>L&rsquo;usage interne de <code>LazyGhostTrait</code> et <code>LazyProxyTrait</code> par Symfony</li>
<li>La surcharge de méthode HTTP pour GET et HEAD (seul POST a du sens sémantiquement)</li>
</ul>
<p>Symfony 8.0 est une rupture propre, et ce genre de rupture ne devient possible que quand le plancher PHP s&rsquo;élève. Les objets paresseux de PHP 8.4 sont l&rsquo;exemple le plus clair : la fonctionnalité existe maintenant dans le langage, donc le framework peut simplement arrêter de l&rsquo;implémenter.</p>
<h2 id="console-devient-plus-ergonomique-pour-les-commandes-invocables">Console devient plus ergonomique pour les commandes invocables</h2>
<p>Les commandes invocables reçoivent une mise à niveau significative. L&rsquo;attribut <code>#[Input]</code> transforme un DTO en bag d&rsquo;arguments/options de la commande. Fini d&rsquo;appeler <code>$input-&gt;getArgument()</code> dans le handler :</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">#[AsCommand(name: &#39;app:send-report&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SendReportCommand</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__invoke</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[Input] SendReportInput $input,
</span></span></span><span style="display:flex;"><span>    )<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// $input-&gt;email, $input-&gt;dryRun, etc.
</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></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>BackedEnum</code> est supporté dans les commandes invocables, donc une option déclarée comme enum <code>Status</code> est validée et castée automatiquement. Les commandes interactives reçoivent les attributs <code>#[Interact]</code> et <code>#[Ask]</code> pour déclarer les prompts de questions en ligne. <code>CommandTester</code> fonctionne avec les commandes invocables sans câblage supplémentaire.</p>
<h2 id="le-routing-trouve-ses-propres-contrôleurs">Le Routing trouve ses propres contrôleurs</h2>
<p>Les routes définies via <code>#[Route]</code> sur les classes de contrôleurs sont auto-enregistrées sans avoir besoin d&rsquo;une entrée <code>resource:</code> explicite dans <code>config/routes.yaml</code>. Le tag <code>routing.controller</code> est appliqué automatiquement. On contrôle toujours quels répertoires sont scannés, mais la config YAML rétrécit jusqu&rsquo;à un pointeur vers un répertoire plutôt qu&rsquo;une liste de fichiers manuelle.</p>
<p><code>#[Route]</code> reçoit aussi un paramètre <code>_query</code> pour définir des paramètres de query à la génération, et plusieurs environnements dans l&rsquo;option <code>env</code>.</p>
<h2 id="sécurité--csrf-et-oidc-reçoivent-de-meilleurs-outils">Sécurité : CSRF et OIDC reçoivent de meilleurs outils</h2>
<p><code>#[IsCsrfTokenValid]</code> reçoit un argument <code>$tokenSource</code> pour spécifier d&rsquo;où vient le token (header, cookie, champ de formulaire) plutôt que de s&rsquo;appuyer sur une convention fixe. <code>SameOriginCsrfTokenManager</code> ajoute la validation du header <code>Sec-Fetch-Site</code>, un mécanisme de protection CSRF natif au navigateur qui n&rsquo;a pas besoin d&rsquo;injection de token du tout.</p>
<p>La commande <code>security:oidc-token:generate</code> crée des tokens pour tester les endpoints protégés OIDC en local. Plusieurs endpoints de découverte OIDC sont maintenant supportés, utile dans les setups multi-tenant où chaque tenant a son propre identity provider.</p>
<p>Deux nouvelles fonctions Twig : <code>access_decision()</code> et <code>access_decision_for_user()</code> exposent le résultat du voter d&rsquo;autorisation dans les templates sans passer par la façade de sécurité. <code>#[IsGranted]</code> peut être sous-classé pour les patterns d&rsquo;autorisation répétés qui méritent leur propre attribut nommé.</p>
<h2 id="objectmapper-et-jsonstreamer-sortent-dexpérimental">ObjectMapper et JsonStreamer sortent d&rsquo;expérimental</h2>
<p>Les deux composants introduits dans 7.x sont stables dans 8.0. <code>ObjectMapper</code> mappe entre objets sans transformateurs écrits à la main, via une configuration basée sur des attributs. <code>JsonStreamer</code> lit et écrit de grands JSON sans charger le document entier en mémoire, et il supporte maintenant les propriétés synthétiques : des champs virtuels calculés à la sérialisation.</p>
<p><code>JsonStreamer</code> abandonne aussi sa dépendance sur <code>nikic/php-parser</code>. La génération de code pour le reader/writer utilise maintenant un mécanisme interne plus simple, supprimant une lourde dépendance de dev.</p>
<h2 id="uid-par-défaut-vers-uuidv7">Uid par défaut vers UUIDv7</h2>
<p><code>UuidFactory</code> génère maintenant UUIDv7 par défaut au lieu d&rsquo;UUIDv4. La différence : v7 est ordonné dans le temps, donc les UUIDs générés se trient chronologiquement. Ça compte beaucoup pour la performance des index de base de données. <code>MockUuidFactory</code> fournit une génération déterministe d&rsquo;UUID dans les tests.</p>
<h2 id="yaml-lève-une-erreur-sur-les-clés-dupliquées">Yaml lève une erreur sur les clés dupliquées</h2>
<p>Auparavant, un fichier YAML avec deux clés identiques gardait silencieusement la dernière. 8.0 lève une erreur de parsing. Ça attrape de vrais bugs : les clés dupliquées dans <code>services.yaml</code> ou <code>config/packages/*.yaml</code> sont presque toujours des erreurs de copier-coller et on veut définitivement être informé.</p>
<h2 id="validator--contrainte-video-et-protocoles-wildcard">Validator : contrainte Video et protocoles wildcard</h2>
<p>Une contrainte <code>Video</code> rejoint la contrainte <code>Image</code> pour valider les fichiers vidéo uploadés (type MIME, durée, codec). La contrainte <code>Url</code> accepte <code>protocols: ['*']</code> pour autoriser n&rsquo;importe quel schéma conforme à la RFC 3986, utile pour stocker des URLs arbitraires qui incluent <code>git+ssh://</code>, <code>file://</code>, ou des schémas d&rsquo;application personnalisés.</p>
<h2 id="messenger--retry-natif-sqs-et-nouveaux-événements">Messenger : retry natif SQS et nouveaux événements</h2>
<p>Le transport SQS peut maintenant utiliser sa propre configuration native de retry et de dead-letter queue au lieu du middleware de retry de Symfony. Pour les queues à haut volume sur AWS, ça supprime un aller-retour par PHP pour les échecs transitoires. Un <code>MessageSentToTransportsEvent</code> se déclenche après qu&rsquo;un message est dispatché, portant des informations sur quels transports l&rsquo;ont réellement reçu.</p>
<p><code>messenger:consume</code> reçoit <code>--exclude-receivers</code> pour se combiner avec <code>--all</code>.</p>
<h2 id="mailer--transport-microsoft-graph">Mailer : transport Microsoft Graph</h2>
<p>Un nouveau transport envoie des emails via l&rsquo;API Microsoft Graph, ce que Microsoft recommande pour les applications sur Azure Active Directory ces jours-ci. Les autres options (relai SMTP, Exchange EWS) fonctionnent encore, mais Graph est le bon choix pour les nouveaux déploiements Azure.</p>
<h2 id="workflow--transitions-pondérées">Workflow : transitions pondérées</h2>
<p>Les transitions peuvent maintenant déclarer des poids. Quand plusieurs transitions sont activées depuis la même place, celle avec le poids le plus élevé gagne. Ça permet d&rsquo;exprimer la priorité directement dans la définition du workflow sans ajouter un guard qui lit un compteur externe.</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">new</span> <span style="color:#a6e22e">Definition</span>(<span style="color:#a6e22e">states</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;draft&#39;</span>, <span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#e6db74">&#39;published&#39;</span>]))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addTransition</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Transition</span>(<span style="color:#e6db74">&#39;publish&#39;</span>, <span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#e6db74">&#39;published&#39;</span>, <span style="color:#a6e22e">weight</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">10</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addTransition</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Transition</span>(<span style="color:#e6db74">&#39;reject&#39;</span>, <span style="color:#e6db74">&#39;review&#39;</span>, <span style="color:#e6db74">&#39;draft&#39;</span>, <span style="color:#a6e22e">weight</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>));
</span></span></code></pre></div><h2 id="lock--lockkeynormalizer">Lock : LockKeyNormalizer</h2>
<p><code>LockKeyNormalizer</code> normalise une clé de lock vers une string cohérente avant le hachage. Utile quand la clé est dérivée d&rsquo;entrées utilisateur ou de données externes qui peuvent varier en espaces blancs ou casse : le normalizer s&rsquo;assure que la même clé logique correspond toujours au même lock.</p>
<h2 id="httpfoundation--méthode-query-et-parsing-de-corps-plus-propre">HttpFoundation : méthode QUERY et parsing de corps plus propre</h2>
<p>La méthode IETF <code>QUERY</code> (une méthode sûre et idempotente avec un corps, contrairement à <code>GET</code>) est maintenant supportée partout dans la stack : <code>Request</code>, cache HTTP, WebProfiler et HttpClient. Si on construit des APIs de recherche qui nécessitent un corps de requête structuré et veulent aussi du cache, <code>QUERY</code> est le bon choix sémantique.</p>
<p><code>Request::createFromGlobals()</code> parse maintenant automatiquement le corps des requêtes <code>PUT</code>, <code>DELETE</code>, <code>PATCH</code> et <code>QUERY</code>.</p>
<h2 id="config--schéma-json-pour-la-validation-yaml">Config : schéma JSON pour la validation YAML</h2>
<p>Symfony 8.0 auto-génère un fichier JSON Schema pour chaque section de configuration. Les IDEs qui supportent JSON Schema pour les fichiers YAML (VS Code, PhpStorm) peuvent maintenant valider <code>config/packages/*.yaml</code> contre ces schémas et fournir de l&rsquo;autocomplétion sans plugin. Le schéma est généré pendant le cache warmup et placé dans <code>config/reference.php</code>.</p>
<h2 id="runtime--auto-détection-frankenphp">Runtime : auto-détection FrankenPHP</h2>
<p>Le composant Runtime détecte FrankenPHP automatiquement et active le mode worker sans package supplémentaire ni variable d&rsquo;environnement. Si <code>$_SERVER['APP_RUNTIME']</code> est défini, cette classe de runtime a la priorité. On peut aussi choisir le renderer d&rsquo;erreurs basé sur <code>APP_RUNTIME_MODE</code>, ce qui est utile quand on fait tourner la même codebase dans des contextes HTTP et CLI avec des besoins de présentation d&rsquo;erreurs différents.</p>
]]></content:encoded></item><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>Symfony 7.0 : PHP 8.2 minimum et les annotations enfin disparues</title><link>https://guillaumedelre.github.io/fr/2024/01/12/symfony-7.0-php-8.2-minimum-et-les-annotations-enfin-disparues/</link><pubDate>Fri, 12 Jan 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/01/12/symfony-7.0-php-8.2-minimum-et-les-annotations-enfin-disparues/</guid><description>Part 9 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 7.0 exige PHP 8.2, abandonne entièrement les annotations Doctrine et livre un composant Workflow reconstruit.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 7.0 est sorti le 29 novembre 2023, le même jour que 6.4. Le pattern tient : la version X.0 coupe le code déprécié et élève le plancher PHP. 7.0 exige PHP 8.2 et supprime tout ce que 6.4 avait marqué comme déprécié.</p>
<p>La suppression la plus visible : les annotations Doctrine. <code>@Route</code>, <code>@ORM\Column</code>, <code>@Assert</code> — disparus. Les attributs PHP natifs sont l&rsquo;approche recommandée depuis Symfony 5.2. 7.0 rend juste ça officiel.</p>
<h2 id="les-attributs-partout">Les attributs partout</h2>
<p>La migration des annotations vers les attributs est principalement mécanique : la syntaxe passe de <code>@</code> à <code>#[]</code>, et les références de classes passent des classes d&rsquo;annotation Doctrine aux classes d&rsquo;attribut PHP :</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><span style="color:#e6db74">/** @Route(&#39;/users&#39;, methods={&#34;GET&#34;}) */</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// après
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[Route(&#39;/users&#39;, methods: [&#39;GET&#39;])]
</span></span></span></code></pre></div><p>Le vrai gain n&rsquo;est pas juste la syntaxe : les attributs sont validés par le moteur PHP, pas un parseur de docblock. Les IDEs peuvent les résoudre sans plugins personnalisés. Les outils d&rsquo;analyse statique les comprennent nativement. Fini les &ldquo;ça échoue silencieusement à l&rsquo;exécution à cause d&rsquo;une faute de frappe dans un commentaire.&rdquo;</p>
<h2 id="workflow-avec-attributs-php">Workflow avec attributs PHP</h2>
<p>Les listeners et guards d&rsquo;événements Workflow peuvent maintenant être enregistrés via des attributs :</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">#[AsGuard(workflow: &#39;order&#39;, transition: &#39;ship&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">canShip</span>(<span style="color:#a6e22e">Event</span> $event)<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> (<span style="color:#f92672">!</span>$event<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getSubject</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">isPaymentConfirmed</span>()) {
</span></span><span style="display:flex;"><span>        $event<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setBlocked</span>(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le profiler de workflow, un panneau dédié montrant le marquage courant et les transitions disponibles, est un outil de debug vraiment utile quand on travaille avec des machines à états complexes.</p>
<h2 id="datepoint-dans-le-composant-clock">DatePoint dans le composant Clock</h2>
<p><code>DatePoint</code>, le <code>DateTime</code> immutable avec gestion stricte des erreurs introduit dans 6.4, est maintenant la façon recommandée de travailler avec les dates. Combiné avec les propriétés readonly de PHP 8.2, les objets-valeur de dates dans le code de domaine deviennent presque trivialement propres :</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">readonly</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Order</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">DatePoint</span> $createdAt,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">DatePoint</span> $shippedAt <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="ce-que-70-supprime">Ce que 7.0 supprime</h2>
<p>La liste complète des suppressions : le support des annotations Doctrine, le bridge du composant <code>Templating</code>, le bridge <code>ProxyManager</code>, le bridge <code>Monolog</code> pour les versions inférieures à 3.0, et le transport Sendinblue (remplacé par Brevo). Le support PHP 8.0 et 8.1 se termine aussi. 8.2 est le plancher maintenant.</p>
<p>Monter de 6.4 avec toutes les notices de dépréciation corrigées, et 7.0 est fluide. Sauter cette étape et on s&rsquo;expose à une mauvaise surprise.</p>
<h2 id="scheduler-et-assetmapper-diplômés">Scheduler et AssetMapper diplômés</h2>
<p>Deux composants qui sont sortis en expérimental dans 6.3 sont maintenant stables : Scheduler et AssetMapper. Stable signifie des APIs verrouillées, plus de mises en garde <code>@experimental</code>, et ils apparaissent correctement dans le guide de mise à jour. On peut vraiment compter sur eux maintenant.</p>
<p>Scheduler reçoit <code>#[AsCronTask]</code> et <code>#[AsPeriodicTask]</code> pour l&rsquo;enregistrement de tâches par attribut, la modification de planning à l&rsquo;exécution avec recalcul du heap, <code>FailureEvent</code>, et une option <code>--date</code> sur <code>schedule:debug</code>. AssetMapper ajoute le support des fichiers CSS dans l&rsquo;importmap, une commande <code>outdated</code>, une commande <code>audit</code>, et le préchargement automatique via WebLink.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[AsCronTask(&#39;0 2 * * *&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">NightlyReportMessage</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[AsPeriodicTask(frequency: &#39;1 hour&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HourlyCleanupMessage</span> {}
</span></span></code></pre></div><h2 id="le-câblage-de-services-reçoit-deux-nouveaux-attributs">Le câblage de services reçoit deux nouveaux attributs</h2>
<p><code>#[AutowireLocator]</code> et <code>#[AutowireIterator]</code> ont atterri dans 6.4 et sont stables dans 7.0. Ils remplacent la configuration verbose XML/YAML des service locators taggués par quelque chose qu&rsquo;on peut juste mettre directement en PHP :</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">HandlerRegistry</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:#75715e">#[AutowireLocator(&#39;app.handler&#39;, indexAttribute: &#39;key&#39;)]
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">ContainerInterface</span> $handlers,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>#[Target]</code> est aussi plus intelligent : quand un service a un alias d&rsquo;autowiring nommé comme <code>invoice.lock.factory</code>, on peut maintenant écrire <code>#[Target('invoice')]</code> au lieu du nom complet de l&rsquo;alias. Moins de bruit quand le type dit déjà ce qu&rsquo;on veut.</p>
<h2 id="messenger-reçoit-une-gestion-plus-précise-des-échecs">Messenger reçoit une gestion plus précise des échecs</h2>
<p><code>RejectRedeliveredMessageException</code> dit au worker de ne pas retenter un message, ce qui est pratique quand un message arrive deux fois à cause d&rsquo;un timeout d&rsquo;ack du transport et qu&rsquo;on a besoin d&rsquo;une sémantique exactly-once. <code>messenger:failed:remove --all</code> vide tout le transport d&rsquo;échec en un coup, pas de boucle nécessaire. Les retries échoués peuvent aussi aller directement au transport d&rsquo;échec, en contournant entièrement la queue de retry.</p>
<p>Plusieurs hôtes Redis Sentinel sont maintenant supportés dans le DSN :</p>
<pre tabindex="0"><code>redis-sentinel://host1:26379,host2:26379,host3:26379/mymaster
</code></pre><h2 id="console-reçoit-les-noms-de-signaux-et-le-profilage-de-commandes">Console reçoit les noms de signaux et le profilage de commandes</h2>
<p><code>SignalMap</code> mappe les entiers de signaux à leurs noms POSIX. Quand un worker attrape <code>SIGTERM</code>, le log dit maintenant <code>SIGTERM</code> au lieu de <code>15</code>. Petite chose, vraie amélioration. <code>ConsoleTerminateEvent</code> est dispatché même quand le processus se termine via signal, ce qui n&rsquo;était pas le cas avant 7.0.</p>
<p>Le profilage de commandes arrive aussi : passer <code>--profile</code> à <code>bin/console</code> et les données collectées vont directement dans le profiler Symfony, navigable depuis l&rsquo;UI web.</p>
<h2 id="form--des-petites-choses-qui-saccumulent">Form : des petites choses qui s&rsquo;accumulent</h2>
<p><code>ChoiceType</code> reçoit une option <code>duplicate_preferred_choices</code>. La définir à <code>false</code> et on arrête de montrer la même option deux fois quand les choix préférés chevauchent la liste complète. <code>FormEvent::setData()</code> est déprécié pour les événements où les données sont déjà verrouillées à ce point du cycle de vie. La barre oblique auto-fermante sur les éléments <code>&lt;input&gt;</code> est aussi supprimée : <code>&lt;input&gt;</code> est un élément void en HTML5 et la barre oblique était techniquement invalide.</p>
<p>Le support des enums dans les formulaires est bien fait : <code>ChoiceType</code> rend les backed enums directement, et les enums translatable reçoivent leurs labels via le translator sans câblage personnalisé.</p>
<h2 id="httpfoundation--small-but-useful">HttpFoundation : small but useful</h2>
<p><code>Response::send()</code> reçoit un paramètre <code>$flush</code>. Passer <code>false</code> pour bufferiser la sortie sans la flusher au client, utile quand on enchaîne des middlewares qui doivent inspecter la réponse avant qu&rsquo;elle quitte le processus.</p>
<p><code>UriSigner</code> passe de HttpKernel à HttpFoundation, où il appartient sémantiquement. Même nom de classe, namespace différent.</p>
<p>Les cookies reçoivent le support CHIPS (Cookies Having Independent Partitioned State), le mécanisme navigateur pour les cookies cross-site dans une partition first-party. Ça ne compte que si on construit des widgets embarquables, mais bon à savoir que c&rsquo;est là.</p>
<h2 id="translation--provider-phrase-et-sortie-arborescente">Translation : provider Phrase et sortie arborescente</h2>
<p>Phrase rejoint Crowdin et Lokalise comme provider de traduction supporté. Le configurer dans <code>config/packages/translation.yaml</code> et les commandes <code>translation:push</code> / <code>translation:pull</code> gèrent la synchronisation.</p>
<p><code>translation:pull</code> reçoit une option <code>--as-tree</code> qui écrit les fichiers de traduction en YAML imbriqué plutôt qu&rsquo;en clés à notation pointée plate. Si c&rsquo;est vraiment mieux dépend entièrement de l&rsquo;équipe.</p>
<p><code>LocaleSwitcher::runWithLocale()</code> passe maintenant la locale courante comme argument au callback, évitant un appel <code>getLocale()</code> à l&rsquo;intérieur :</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>$switcher<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">runWithLocale</span>(<span style="color:#e6db74">&#39;fr&#39;</span>, <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">string</span> $locale) <span style="color:#66d9ef">use</span> ($mailer) {
</span></span><span style="display:flex;"><span>    $mailer<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">send</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">buildEmail</span>($locale));
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h2 id="quelques-choses-dans-serializer-et-domcrawler">Quelques choses dans Serializer et DomCrawler</h2>
<p>L&rsquo;attribut <code>Context</code> du Serializer peut maintenant cibler des classes spécifiques, donc un seul DTO peut se comporter différemment pendant la (dé)sérialisation selon quelle classe détient le contexte. <code>TranslatableNormalizer</code> arrive pour normaliser les objets qui implémentent <code>TranslatableInterface</code> : le translator est appelé pendant la normalisation, pas avant.</p>
<p><code>Crawler::attr()</code> reçoit un paramètre <code>$default</code>. Au lieu de null-checker la valeur de retour, on passe une valeur de repli :</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>$src <span style="color:#f92672">=</span> $crawler<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">attr</span>(<span style="color:#e6db74">&#39;src&#39;</span>, <span style="color:#e6db74">&#39;/placeholder.png&#39;</span>);
</span></span></code></pre></div><p><code>assertAnySelectorText()</code> et <code>assertAnySelectorTextContains()</code> rejoignent l&rsquo;ensemble d&rsquo;assertions DomCrawler. Ils passent si au moins un élément correspondant satisfait la condition, plutôt que d&rsquo;en exiger que tous correspondent.</p>
<h2 id="httpclient--réponses-har-pour-les-tests">HttpClient : réponses HAR pour les tests</h2>
<p><code>MockResponse</code> accepte maintenant les fichiers HAR (HTTP Archive). Enregistrer de vraies interactions HTTP dans le navigateur ou avec un proxy, déposer le fichier <code>.har</code> dans les fixtures de tests, et les rejouer :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$client <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">MockHttpClient</span>(<span style="color:#a6e22e">HarFileResponseFactory</span><span style="color:#f92672">::</span><span style="color:#a6e22e">createFromFile</span>(<span style="color:#66d9ef">__DIR__</span><span style="color:#f92672">.</span><span style="color:#e6db74">&#39;/fixtures/api.har&#39;</span>));
</span></span></code></pre></div><p>Bien mieux qu&rsquo;écrire des stubs de réponse à la main quand on traite avec une API complexe.</p>
]]></content:encoded></item><item><title>Symfony 6.4 LTS : AssetMapper, Scheduler, Webhook et la version long terme</title><link>https://guillaumedelre.github.io/fr/2024/01/10/symfony-6.4-lts-assetmapper-scheduler-webhook-et-la-version-long-terme/</link><pubDate>Wed, 10 Jan 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/01/10/symfony-6.4-lts-assetmapper-scheduler-webhook-et-la-version-long-terme/</guid><description>Part 8 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 6.4 LTS stabilise AssetMapper — une approche frontend sans bundler — aux côtés des composants Scheduler et Webhook.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 6.4 est sorti le 29 novembre 2023. C&rsquo;est une LTS avec une histoire : quatre composants qui sont sortis en expérimental dans des versions précédentes sont maintenant stables. Le plus important, c&rsquo;est AssetMapper.</p>
<h2 id="assetmapper">AssetMapper</h2>
<p>La gestion frontend moderne dans Symfony, ça voulait dire Webpack Encore. Encore fonctionne : il gère la transpilation, le bundling, le versioning, le hot reload. Il nécessite aussi Node.js, une étape de build séparée, et une quantité non négligeable de configuration pour ce qui est souvent un frontend assez modeste.</p>
<p>AssetMapper prend une position différente. Les navigateurs modernes supportent les modules ES nativement. Au lieu de bundler, livrer les fichiers tels quels, laisser le navigateur résoudre les imports via une importmap, et gérer les dépendances vendor via des fichiers téléchargés plutôt que des packages npm.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>composer require symfony/asset-mapper
</span></span><span style="display:flex;"><span>php bin/console importmap:require lodash
</span></span></code></pre></div><p>Pas de Node.js. Pas de npm. Pas d&rsquo;étape de build. Les fichiers JavaScript et CSS sont versionnés et servis directement, avec un digest dans l&rsquo;URL pour le cache busting. Pour les applications où le frontend n&rsquo;est pas la principale préoccupation d&rsquo;ingénierie, ça supprime toute une chaîne d&rsquo;outils de l&rsquo;équation.</p>
<p>6.4 ajoute les fichiers CSS à l&rsquo;importmap, le préchargement CSS automatique via WebLink, et des commandes pour auditer et mettre à jour les dépendances vendor. L&rsquo;expérience package.json, sans npm.</p>
<h2 id="scheduler">Scheduler</h2>
<p>Le composant Scheduler (planification de tâches périodiques et de style cron sans runner externe) sort d&rsquo;expérimental et devient stable. L&rsquo;API utilise des attributs :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[AsCronTask(&#39;0 * * * *&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HourlyReport</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ScheduledTaskInterface</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">run</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> { <span style="color:#f92672">...</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Soutenu par les transports Messenger, les tâches tournent dans tout environnement où un worker est en cours d&rsquo;exécution. Pour beaucoup de cas d&rsquo;usage, ça remplace le pattern classique entrée <code>cron</code> + commande console.</p>
<h2 id="webhook-et-remoteevent">Webhook et RemoteEvent</h2>
<p>Aussi diplômés d&rsquo;expérimental : le composant Webhook gère les webhooks entrants depuis des services externes. Au lieu d&rsquo;écrire des contrôleurs bruts qui parsent les payloads et dispatchent des événements à la main, on configure des parseurs pour des services connus (Stripe, GitHub, Mailgun) et on obtient des événements typés.</p>
<h2 id="datepoint">DatePoint</h2>
<p>Une nouvelle classe <code>DatePoint</code> dans le composant Clock : un wrapper <code>DateTime</code> immutable qui lève des exceptions sur les modificateurs invalides au lieu de retourner silencieusement <code>false</code>. Petite chose, mais significative pour le code qui manipule des dates et veut réellement savoir quand quelque chose va mal.</p>
<h2 id="la-fenêtre-de-support">La fenêtre de support</h2>
<p>6.4 LTS reçoit des corrections de bugs jusqu&rsquo;en novembre 2026 et des correctifs de sécurité jusqu&rsquo;en novembre 2027. Le chemin de 6.4 vers 7.4 (la prochaine LTS) passe par les notices de dépréciation de 6.4, comme d&rsquo;habitude.</p>
<h2 id="routes-sans-strings-magiques">Routes sans strings magiques</h2>
<p>Les alias de routes basés sur le FQCN sont maintenant générés automatiquement. Si une méthode de contrôleur a une seule route, Symfony crée un alias en utilisant son nom de classe complet :</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">// Auparavant : seul &#39;blog_index&#39; fonctionnait
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Maintenant : les deux fonctionnent de manière identique
</span></span></span><span style="display:flex;"><span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">urlGenerator</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">generate</span>(<span style="color:#e6db74">&#39;blog_index&#39;</span>);
</span></span><span style="display:flex;"><span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">urlGenerator</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">generate</span>(<span style="color:#a6e22e">BlogController</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span><span style="color:#f92672">.</span><span style="color:#e6db74">&#39;::index&#39;</span>);
</span></span></code></pre></div><p>Pour les contrôleurs invocables, l&rsquo;alias est juste le nom de classe. L&rsquo;avantage pratique : navigation IDE et sécurité au refactoring — on référence une constante de classe, pas une string qui peut silencieusement diverger.</p>
<h2 id="deux-nouveaux-attributs-di">Deux nouveaux attributs DI</h2>
<p><code>#[AutowireLocator]</code> et <code>#[AutowireIterator]</code> rejoignent la famille d&rsquo;attributs DI. Au lieu de configurer des service locators et des itérables taggués en YAML, on les déclare juste sur les paramètres du constructeur :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[AutowireLocator([FooHandler::class, BarHandler::class])]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">ContainerInterface</span> $handlers,
</span></span><span style="display:flex;"><span>) {}
</span></span></code></pre></div><p>Alias, services optionnels (préfixés avec <code>?</code>), et injection de paramètres via <code>SubscribedService</code> sont tous supportés. Le locator charge paresseusement, donc seuls les handlers qu&rsquo;on appelle vraiment sont instanciés.</p>
<h2 id="messenger-reçoit-des-handlers-intégrés">Messenger reçoit des handlers intégrés</h2>
<p>Trois nouvelles classes de message couvrent des tâches courantes qui nécessitaient auparavant des handlers personnalisés.</p>
<p><code>RunProcessMessage</code> dispatche une commande <code>Process</code> via le bus. <code>RunCommandMessage</code> fait de même pour les commandes console. Les deux retournent un objet de contexte avec le code de sortie et la sortie. <code>PingWebhookMessage</code> pingue une URL, ce qui est utile pour surveiller les tâches planifiées sans mettre en place un service de health-check dédié :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">bus</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">RunCommandMessage</span>(<span style="color:#e6db74">&#39;cache:clear&#39;</span>));
</span></span><span style="display:flex;"><span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">bus</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PingWebhookMessage</span>(<span style="color:#e6db74">&#39;GET&#39;</span>, <span style="color:#e6db74">&#39;https://healthchecks.io/ping/abc123&#39;</span>));
</span></span></code></pre></div><p>Le problème d&rsquo;héritage des sous-processus a aussi été résolu avec <code>PhpSubprocess</code>. Quand on lance PHP avec une limite mémoire personnalisée (<code>-d memory_limit=-1</code>), les processus enfants lancés avec <code>Process</code> ne l&rsquo;héritent pas. <code>PhpSubprocess</code> le fait :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$sub <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PhpSubprocess</span>([<span style="color:#e6db74">&#39;bin/console&#39;</span>, <span style="color:#e6db74">&#39;app:heavy-import&#39;</span>]);
</span></span></code></pre></div><h2 id="sécurité--trois-corrections-pour-des-situations-réelles">Sécurité : trois corrections pour des situations réelles</h2>
<p>Le profiler montre maintenant comment les badges de sécurité ont été résolus pendant l&rsquo;authentification : lesquels ont passé, lesquels ont échoué, et pourquoi. Avant, il fallait ajouter de la sortie de debug manuellement quand un authentificateur personnalisé ne se comportait pas bien.</p>
<p>Le throttling de login via RateLimiter hache maintenant automatiquement les PII dans les logs. Les adresses IP et les noms d&rsquo;utilisateur sont hachés avec le secret du kernel avant d&rsquo;être écrits. Pas de config nécessaire, pas de regex sur les lignes de log.</p>
<p>Les patterns de firewall acceptent maintenant des tableaux :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">firewalls</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">no_security</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;^/register$&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;^/api/webhooks/&#34;</span>
</span></span></code></pre></div><p>Fini les acrobaties regex pour les exclusions multi-chemins.</p>
<h2 id="déconnexion-sans-contrôleur-bidon">Déconnexion sans contrôleur bidon</h2>
<p>La route de déconnexion nécessitait auparavant un contrôleur qui ne faisait rien que lever une exception, avec un commentaire expliquant que oui, c&rsquo;est intentionnel. 6.4 élimine ça :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># config/routes/security.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">_security_logout</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resource</span>: <span style="color:#ae81ff">security.route_loader.logout</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">service</span>
</span></span></code></pre></div><p>Le route loader s&rsquo;en occupe. Le contrôleur bidon est parti. Flex met à jour la recette.</p>
<h2 id="le-sérialiseur-en-meilleure-forme">Le sérialiseur en meilleure forme</h2>
<p>Trois améliorations du sérialiseur qui résolvent chacune un vrai problème.</p>
<p>Attribut <code>#[Groups]</code> au niveau de la classe : appliquer un groupe à la classe entière, puis surcharger par propriété. Utile quand une ressource a un groupe de sérialisation par défaut et quelques champs qui nécessitent un contrôle plus fin.</p>
<p>Les objets translatable ont maintenant un normaliseur dédié. Les strings translatable (enveloppant <code>TranslatableInterface</code> de Doctrine) sont traduites vers la locale passée via <code>NORMALIZATION_LOCALE_KEY</code> pendant la normalisation. Avant ça, il fallait écrire un normaliseur personnalisé.</p>
<p>En mode debug, les erreurs de décodage JSON utilisent maintenant <code>seld/jsonlint</code> pour de meilleurs messages. Au lieu de &ldquo;Syntax error&rdquo;, on obtient la ligne et ce qui s&rsquo;est vraiment passé :</p>
<pre tabindex="0"><code>Parse error on line 1: {&#39;foo&#39;: &#39;bar&#39;}
           ^ Invalid string, used single quotes instead of double quotes
</code></pre><h2 id="profilers-pour-les-choses-qui-nétaient-pas-des-requêtes-http">Profilers pour les choses qui n&rsquo;étaient pas des requêtes HTTP</h2>
<p>Le profiler de commande étend le profiler existant aux commandes console. Ajouter <code>--profile</code> à n&rsquo;importe quelle commande et obtenir une entrée complète dans le profiler : entrée/sortie, temps d&rsquo;exécution, mémoire, requêtes en base, messages de log. Les commandes qui nécessitaient <code>--verbose</code> plus du timing manuel ont maintenant la même expérience de debug que les requêtes HTTP.</p>
<p>Le profiler de workflow fait de même pour les machines à états. Un nouveau panneau montre une représentation graphique des workflows et les transitions déclenchées pendant la requête. Zéro configuration.</p>
<h2 id="laccumulation-de-dx">L&rsquo;accumulation de DX</h2>
<p>Plusieurs additions plus petites qui se combinent.</p>
<p><code>renderBlock()</code> et <code>renderBlockView()</code> sur <code>AbstractController</code> permettent de rendre un bloc Twig nommé et de le retourner comme <code>Response</code> ou string. Pratique pour les réponses Turbo Stream où on veut mettre à jour un fragment sans une action de contrôleur complète.</p>
<p>Le processeur d&rsquo;env <code>defined</code> retourne un booléen plutôt que la valeur : <code>true</code> si la variable existe et n&rsquo;est pas vide, <code>false</code> sinon. Utile pour les feature flags pilotés par des variables d&rsquo;environnement :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">is_feature_enabled</span>: <span style="color:#e6db74">&#39;%env(defined:FEATURE_FLAG_KEY)%&#39;</span>
</span></span></code></pre></div><p><code>HttpClient</code> accepte maintenant <code>max_retries</code> par requête, surchargeant la stratégie globale de retry. La méthode <code>filter()</code> du composant Finder accepte un second argument pour élaguer des répertoires entiers tôt, ce qui compte quand on cherche dans de grands arbres.</p>
<p>La méthode <code>click()</code> de <code>BrowserKit</code> accepte maintenant des paramètres serveur comme en-têtes supplémentaires, utile dans les tests fonctionnels qui doivent simuler des appels API authentifiés en suivant des liens.</p>
<h2 id="limpersonation-devient-utilisable-dans-les-templates">L&rsquo;impersonation devient utilisable dans les templates</h2>
<p>Deux nouveaux helpers Twig : <code>impersonation_path()</code> et <code>impersonation_url()</code>. Ils génèrent les URLs correctes incluant le paramètre de query switch-user, qui est configurable et n&rsquo;a aucune raison d&rsquo;être codé en dur dans les templates. Les associer avec l&rsquo;existant <code>impersonation_exit_path()</code> pour le flux complet d&rsquo;impersonation admin.</p>
<h2 id="contrôle-des-locales-partout-où-ça-manquait">Contrôle des locales, partout où ça manquait</h2>
<p>Trois lacunes comblées. <code>TemplatedEmail</code> a maintenant une méthode <code>locale()</code> pour rendre les emails dans la langue du destinataire. <code>runWithLocale()</code> du locale switcher passe maintenant la locale comme argument au callback, donc on n&rsquo;a pas à la capturer depuis la portée extérieure. Et <code>app.enabledLocales</code> est disponible dans Twig, donc on peut construire des sélecteurs de langue sans coder en dur les listes de locales.</p>
<h2 id="déployer-sur-des-filesystems-en-lecture-seule">Déployer sur des filesystems en lecture seule</h2>
<p><code>APP_BUILD_DIR</code> est maintenant une variable d&rsquo;environnement reconnue par le kernel. La définir pour rediriger les artefacts compilés (cache du router, proxies Doctrine, traductions préchargées) vers un répertoire qui existe, même quand le répertoire cache par défaut n&rsquo;existe pas. <code>MicroKernelTrait</code> l&rsquo;utilise automatiquement. <code>WarmableInterface</code> a reçu un paramètre <code>$buildDir</code> pour supporter cette séparation : les warmers de cache personnalisés qui écrivent des artefacts en lecture seule doivent se mettre à jour en conséquence.</p>
]]></content:encoded></item><item><title>Symfony 6.0 : PHP 8.1 uniquement, et le système de sécurité reconstruit</title><link>https://guillaumedelre.github.io/fr/2022/01/12/symfony-6.0-php-8.1-uniquement-et-le-syst%C3%A8me-de-s%C3%A9curit%C3%A9-reconstruit/</link><pubDate>Wed, 12 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/01/12/symfony-6.0-php-8.1-uniquement-et-le-syst%C3%A8me-de-s%C3%A9curit%C3%A9-reconstruit/</guid><description>Part 7 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 6.0 exige PHP 8.1, supprime l&amp;#39;ancien système de sécurité et reconstruit l&amp;#39;authentification sur des fondations plus propres.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 6.0 est sorti le 29 novembre 2021. La caractéristique définissante : PHP 8.1 est le minimum. Pas supporté, requis. L&rsquo;équipe de releases a attendu que PHP 8.1 sorte, puis a coupé Symfony 6.0 le lendemain.</p>
<p>Ce n&rsquo;est pas juste un bump de version. C&rsquo;est un engagement à construire contre le langage actuel plutôt que le plancher historique.</p>
<h2 id="le-système-de-sécurité-enfin-reconstruit">Le système de sécurité, enfin reconstruit</h2>
<p>Le composant de sécurité Symfony a deux systèmes. L&rsquo;ancien (<code>AnonymousToken</code>, <code>GuardAuthenticatorInterface</code>, un enchevêtrement d&rsquo;interfaces qui vous faisaient implémenter des méthodes dont vous n&rsquo;aviez pas besoin) avait été déprécié. 6.0 le supprime entièrement.</p>
<p>Le nouveau système de sécurité (<code>security.enable_authenticator_manager: true</code> en 5.x) est maintenant le seul système. C&rsquo;est plus propre : une seule interface à implémenter, séparation claire entre authentification et autorisation, vérification des credentials basée sur des passeports. La migration depuis les anciens guard authenticators n&rsquo;est pas indolore, mais la destination est beaucoup moins confuse.</p>
<h2 id="la-classe-path-du-filesystem">La classe Path du Filesystem</h2>
<p>Travailler avec des chemins de fichiers en PHP est fondamentalement un problème de manipulation de strings. <code>__DIR__</code>, concaténation, <code>realpath()</code>, séparateurs spécifiques à la plateforme : la bibliothèque standard donne des primitives mais pas vraiment un modèle.</p>
<p>La nouvelle classe <code>Path</code> gère ça :</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">join</span>(<span style="color:#e6db74">&#39;/var/www&#39;</span>, <span style="color:#e6db74">&#39;html&#39;</span>, <span style="color:#e6db74">&#39;../uploads&#39;</span>); <span style="color:#75715e">// /var/www/uploads
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">Path</span><span style="color:#f92672">::</span><span style="color:#a6e22e">makeRelative</span>(<span style="color:#e6db74">&#39;/var/www/html&#39;</span>, <span style="color:#e6db74">&#39;/var/www&#39;</span>); <span style="color:#75715e">// html
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">Path</span><span style="color:#f92672">::</span><span style="color:#a6e22e">isAbsolute</span>(<span style="color:#e6db74">&#39;./relative/path&#39;</span>); <span style="color:#75715e">// false
</span></span></span></code></pre></div><p>Multiplateforme, sans effets de bord, sans accès au filesystem nécessaire. Aussi dans 6.0 : support des patterns <code>.gitignore</code> imbriqués dans Finder.</p>
<h2 id="les-enums-dans-le-système-de-formulaires">Les enums dans le système de formulaires</h2>
<p>En s&rsquo;appuyant sur les fondations posées par 5.4, 6.0 pousse le support des enums plus loin. Les valeurs <code>BackedEnum</code> font des allers-retours à travers les formulaires et le sérialiseur sans transformateurs personnalisés. Le composant de formulaire comprend les cases d&rsquo;enum comme options de choix nativement.</p>
<h2 id="ce-que-60-supprime">Ce que 6.0 supprime</h2>
<p>La liste des suppressions est extensive : l&rsquo;ancien système de sécurité, le composant <code>Templating</code>, le support des annotations PHP (remplacées par les attributs natifs), le support du cache Doctrine, <code>ContainerAwareTrait</code>. Six années de marqueurs <code>@deprecated</code> accumulés, finalement nettoyés.</p>
<p>Les applications qui avaient pris au sérieux les warnings de dépréciation de 5.4 avaient un chemin de migration propre. Celles qui ne l&rsquo;avaient pas fait avaient du travail à faire.</p>
<h2 id="la-complétion-automatique-était-toujours-le-manque">La complétion automatique était toujours le manque</h2>
<p>Le composant Console a reçu l&rsquo;autocomplétion shell, et c&rsquo;est proprement intégré : définir une méthode <code>complete()</code> sur sa commande, et Tab dans Bash suggère des valeurs valides pour les options et arguments.</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">DeployCommand</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Command</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;env&#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;prod&#39;</span>, <span style="color:#e6db74">&#39;staging&#39;</span>, <span style="color:#e6db74">&#39;dev&#39;</span>]);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Toutes les commandes Symfony intégrées ont reçu la complétion aussi : <code>debug:router</code>, <code>cache:pool:clear</code>, <code>lint:yaml</code>, et une quinzaine d&rsquo;autres. Exécuter <code>bin/console completion bash &gt;&gt; ~/.bashrc</code> et c&rsquo;est terminé.</p>
<h2 id="messenger-maintenant-avec-attributs-et-traitement-par-lots">Messenger, maintenant avec attributs et traitement par lots</h2>
<p>L&rsquo;attribut <code>#[AsMessageHandler]</code> remplace l&rsquo;ancienne <code>MessageHandlerInterface</code>. Moins de boilerplate, et on peut maintenant configurer l&rsquo;affinité de transport et la priorité directement sur l&rsquo;attribut :</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">SendWelcomeEmailHandler</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">UserRegistered</span> $message)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> { <span style="color:#f92672">...</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;autre ajout significatif : <code>BatchHandlerInterface</code>. Quand on insère un millier de lignes, traiter les messages un par un est du gaspillage. Les batch handlers collectent les messages et les traitent en groupes. La taille de lot par défaut est 10, contrôlée par <code>BatchHandlerTrait::shouldFlush()</code>. L&rsquo;<code>Acknowledger</code> gère le succès et l&rsquo;échec individuels dans le lot.</p>
<p><code>reset_on_message: true</code> dans la config Messenger réinitialise les services du conteneur entre les messages. Auparavant, un buffer Monolog pouvait se remplir à travers la gestion des messages et personne ne s&rsquo;en rendait compte avant la production. Ça évite cette catégorie de bugs de stateful sans nécessiter de nettoyage manuel.</p>
<h2 id="le-conteneur-di-devient-plus-expressif">Le conteneur DI devient plus expressif</h2>
<p>Trois changements qui comptent en pratique.</p>
<p>Les types union et intersection s&rsquo;autowire maintenant. PHP 8.1 a ajouté les types intersection, et Symfony 6.0 les câble :</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>Ça fonctionne tant que les deux interfaces pointent vers le même service via les alias d&rsquo;autowiring.</p>
<p><code>TaggedIterator</code> et <code>TaggedLocator</code> ont reçu les options <code>defaultPriorityMethod</code> et <code>defaultIndexMethod</code>. On n&rsquo;a plus besoin de YAML pour exprimer l&rsquo;ordonnancement ou l&rsquo;indexation pour les services taggués :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[TaggedIterator(tag: &#39;app.handler&#39;, defaultPriorityMethod: &#39;getPriority&#39;)]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">iterable</span> $handlers,
</span></span><span style="display:flex;"><span>) {}
</span></span></code></pre></div><p><code>SubscribedService</code> (l&rsquo;attribut qui remplace la magie implicite de <code>ServiceSubscriberTrait</code>) rend l&rsquo;accès paresseux aux services explicite et typable :</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">#[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">mailer</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">MailerInterface</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></code></pre></div><h2 id="la-validation-reçoit-trois-nouveaux-outils">La validation reçoit trois nouveaux outils</h2>
<p><code>CssColor</code> valide les valeurs de couleurs CSS dans les formats voulus : hex, RGB, HSL, couleurs nommées, ou n&rsquo;importe quel mélange. Utile pour les champs de configuration de thème où on veut accepter <code>#ff0000</code> mais pas <code>red</code>, ou vice versa.</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(formats: Assert\CssColor::HEX_LONG)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span> $brandColor;
</span></span></code></pre></div><p><code>Cidr</code> valide la notation CIDR pour IPv4 et IPv6, avec des options pour fixer la version et contraindre la plage de masque réseau. Les outils d&rsquo;infrastructure et les formulaires de config réseau ont enfin une contrainte de première classe.</p>
<p>Le troisième ajout n&rsquo;est pas une nouvelle contrainte. Ce sont les attributs imbriqués PHP 8.1 qui rendent les contraintes composées existantes utilisables sans XML. <code>AtLeastOneOf</code>, <code>Collection</code>, <code>All</code>, <code>Sequentially</code> : tout ça nécessitait auparavant des contournements d&rsquo;annotation. Maintenant ça fonctionne juste comme attributs :</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\Collection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fields</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;email&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\Email</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;role&#39;</span>  <span style="color:#f92672">=&gt;</span> [<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\NotBlank</span>(), <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Assert\Choice</span>([<span style="color:#e6db74">&#39;admin&#39;</span>, <span style="color:#e6db74">&#39;user&#39;</span>])],
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">array</span> $payload;
</span></span></code></pre></div><h2 id="le-sérialiseur-nettoyé">Le sérialiseur, nettoyé</h2>
<p>Deux choses. D&rsquo;abord, le contexte de sérialisation est maintenant configurable globalement au lieu d&rsquo;être répété à chaque appel <code>serialize()</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:#75715e"># config/packages/serializer.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">serializer</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">default_context</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">enable_max_depth</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Ensuite, l&rsquo;option <code>COLLECT_DENORMALIZATION_ERRORS</code> change comment le sérialiseur gère les erreurs de type à la désérialisation. Au lieu de lever une exception au premier problème, il les collecte tous et les expose via <code>PartialDenormalizationException</code>. Si on écrit une API qui désérialise des corps de requête, c&rsquo;est la différence entre retourner &ldquo;le premier champ qui échoue&rdquo; et &ldquo;tous les champs qui échouent&rdquo; dans une seule réponse.</p>
<h2 id="les-utilitaires-de-string-que-personne-ne-savait-vouloir">Les utilitaires de string que personne ne savait vouloir</h2>
<p><code>trimPrefix()</code> et <code>trimSuffix()</code> sur les classes <code>UnicodeString</code> / <code>ByteString</code>. Pas glamour, mais supprimer un préfixe connu avec <code>ltrim()</code> est un piège subtil : ça supprime des caractères, pas des strings. Ceux-ci sont corrects :</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:#66d9ef">function</span> <span style="color:#a6e22e">Symfony\Component\String\u</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">u</span>(<span style="color:#e6db74">&#39;file-image-001.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-001.png&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">u</span>(<span style="color:#e6db74">&#39;report.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;report.html&#39;
</span></span></span></code></pre></div><p>Aussi dans cette version : <code>NilUlid</code> pour les ULIDs à valeur zéro, <code>perMonth()</code> et <code>perYear()</code> sur RateLimiter pour quand les limites horaires n&rsquo;ont pas de sens, et <code>appendToFile()</code> dans le composant Filesystem a reçu un paramètre <code>LOCK_EX</code> optionnel pour les écrivains concurrents.</p>
<h2 id="déboguer-lenvironnement">Déboguer l&rsquo;environnement</h2>
<p><code>debug:dotenv</code> est une nouvelle commande console qui montre quels fichiers <code>.env</code> ont été chargés et d&rsquo;où vient chaque valeur. Quand on a <code>.env</code>, <code>.env.local</code>, <code>.env.test</code>, et <code>.env.test.local</code> qui se battent et que quelque chose ne va pas, cette commande dit exactement quel fichier a gagné. Elle n&rsquo;apparaît que quand le composant Dotenv est utilisé, ce qui est le cas pour toute application Symfony standard.</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 5.0 : String, Notifier et le coffre-fort de secrets</title><link>https://guillaumedelre.github.io/fr/2020/01/06/symfony-5.0-string-notifier-et-le-coffre-fort-de-secrets/</link><pubDate>Mon, 06 Jan 2020 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2020/01/06/symfony-5.0-string-notifier-et-le-coffre-fort-de-secrets/</guid><description>Part 5 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 5.0 ajoute un composant String Unicode-aware, un Notifier multi-canal, et un coffre-fort de secrets intégré.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 5.0 est sorti le 21 novembre 2019, le même jour que la 4.4. Là où la 4.4 mise sur la stabilité et une longue fenêtre de support, la 5.0 ouvre un nouveau chapitre : plus de code déprécié, PHP 7.2.5 minimum, et quelques nouveaux composants qui comblent enfin des lacunes accumulées depuis des années.</p>
<h2 id="le-composant-string">Le composant String</h2>
<p>La gestion des chaînes en PHP est notoirement éparpillée : des fonctions avec préfixe par-ci (<code>str_</code>), avec suffixe par-là (<code>strpos</code>), un support d&rsquo;encodage incohérent, et rien d&rsquo;orienté objet en vue. Le composant String enveloppe tout ça dans une API fluide orientée objet avec support Unicode :</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\String\UnicodeString</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$str <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">UnicodeString</span>(<span style="color:#e6db74">&#39;  Hello World  &#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $str<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">trim</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">lower</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#e6db74">&#39;-&#39;</span>); <span style="color:#75715e">// hello-world
</span></span></span></code></pre></div><p>L&rsquo;ajout pratique, c&rsquo;est le <code>Slugger</code>, un générateur de slug locale-aware qui gère vraiment correctement les caractères accentués :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$slug <span style="color:#f92672">=</span> $slugger<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">slug</span>(<span style="color:#e6db74">&#39;L\&#39;été à Montréal&#39;</span>); <span style="color:#75715e">// l-ete-a-montreal
</span></span></span></code></pre></div><p>Avant, il fallait intégrer une bibliothèque tierce ou en écrire une soi-même. Maintenant ça ship avec FrameworkBundle, disponible par défaut.</p>
<h2 id="notifier">Notifier</h2>
<p>Le courrier électronique est géré par Mailer. SMS, notifications push, messages de chat : pas de solution first-party, jusqu&rsquo;à maintenant. Le composant Notifier en ajoute une : une interface unifiée sur des dizaines de canaux et fournisseurs.</p>
<p>La même notification peut atterrir sur Slack, déclencher un SMS via Twilio, ou finir comme notification push, tout configuré via des DSN. Ajouter un nouveau canal, c&rsquo;est un changement de config, pas un changement de code.</p>
<h2 id="le-coffre-fort-de-secrets">Le coffre-fort de secrets</h2>
<p>Stocker des secrets dans des fichiers <code>.env</code> fonctionne, mais les valeurs sont en clair, les environnements partagés sont une galère, et il n&rsquo;y a aucun moyen natif de chiffrer quoi que ce soit au repos.</p>
<p>Symfony 5.0 ajoute une famille de commandes <code>secrets:</code> et un mécanisme de coffre-fort. Les secrets sont chiffrés avec une paire de clés stockée hors du dépôt. Les fichiers chiffrés sont commités ; la clé de déchiffrement ne l&rsquo;est pas. En production, la clé arrive comme variable d&rsquo;environnement ou est injectée depuis un gestionnaire de secrets.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>php bin/console secrets:set DATABASE_PASSWORD
</span></span><span style="display:flex;"><span>php bin/console secrets:decrypt-to-local --force
</span></span></code></pre></div><p>Pas une solution de gestion de secrets à part entière, mais un vrai pas en avant par rapport à un fichier <code>.env</code> en clair qui traîne non chiffré dans le dépôt.</p>
<h2 id="mailer-reçoit-une-couche-de-notification">Mailer reçoit une couche de notification</h2>
<p>Le composant Mailer est arrivé en 4.4. Ce que la 5.0 ajoute par-dessus, c&rsquo;est la <code>NotificationEmail</code> : un email pré-stylisé et responsive construit sur Foundation for Emails, avec une API explicite pour les niveaux d&rsquo;importance et les boutons d&rsquo;appel à l&rsquo;action :</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\Bridge\Twig\Mime\NotificationEmail</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$email <span style="color:#f92672">=</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">NotificationEmail</span>())
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">from</span>(<span style="color:#e6db74">&#39;alerts@example.com&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">to</span>(<span style="color:#e6db74">&#39;admin@example.com&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">subject</span>(<span style="color:#e6db74">&#39;Disk usage critical&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">markdown</span>(<span style="color:#e6db74">&#39;The disk on **prod-01** is at 94%. Check it now.&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">action</span>(<span style="color:#e6db74">&#39;Open dashboard&#39;</span>, <span style="color:#e6db74">&#39;https://example.com/servers&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">importance</span>(<span style="color:#a6e22e">NotificationEmail</span><span style="color:#f92672">::</span><span style="color:#a6e22e">IMPORTANCE_URGENT</span>);
</span></span></code></pre></div><p>Pas de template à écrire, pas de CSS inline à dompter. Pour les alertes transactionnelles, les notifications de facturation et les emails système, ça couvre 80 % de ce dont on a besoin sans toucher à quoi que ce soit.</p>
<h2 id="les-firewalls-paresseux-et-le-problème-de-cache">Les firewalls paresseux et le problème de cache</h2>
<p>Chaque firewall stateful dans Symfony charge l&rsquo;utilisateur depuis la session à chaque requête, que l&rsquo;action en ait besoin ou non. Ce qui signifie que toute réponse est non-cacheable par défaut, même pour des pages qui ne touchent jamais à <code>$this-&gt;getUser()</code>.</p>
<p>La 5.0 ajoute le mode <code>lazy</code> pour les firewalls, qui diffère l&rsquo;accès à la session jusqu&rsquo;à ce que le code appelle réellement <code>is_granted()</code> ou accède au token 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># config/packages/security.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">security</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">firewalls</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">main</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">pattern</span>: <span style="color:#ae81ff">^/</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">anonymous</span>: <span style="color:#ae81ff">lazy</span>
</span></span></code></pre></div><p>Les pages qui n&rsquo;ont pas besoin de l&rsquo;utilisateur redeviennent cacheables. Les nouveaux projets obtiennent ça par défaut via la recette Flex ; les existants ont besoin d&rsquo;un changement de config en une ligne.</p>
<h2 id="les-migrations-de-mots-de-passe-sans-grand-soir">Les migrations de mots de passe sans grand soir</h2>
<p>Migrer une app en production de bcrypt vers argon2id impliquait jusqu&rsquo;ici de forcer une réinitialisation du mot de passe pour chaque utilisateur. Le <code>PasswordUpgraderInterface</code> rend ça progressif : à la connexion, Symfony vérifie si le hash stocké correspond à l&rsquo;algorithme courant. Sinon, il le re-hash sur place et appelle votre upgrader pour le sauvegarder :</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">// src/Repository/UserRepository.php
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UserRepository</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">ServiceEntityRepository</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">PasswordUpgraderInterface</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">upgradePassword</span>(<span style="color:#a6e22e">UserInterface</span> $user, <span style="color:#a6e22e">string</span> $newHashedPassword)<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>        $user<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setPassword</span>($newHashedPassword);
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getEntityManager</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">flush</span>();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Associez ça à <code>algorithm: auto</code> dans la config de l&rsquo;encodeur, et les anciens hashs migrent silencieusement à mesure que les utilisateurs se connectent. Pas de script de migration, pas de downtime, pas de friction pour l&rsquo;utilisateur.</p>
<h2 id="errorhandler-remplace-debug">ErrorHandler remplace Debug</h2>
<p>Le composant Debug est parti. Son remplaçant, ErrorHandler, fait le même travail (convertir les erreurs PHP en exceptions, afficher de belles pages d&rsquo;erreur) mais sans nécessiter Twig. Pour les apps API qui ne rendent jamais de HTML, ça compte : ErrorHandler génère les erreurs dans le format de la requête (JSON, XML, texte) en suivant RFC 7807 :</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>La config de routing passe de <code>TwigBundle</code> à <code>FrameworkBundle</code>, et c&rsquo;est la seule étape de migration pour la plupart des projets. Une ligne, c&rsquo;est fait.</p>
<h2 id="les-listeners-dévénements-enfin-moins-verbeux">Les listeners d&rsquo;événements, enfin moins verbeux</h2>
<p>Enregistrer un listener d&rsquo;événement kernel impliquait auparavant de nommer explicitement l&rsquo;événement dans le tag de service. Symfony 5.0 l&rsquo;infère depuis la signature de méthode :</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">// Pas de configuration de tag au-delà de kernel.event_listener
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SecurityListener</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">onKernelRequest</span>(<span style="color:#a6e22e">RequestEvent</span> $event)<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:#75715e">// Symfony lit le type hint et détermine l&#39;événement
</span></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:#75715e"># config/services.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">App\EventListener\SecurityListener</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tags</span>: [<span style="color:#ae81ff">kernel.event_listener]</span>
</span></span></code></pre></div><p>Utilisez <code>__invoke()</code> et ça fonctionne de la même façon. Enregistrez en masse tout un répertoire de listeners avec un seul bloc resource, et Symfony détermine quel événement chacun gère.</p>
<h2 id="httpclient-grandit">HttpClient grandit</h2>
<p>Le composant HttpClient est arrivé en 4.4 comme stable. La 5.0 ajoute quelques choses utiles par-dessus :</p>
<p>L&rsquo;authentification NTLM pour les environnements d&rsquo;entreprise, le buffering conditionnel via un callback (bufferiser les grandes réponses seulement quand le content-type correspond), une option <code>max_duration</code> qui plafonne le temps total de requête indépendamment des conditions réseau, et <code>toStream()</code> pour transformer n&rsquo;importe quelle réponse en un flux PHP standard pour le code qui attend du <code>fread()</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>$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/large-export&#39;</span>, [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;max_duration&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">30.0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;buffer&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">fn</span>(<span style="color:#66d9ef">array</span> $headers)<span style="color:#f92672">:</span> <span style="color:#a6e22e">bool</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">str_contains</span>($headers[<span style="color:#e6db74">&#39;content-type&#39;</span>][<span style="color:#ae81ff">0</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>, <span style="color:#e6db74">&#39;json&#39;</span>),
</span></span><span style="display:flex;"><span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Le streamer plutôt que de tout charger en mémoire
</span></span></span><span style="display:flex;"><span>$stream <span style="color:#f92672">=</span> $response<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">toStream</span>();
</span></span></code></pre></div><p>Le client a aussi obtenu une interopérabilité complète avec PSR-18 et HTTPlug v1/v2, donc toute bibliothèque qui dépend de ces abstractions fonctionne directement avec lui.</p>
<h2 id="ce-que-la-50-supprime">Ce que la 5.0 supprime</h2>
<p>La 5.0 abandonne tout ce qui était déprécié en 4.4. Les plus notables :</p>
<ul>
<li><code>WebServerBundle</code> (utilisez <code>symfony server:start</code> depuis l&rsquo;outil CLI à la place)</li>
<li>L&rsquo;<code>AnonymousToken</code> de l&rsquo;ancien système de sécurité (remplacé par <code>NullToken</code>)</li>
<li>Les anciens noms d&rsquo;événements de formulaire</li>
<li>Le ClassLoader interne de Symfony</li>
<li>Le composant Debug (remplacé par ErrorHandler)</li>
</ul>
<p>Si vous avez fait tourner votre app 4.4 avec les notices de dépréciation actives et corrigé les avertissements, la mise à niveau vers la 5.0 ne nécessite aucun changement de code.</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><item><title>Symfony 4.0 : Flex et la fin de la Standard Edition</title><link>https://guillaumedelre.github.io/fr/2018/01/14/symfony-4.0-flex-et-la-fin-de-la-standard-edition/</link><pubDate>Sun, 14 Jan 2018 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2018/01/14/symfony-4.0-flex-et-la-fin-de-la-standard-edition/</guid><description>Part 3 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 4.0 enterre la Standard Edition et introduit Flex : un microframework qui grandit exactement là où on en a besoin.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 4.0 est sorti le 30 novembre 2017, le même jour que la 3.4. La date de sortie commune est à peu près la seule chose qu&rsquo;ils ont en commun.</p>
<p>4.0, c&rsquo;est une philosophie différente. La Symfony Standard Edition, ce point de départ monolithique qui embarquait tout et vous laissait retirer ce dont vous n&rsquo;aviez pas besoin, a disparu. À sa place : un microframework qui grandit.</p>
<h2 id="flex">Flex</h2>
<p>Symfony Flex est un plugin Composer qui change la façon dont on installe les packages Symfony. Avant Flex, ajouter un bundle impliquait : l&rsquo;installer via Composer, l&rsquo;enregistrer dans <code>AppKernel.php</code>, ajouter la config dans <code>config/</code>, mettre à jour le routing si nécessaire. Quatre étapes, toutes manuelles.</p>
<p>Avec Flex, installer un package exécute une &ldquo;recette&rdquo; : un ensemble d&rsquo;étapes automatisées qui enregistre le bundle, génère un squelette de config et câble le routing. Installer Doctrine :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>composer require symfony/orm-pack
</span></span></code></pre></div><p>Cette commande installe les packages, crée <code>config/packages/doctrine.yaml</code>, ajoute les stubs de variables d&rsquo;environnement dans <code>.env</code>, et enregistre tout. Une commande, zéro étape manuelle.</p>
<p>Les recettes sont contribuées par la communauté et hébergées sur un serveur central. La qualité varie, mais pour les packages majeurs elles sont maintenues en parallèle des packages eux-mêmes.</p>
<h2 id="la-nouvelle-structure-de-projet">La nouvelle structure de projet</h2>
<p>Le layout de la Standard Edition (<code>app/</code>, <code>src/</code>, <code>web/</code>) est remplacé par une structure plus légère. La config se trouve dans <code>config/</code>, découpée par environnement. Le répertoire public s&rsquo;appelle désormais <code>public/</code>, plus <code>web/</code>. Le kernel est plus petit. Les controllers sont des classes ordinaires, plus besoin d&rsquo;<code>extends Controller</code>.</p>
<p>Plus important encore, le <code>services.yaml</code> par défaut utilise les conventions d&rsquo;autowiring de la 3.3 qui rendent la configuration explicite des services largement inutile. Les nouveaux projets démarrent minimaux et grossissent en ajoutant ce dont ils ont réellement besoin.</p>
<h2 id="services-privés-par-défaut">Services privés par défaut</h2>
<p>La plus grosse rupture de compatibilité de la 4.0 pour les apps existantes : tous les services sont privés par défaut. On ne peut plus récupérer un service directement depuis le container, il doit être injecté. C&rsquo;est le bon choix du point de vue de l&rsquo;injection de dépendances, mais ça casse tout ce qui utilisait <code>$this-&gt;get('service_id')</code> dans les controllers.</p>
<p>Le chemin de migration, c&rsquo;est <code>AbstractController</code>, qui fournit les mêmes méthodes pratiques via des service locators lazy plutôt qu&rsquo;un accès direct au container.</p>
<h2 id="ce-qui-a-été-supprimé">Ce qui a été supprimé</h2>
<p>4.0 est propre parce qu&rsquo;il supprime tout ce qui était déprécié en 3.4 :</p>
<ul>
<li>Les anciens événements de formulaire, les anciennes interfaces de sécurité, les anciens formats de configuration</li>
<li>Le support de PHP &lt; 7.1.3</li>
<li>Le composant ClassLoader</li>
<li>Le support ACL dans le SecurityBundle</li>
</ul>
<p>Les suppressions sont musclées. Les apps qui ont sauté la correction de leurs dépréciations 3.4 vont souffrir. Celles qui ont fait le ménage avant ont une migration tranquille.</p>
<p>Symfony 4.0, c&rsquo;est le reset dont le framework avait besoin. La Standard Edition avait accumulé des années de &ldquo;c&rsquo;est comme ça qu&rsquo;on fait&rdquo; que Flex balaie d&rsquo;un coup.</p>
<h2 id="des-variables-denvironnement-qui-connaissent-leur-type">Des variables d&rsquo;environnement qui connaissent leur type</h2>
<p>Avant 3.4 et 4.0, les variables d&rsquo;environnement étaient des chaînes. Toujours. Essayer d&rsquo;injecter <code>DATABASE_PORT</code> dans un paramètre de type <code>int</code> plantait silencieusement ou explosait avec une erreur de type. Le correctif était laid : caster en PHP ou éviter les paramètres typés.</p>
<p>4.0 embarque des processeurs de variables d&rsquo;environnement qui gèrent la conversion au niveau du container :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">app.connection.port</span>: <span style="color:#e6db74">&#39;%env(int:DATABASE_PORT)%&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">app.debug_mode</span>: <span style="color:#e6db74">&#39;%env(bool:APP_DEBUG)%&#39;</span>
</span></span></code></pre></div><p>Au-delà du casting, les processeurs peuvent décoder du base64, charger depuis des fichiers, parser du JSON, ou résoudre des paramètres du container dans une valeur. La combinaison <code>json:file:</code> est devenue un pattern propre pour charger des secrets depuis des fichiers montés dans des déploiements conteneurisé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">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env(SECRETS_FILE)</span>: <span style="color:#e6db74">&#39;/run/secrets/app.json&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">app.secrets</span>: <span style="color:#e6db74">&#39;%env(json:file:SECRETS_FILE)%&#39;</span>
</span></span></code></pre></div><p>Vous pouvez aussi écrire des processeurs personnalisés en implémentant <code>EnvVarProcessorInterface</code> et en taguant le service. Ça ressemble à de la sur-ingénierie jusqu&rsquo;au jour où vous en avez besoin.</p>
<h2 id="des-services-taggués-sans-boilerplate">Des services taggués sans boilerplate</h2>
<p>Avant 4.0, rassembler tous les services portant un tag donné dans un service signifiait écrire un compiler pass. Quarante lignes de PHP pour dire &ldquo;donne-moi tout ce qui est tagué <code>app.handler</code>.&rdquo;</p>
<p>3.4 a introduit le raccourci YAML <code>!tagged</code>, et 4.0 l&rsquo;emporte avec lui :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\HandlerCollection</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">arguments</span>: [!<span style="color:#ae81ff">tagged app.handler]</span>
</span></span></code></pre></div><p>La collection est lazy par défaut quand elle est type-hintée en <code>iterable</code>, donc les services ne sont pas instanciés tant qu&rsquo;on n&rsquo;itère pas dessus. Ça a remplacé toute une catégorie de compiler passes qui n&rsquo;existaient que pour construire des listes.</p>
<h2 id="php-comme-format-de-configuration">PHP comme format de configuration</h2>
<p>YAML est la valeur par défaut depuis si longtemps que ça semble obligatoire. Ce n&rsquo;est pas le cas. 4.0 embarque une configuration en PHP via une interface fluent :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// config/services.php
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">ContainerConfigurator</span> $container) {
</span></span><span style="display:flex;"><span>    $services <span style="color:#f92672">=</span> $container<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">services</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">defaults</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">autowire</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">autoconfigure</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $services<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">load</span>(<span style="color:#e6db74">&#39;App\\&#39;</span>, <span style="color:#e6db74">&#39;../src/&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">exclude</span>(<span style="color:#e6db74">&#39;../src/{Entity,Repository}&#39;</span>);
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>La même approche fonctionne pour les routes. L&rsquo;avantage pratique : autocomplétion de l&rsquo;IDE, vérification des types, et vraie logique PHP dans la configuration sans la syntaxe d&rsquo;interpolation <code>%</code>. YAML n&rsquo;est pas près de disparaître, mais maintenant vous avez le choix.</p>
<h2 id="argon2i-parce-que-bcrypt-vieillissait-déjà">Argon2i, parce que bcrypt vieillissait déjà</h2>
<p>Symfony 3.4/4.0 a ajouté le support d&rsquo;Argon2i, vainqueur du Password Hashing Competition 2015. La configuration tient en une ligne :</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">security</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">encoders</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">App\Entity\User</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">algorithm</span>: <span style="color:#ae81ff">argon2i</span>
</span></span></code></pre></div><p>Argon2i est intégré à PHP 7.2+ et disponible via l&rsquo;extension sodium sur les versions antérieures. Comme bcrypt, il se sale lui-même, inutile de gérer des colonnes de sel. Contrairement à bcrypt, il est conçu pour résister aux attaques par GPU avec une utilisation mémoire configurable. Si vous démarrez un nouveau projet sur 4.0, il n&rsquo;y a vraiment aucune raison de choisir bcrypt.</p>
<h2 id="la-couche-formulaire-reçoit-un-thème-bootstrap-4">La couche formulaire reçoit un thème Bootstrap 4</h2>
<p>Le thème de formulaire Bootstrap 3 existant remonte à Symfony 2.x. Bootstrap 4 arrive comme option de premier ordre en 4.0 :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">twig</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">form_themes</span>: [<span style="color:#e6db74">&#39;bootstrap_4_layout.html.twig&#39;</span>]
</span></span></code></pre></div><p>Plus utile en pratique : les types d&rsquo;input HTML5 <code>tel</code> et <code>color</code> sont désormais disponibles comme types de formulaire <code>TelType</code> et <code>ColorType</code>. Avant, il fallait écrire des types personnalisés ou surcharger des widgets bruts pour ça.</p>
<h2 id="binding-de-service-local">Binding de service local</h2>
<p>Les bindings <code>_defaults</code> globaux s&rsquo;appliquent à tous les services. Parfois on a besoin d&rsquo;un binding limité à une classe ou un namespace spécifique, comme des instances de logger différentes pour des sous-systèmes différents.</p>
<p>4.0 supporte <code>bind</code> par service exactement pour ça :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\Service\OrderService</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</span>: <span style="color:#e6db74">&#39;@monolog.logger.orders&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\Service\PaymentService</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</span>: <span style="color:#e6db74">&#39;@monolog.logger.payments&#39;</span>
</span></span></code></pre></div><p>Même interface, deux implémentations différentes, pas de factory, pas de configuration supplémentaire. Petite fonctionnalité, mais elle élimine toute une catégorie de bidouilles bancales.</p>
]]></content:encoded></item><item><title>Symfony 3.4 LTS : le pont qu'on a vraiment envie de traverser</title><link>https://guillaumedelre.github.io/fr/2018/01/12/symfony-3.4-lts-le-pont-quon-a-vraiment-envie-de-traverser/</link><pubDate>Fri, 12 Jan 2018 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2018/01/12/symfony-3.4-lts-le-pont-quon-a-vraiment-envie-de-traverser/</guid><description>Part 2 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 3.4 LTS est le pont de migration : mêmes fonctionnalités que 3.3 plus chaque avertissement de dépréciation que 4.0 va rendre obligatoire.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 3.4 et 4.0 sont sortis le même jour : le 30 novembre 2017. Ce n&rsquo;est pas une coïncidence, c&rsquo;est la stratégie.</p>
<p>3.4 n&rsquo;est pas une version de fonctionnalités. Elle livre exactement les mêmes fonctionnalités que 3.3, plus chaque avertissement de dépréciation que 4.0 va rendre obligatoire. Son seul objectif est d&rsquo;être l&rsquo;outil de migration : monter de 3.3 à 3.4, corriger ce qui apparaît dans les logs, puis passer à 4.0 proprement.</p>
<h2 id="pourquoi-les-versions-lts-comptent-dans-le-modèle-symfony">Pourquoi les versions LTS comptent dans le modèle Symfony</h2>
<p>Symfony publie une nouvelle version mineure tous les six mois. Ce rythme serait brutal pour les applications en production, donc le projet désigne chaque quatrième mineure comme LTS : trois ans de corrections de bugs, quatre de correctifs de sécurité. Ce qui signifie que les équipes peuvent cibler 3.4 et arrêter de penser aux mises à jour pendant un moment.</p>
<p>3.4 est la dernière LTS de la ligne 3.x. Si on est encore sur 2.x ou un 3.x ancien, c&rsquo;est la zone d&rsquo;atterrissage.</p>
<h2 id="la-couche-de-dépréciations">La couche de dépréciations</h2>
<p>Chaque fonctionnalité supprimée par 4.0 est dépréciée dans 3.4. Faire tourner son application sur 3.4 avec les notices de dépréciation activées transforme les logs en une liste de tâches. Les plus courantes :</p>
<ul>
<li>Les services sans visibilité explicite (public/private) génèrent des warnings — 4.0 rend tous les services privés par défaut</li>
<li><code>ControllerTrait</code> est déprécié au profit de <code>AbstractController</code></li>
<li>Les anciennes interfaces d&rsquo;authentificateur de sécurité sont marquées pour suppression</li>
<li>La configuration de services YAML seule sans annotations d&rsquo;autowiring déclenche des warnings</li>
</ul>
<p>Le workflow prévu : monter sur 3.4, faire tourner la suite de tests avec les notices de dépréciation comme erreurs (<code>SYMFONY_DEPRECATIONS_HELPER=max[self]=0</code> dans PHPUnit), corriger tout ce qui échoue. Après ça, la montée vers 4.0 est essentiellement mécanique.</p>
<h2 id="la-fenêtre-de-support">La fenêtre de support</h2>
<p>3.4 LTS reçoit des corrections de bugs jusqu&rsquo;en novembre 2020 et des correctifs de sécurité jusqu&rsquo;en novembre 2021. C&rsquo;est une marge confortable pour les applications qui ne peuvent pas suivre chaque version. Le coût : rester sur l&rsquo;architecture 3.x, sans Flex, sans structure micro-framework, sans autowiring zéro-config par défaut.</p>
<p>Le pont est là. Savoir si et quand on le traverse est une décision business, pas technique.</p>
<h2 id="les-services-passent-privés">Les services passent privés</h2>
<p>3.4 a inversé la visibilité par défaut des services de public à privé. Avant, <code>$container-&gt;get('app.my_service')</code> était du code parfaitement normal. Après, c&rsquo;est un anti-pattern qui génère un warning de dépréciation dans 3.4 et casse complètement dans 4.0.</p>
<p>La raison est simple : récupérer des services directement depuis le conteneur masque les dépendances et déjoue l&rsquo;analyse statique. En injectant via le constructeur, le conteneur peut optimiser le graphe, supprimer les services inutilisés, et détecter les erreurs à la compilation. En les récupérant à l&rsquo;exécution, il ne peut pas.</p>
<p>Pour les applications qui utilisent déjà l&rsquo;autowiring, la migration est généralement légère. Le point délicat ce sont les contrôleurs qui étendent <code>Controller</code> et appellent <code>$this-&gt;get('quelque-chose')</code>. La correction consiste à passer à <code>AbstractController</code>, qui fournit les mêmes raccourcis mais via des service locators paresseux plutôt que l&rsquo;accès direct au conteneur.</p>
<p>Pour les services qui ont vraiment besoin d&rsquo;être publics (accédés depuis du code legacy ou des tests fonctionnels), les marquer explicitement :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\Service\LegacyAdapter</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">public</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><h2 id="lier-les-arguments-scalaires-une-seule-fois">Lier les arguments scalaires une seule fois</h2>
<p>Un point de friction classique avec l&rsquo;autowiring : les arguments de constructeur scalaires. Si dix services ont tous besoin de <code>$projectDir</code>, il fallait configurer chacun individuellement. La clé <code>bind</code> sous <code>_defaults</code> règle ça :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">_defaults</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">autowire</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">autoconfigure</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">bind</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">$projectDir</span>: <span style="color:#e6db74">&#39;%kernel.project_dir%&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">$mailerDsn</span>: <span style="color:#e6db74">&#39;%env(MAILER_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">Psr\Log\LoggerInterface $auditLogger</span>: <span style="color:#e6db74">&#39;@monolog.logger.audit&#39;</span>
</span></span></code></pre></div><p>Tout service avec un paramètre de constructeur nommé <code>$projectDir</code> reçoit la valeur liée automatiquement. On peut aussi lier par type-hint, ce qui gère le cas courant où plusieurs canaux de logger existent et on en a besoin d&rsquo;un spécifique. Les liaisons dans <code>_defaults</code> s&rsquo;appliquent à tous les services du fichier ; on peut surcharger par service si nécessaire.</p>
<h2 id="injecter-les-services-taggués-sans-compiler-pass">Injecter les services taggués sans compiler pass</h2>
<p>Avant 3.4, collecter tous les services avec un tag donné nécessitait d&rsquo;écrire un compiler pass. Il y a maintenant un raccourci 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:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\Chain\TransformerChain</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">arguments</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">$transformers</span>: !<span style="color:#ae81ff">tagged app.transformer</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TransformerChain</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">iterable</span> $transformers) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La notation <code>!tagged</code> crée un <code>IteratorArgument</code> : les services sont instanciés paresseusement au fil de l&rsquo;itération, donc les transformers non utilisés ne sont jamais construits. Pour l&rsquo;ordonnancement, ajouter un attribut <code>priority</code> à la définition du tag sur chaque service.</p>
<h2 id="un-logger-livré-avec-le-framework">Un logger livré avec le framework</h2>
<p>Pas de Monolog ? Pas de problème. Symfony 3.4 inclut un logger PSR-3 qui écrit sur <code>php://stderr</code> par défaut. On l&rsquo;injecte avec <code>Psr\Log\LoggerInterface</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Psr\Log\LoggerInterface</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MyService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">LoggerInterface</span> $logger) {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">doSomething</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">logger</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">warning</span>(<span style="color:#e6db74">&#39;Quelque chose de douteux s\&#39;est produit&#39;</span>, [<span style="color:#e6db74">&#39;context&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;ici&#39;</span>]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le niveau minimum par défaut est <code>warning</code>. La cible est les workloads container et Kubernetes où stderr est le puits de logs naturel. C&rsquo;est délibérément minimal : pas de handlers, pas de processors, pas de channels. Quand on en a besoin, on installe Monolog.</p>
<h2 id="les-guard-authenticators-ont-reçu-une-méthode-supports">Les Guard authenticators ont reçu une méthode supports()</h2>
<p>La méthode <code>getCredentials()</code> du composant Guard jouait un double rôle : décider si l&rsquo;authentificateur devait gérer la requête, et extraire les credentials. Retourner <code>null</code> était le signal pour passer. Ça rendait le contrat confus.</p>
<p>3.4 a ajouté <code>supports()</code> pour séparer ces responsabilités :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ApiTokenAuthenticator</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractGuardAuthenticator</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">supports</span>(<span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#a6e22e">bool</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">headers</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">has</span>(<span style="color:#e6db74">&#39;X-API-TOKEN&#39;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getCredentials</span>(<span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#66d9ef">array</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// N&#39;est appelé que quand supports() retourne true.
</span></span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Doit toujours retourner des credentials maintenant.
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> [<span style="color:#e6db74">&#39;token&#39;</span> <span style="color:#f92672">=&gt;</span> $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">headers</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;X-API-TOKEN&#39;</span>)];
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;ancienne <code>GuardAuthenticatorInterface</code> est dépréciée. L&rsquo;avantage pratique : les classes de base peuvent implémenter la logique partagée <code>getUser()</code> et <code>checkCredentials()</code>, tandis que les sous-classes ne surchargent que <code>supports()</code> et <code>getCredentials()</code>. Une responsabilité chacune.</p>
<h2 id="deux-nouvelles-commandes-de-debug">Deux nouvelles commandes de debug</h2>
<p><code>debug:autowiring</code> remplace l&rsquo;ancien <code>debug:container --types</code> pour découvrir quels type-hints fonctionnent avec l&rsquo;autowiring :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ bin/console debug:autowiring log
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Autowirable Services
</span></span><span style="display:flex;"><span><span style="color:#f92672">====================</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  Psr<span style="color:#ae81ff">\L</span>og<span style="color:#ae81ff">\L</span>oggerInterface
</span></span><span style="display:flex;"><span>      alias to monolog.logger
</span></span><span style="display:flex;"><span>  Psr<span style="color:#ae81ff">\L</span>og<span style="color:#ae81ff">\L</span>oggerInterface $auditLogger
</span></span><span style="display:flex;"><span>      alias to monolog.logger.audit
</span></span></code></pre></div><p>Passer un mot-clé pour filtrer. Fini de deviner si c&rsquo;est <code>LoggerInterface</code> ou <code>Logger</code>.</p>
<p><code>debug:form</code> donne la même capacité d&rsquo;introspection pour les types de formulaires :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ bin/console debug:form App<span style="color:#ae81ff">\F</span>orm<span style="color:#ae81ff">\O</span>rderType label_attr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Option: label_attr
</span></span><span style="display:flex;"><span>  Required: false
</span></span><span style="display:flex;"><span>  Default: <span style="color:#f92672">[]</span>
</span></span><span style="display:flex;"><span>  Allowed types: array
</span></span></code></pre></div><p>Sans arguments, il liste tous les types de formulaires enregistrés, extensions et guessers. Avec un nom de type et un nom d&rsquo;option, il montre toutes les contraintes sur cette option. Avant ça, on lisait le source ou on tâtonnait.</p>
<h2 id="les-sessions-sont-devenues-plus-strictes-par-défaut">Les sessions sont devenues plus strictes par défaut</h2>
<p>3.4 implémente <code>SessionUpdateTimestampHandlerInterface</code> de PHP 7.0, ce qui apporte deux choses : les écritures de session paresseuses (écrites seulement quand les données ont vraiment changé) et la validation stricte des ID de session (les IDs qui n&rsquo;existent pas dans le store sont rejetés plutôt que créés silencieusement, ce qui bloque une classe d&rsquo;attaques de fixation de session).</p>
<p>Les anciennes classes <code>WriteCheckSessionHandler</code>, <code>NativeSessionHandler</code> et <code>NativeProxy</code> sont dépréciées. Le <code>MemcacheSessionHandler</code> (note : pas Memcached) est supprimé, puisque l&rsquo;extension PECL sous-jacente a arrêté de recevoir des mises à jour pour PHP 7.</p>
<h2 id="les-thèmes-de-formulaires-twig-peuvent-maintenant-être-scopés">Les thèmes de formulaires Twig peuvent maintenant être scopés</h2>
<p>Les thèmes de formulaires globaux s&rsquo;appliquent à tous les formulaires dans l&rsquo;application. Si un formulaire a besoin d&rsquo;un look complètement différent, il n&rsquo;y avait pas de moyen propre de se désinscrire. Le mot-clé <code>only</code> gère ça :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-twig" data-lang="twig"><span style="display:flex;"><span><span style="color:#75715e">{%</span> <span style="color:#66d9ef">raw</span> <span style="color:#75715e">%}</span>{% form_theme orderForm with [&#39;form/order_layout.html.twig&#39;] only %}<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endraw</span> <span style="color:#75715e">%}</span>
</span></span></code></pre></div><p>Le mot-clé <code>only</code> désactive tous les thèmes globaux pour ce formulaire, y compris le <code>form_div_layout.html.twig</code> de base. Le thème personnalisé doit alors soit fournir tous les blocs qu&rsquo;il utilise, soit les importer explicitement avec <code>{% raw %}{% use 'form_div_layout.html.twig' %}{% endraw %}</code>.</p>
<h2 id="surcharger-les-templates-de-bundle-sans-boucles-infinies">Surcharger les templates de bundle sans boucles infinies</h2>
<p>Surcharger un template de bundle qu&rsquo;on avait aussi besoin d&rsquo;étendre causait autrefois une erreur de référence circulaire. Surcharger <code>@TwigBundle/Exception/error404.html.twig</code> et essayer aussi d&rsquo;en hériter ? L&rsquo;ancienne résolution de namespace suivait la surcharge et bouclait indéfiniment.</p>
<p>3.4 a introduit le préfixe <code>@!</code> pour référencer explicitement le template de bundle original, en contournant toutes les surcharges :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-twig" data-lang="twig"><span style="display:flex;"><span><span style="color:#75715e">{%</span> <span style="color:#66d9ef">raw</span> <span style="color:#75715e">%}</span>{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
</span></span><span style="display:flex;"><span>{% extends &#39;@!Twig/Exception/error404.html.twig&#39; %}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{% block title %}Page non trouvée{% endblock %}<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endraw</span> <span style="color:#75715e">%}</span>
</span></span></code></pre></div><p><code>@TwigBundle</code> résout vers la surcharge si elle existe. <code>@!TwigBundle</code> résout toujours vers l&rsquo;original. Surcharger-et-étendre, sans les acrobaties.</p>
]]></content:encoded></item><item><title>Symfony 3.3 : quand les services ont arrêté d'être un cauchemar de configuration</title><link>https://guillaumedelre.github.io/fr/2017/07/13/symfony-3.3-quand-les-services-ont-arr%C3%AAt%C3%A9-d%C3%AAtre-un-cauchemar-de-configuration/</link><pubDate>Thu, 13 Jul 2017 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2017/07/13/symfony-3.3-quand-les-services-ont-arr%C3%AAt%C3%A9-d%C3%AAtre-un-cauchemar-de-configuration/</guid><description>Part 1 of 11 in &amp;quot;Sorties Symfony&amp;quot;: Symfony 3.3 a rendu l&amp;#39;autowiring par défaut et transformé la configuration des services — des montagnes de YAML en presque rien.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 3.3 est sorti le 29 mai. C&rsquo;est la version qui a changé ma façon de penser la configuration des services. Avec le recul, c&rsquo;était une prévisualisation de ce que 4.0 allait adopter comme nouveau standard.</p>
<h2 id="le-problème-de-lautowiring">Le problème de l&rsquo;autowiring</h2>
<p>Avant 3.3, le DI de Symfony était puissant mais verbeux. Chaque service devait être déclaré explicitement dans <code>services.yml</code> avec ses arguments listés. L&rsquo;autowiring existait depuis 3.1, mais il était opt-in par service et avait assez de cas limites pour vous mordre. Les équipes écrivaient soit des montagnes de YAML, soit s&rsquo;appuyaient sur des bundles tiers pour réduire le bruit.</p>
<p>3.3 a réécrit les defaults. Avec <code>autoconfigure: true</code> et <code>autowire: true</code> définis une seule fois dans la section defaults, chaque classe dans <code>src/</code> devient automatiquement un service, et ses dépendances de constructeur sont résolues par type. Ce qui prenait vingt lignes de YAML ne prend maintenant plus rien :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">_defaults</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">autowire</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">autoconfigure</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">resource</span>: <span style="color:#e6db74">&#39;../src/&#39;</span>
</span></span></code></pre></div><p>Ce bloc unique est toute la configuration de services pour la plupart des applications. Le framework découvre les services, injecte les dépendances, et applique les tags (command, event subscriber, voter&hellip;) en fonction des interfaces que chaque classe implémente.</p>
<h2 id="les-conditionnels-instanceof">Les conditionnels instanceof</h2>
<p>Le mot-clé <code>instanceof</code> dans la configuration des services gère le tagging qui nécessitait auparavant une déclaration explicite :</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">_instanceof</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">Symfony\Component\EventDispatcher\EventSubscriberInterface</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">tags</span>: [<span style="color:#e6db74">&#39;kernel.event_subscriber&#39;</span>]
</span></span></code></pre></div><p>Tout service implémentant <code>EventSubscriberInterface</code> reçoit le tag automatiquement. Même chose pour <code>Command</code>, <code>Voter</code>, <code>MessageHandlerInterface</code>. Le boilerplate s&rsquo;évapore.</p>
<h2 id="le-composant-dotenv">Le composant Dotenv</h2>
<p>Avant 3.3, Symfony n&rsquo;avait aucun moyen natif de charger des fichiers <code>.env</code>. La réponse standard était un package tiers. Le nouveau composant <code>Dotenv</code> lit <code>.env</code> et peuple <code>$_ENV</code> et <code>$_SERVER</code>, faisant de la configuration basée sur l&rsquo;environnement un citoyen de première classe enfin.</p>
<h2 id="découverte-des-services-depuis-le-filesystem">Découverte des services depuis le filesystem</h2>
<p>L&rsquo;option <code>resource</code> rassemble tout. Au lieu d&rsquo;enregistrer chaque classe individuellement, on pointe le conteneur vers un répertoire et il scanne les classes PSR-4 :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">resource</span>: <span style="color:#e6db74">&#39;../src/&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exclude</span>: <span style="color:#e6db74">&#39;../src/{Entity,Migrations}&#39;</span>
</span></span></code></pre></div><p>Chaque classe trouvée devient un service avec son FQCN comme service ID. L&rsquo;option <code>exclude</code> gère les cas comme les entités Doctrine qu&rsquo;on ne veut pas que le conteneur touche. Et non, ce n&rsquo;est pas de la magie : c&rsquo;est un scan filesystem à la compilation, donc le coût est payé une fois pendant le cache warmup, pas par requête.</p>
<h2 id="quand-on-a-besoin-dun-sous-ensemble-du-conteneur">Quand on a besoin d&rsquo;un sous-ensemble du conteneur</h2>
<p>Les service locators résolvent une tension spécifique : certains services ont légitimement besoin d&rsquo;accéder de manière paresseuse à un ensemble variable d&rsquo;autres services, mais injecter le conteneur entier est un anti-pattern — ça masque les dépendances et déjoue l&rsquo;analyse statique. La solution est un locator qui déclare explicitement ce qu&rsquo;il contient.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\Handler\HandlerLocator</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">class</span>: <span style="color:#ae81ff">Symfony\Component\DependencyInjection\ServiceLocator</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">tags</span>: [<span style="color:#e6db74">&#39;container.service_locator&#39;</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">arguments</span>:
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">App\Command\CreateOrder</span>: <span style="color:#e6db74">&#39;@App\Handler\CreateOrderHandler&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">App\Command\CancelOrder</span>: <span style="color:#e6db74">&#39;@App\Handler\CancelOrderHandler&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\Bus\CommandBus</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">arguments</span>: [<span style="color:#e6db74">&#39;@App\Handler\HandlerLocator&#39;</span>]
</span></span></code></pre></div><p>Le locator implémente <code>ContainerInterface</code> de PSR-11, donc la classe réceptrice type-hinte contre <code>Psr\Container\ContainerInterface</code>. Les services à l&rsquo;intérieur sont instanciés de manière paresseuse : si un handler donné n&rsquo;est jamais appelé pendant une requête, il n&rsquo;est jamais construit.</p>
<p>Et à propos de PSR-11 : Symfony 3.3 a fait implémenter ce standard à son conteneur. Ce qui signifie que toute bibliothèque attendant un conteneur PSR-11 fonctionne maintenant directement avec le conteneur Symfony, sans adaptateur.</p>
<h2 id="le-routing-est-devenu-plus-rapide">Le routing est devenu plus rapide</h2>
<p>Le composant de routing a réécrit comment il génère les fichiers dump. Dans une application avec 900 routes, la correspondance d&rsquo;URL est passée de 7,5 ms à 2,5 ms par correspondance : une réduction de 66%. Les optimisations vivent dans la sortie compilée, pas dans le chemin d&rsquo;exécution, donc les définitions de routes existantes en bénéficient automatiquement après un cache clear.</p>
<h2 id="trouver-la-racine-du-projet-sans-compter-les-séparateurs-de-répertoires">Trouver la racine du projet sans compter les séparateurs de répertoires</h2>
<p>Avant 3.3, obtenir la racine du projet nécessitait le pattern délicieusement maladroit <code>%kernel.root_dir%/../</code>, parce que <code>getRootDir()</code> pointait vers le répertoire <code>app/</code>. La nouvelle méthode <code>getProjectDir()</code> remonte depuis le fichier kernel jusqu&rsquo;à trouver <code>composer.json</code> et retourne ce répertoire.</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>$path <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getParameter</span>(<span style="color:#e6db74">&#39;kernel.root_dir&#39;</span>) <span style="color:#f92672">.</span> <span style="color:#e6db74">&#39;/../var/data.db&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Après
</span></span></span><span style="display:flex;"><span>$path <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getParameter</span>(<span style="color:#e6db74">&#39;kernel.project_dir&#39;</span>) <span style="color:#f92672">.</span> <span style="color:#e6db74">&#39;/var/data.db&#39;</span>;
</span></span></code></pre></div><p>Le paramètre correspondant est <code>%kernel.project_dir%</code>. Si vous déployez sans <code>composer.json</code>, vous pouvez surcharger la méthode dans votre classe kernel et retourner n&rsquo;importe quel chemin qui fait sens.</p>
<h2 id="les-messages-flash-sans-toucher-lobjet-session">Les messages flash sans toucher l&rsquo;objet session</h2>
<p>L&rsquo;ancienne façon d&rsquo;itérer les messages flash dans Twig nécessitait de passer par <code>app.session.flashbag</code>, ce qui forçait aussi le démarrage de la session qu&rsquo;il y ait des messages ou non. Le nouveau helper <code>app.flashes</code> évite 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-twig" data-lang="twig"><span style="display:flex;"><span><span style="color:#75715e">{%</span> <span style="color:#66d9ef">raw</span> <span style="color:#75715e">%}</span>{% for label, messages in app.flashes %}
</span></span><span style="display:flex;"><span>    {% for message in messages %}
</span></span><span style="display:flex;"><span>        &lt;div class=&#34;flash-{{ label }}&#34;&gt;{{ message }}&lt;/div&gt;
</span></span><span style="display:flex;"><span>    {% endfor %}
</span></span><span style="display:flex;"><span>{% endfor %}<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endraw</span> <span style="color:#75715e">%}</span>
</span></span></code></pre></div><p>S&rsquo;il n&rsquo;y a pas de messages flash, la session ne démarre jamais. On peut aussi filtrer par type : <code>app.flashes('error')</code> ne retourne que les messages d&rsquo;erreur.</p>
<h2 id="la-commande-encode-password-est-devenue-intelligente">La commande encode-password est devenue intelligente</h2>
<p>La commande console <code>security:encode-password</code> est devenue plus futée. Au lieu d&rsquo;exiger qu&rsquo;on passe la classe utilisateur en argument, elle liste maintenant les classes utilisateur configurées et laisse choisir :</p>
<pre tabindex="0"><code>$ bin/console security:encode-password

  For which user class would you like to encode a password?
  [0] App\Entity\User
  [1] App\Entity\AdminUser
</code></pre><p>Elle normalise aussi la configuration des encodeurs pour gérer les cas limites avec des noms d&rsquo;utilisateur au format email que la version précédente corrompait silencieusement en remplaçant <code>@</code> par des underscores. Beau rattrapage.</p>
<h2 id="http2-push-et-resource-hints">HTTP/2 push et resource hints</h2>
<p>Le composant WebLink gère l&rsquo;en-tête HTTP <code>Link</code>, qui dit aux navigateurs (et aux proxies HTTP/2) de précharger, prérécupérer ou se préconnecter à des ressources avant même que la page les demande. Il se présente sous forme de fonctions Twig :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-twig" data-lang="twig"><span style="display:flex;"><span><span style="color:#75715e">{%</span> <span style="color:#66d9ef">raw</span> <span style="color:#75715e">%}</span>{{ preload(&#39;/fonts/custom.woff2&#39;, { as: &#39;font&#39;, crossorigin: true }) }}
</span></span><span style="display:flex;"><span>{{ prefetch(&#39;/api/next-page-data.json&#39;) }}
</span></span><span style="display:flex;"><span>{{ dns_prefetch(&#39;https://fonts.googleapis.com&#39;) }}<span style="color:#75715e">{%</span> <span style="color:#66d9ef">endraw</span> <span style="color:#75715e">%}</span>
</span></span></code></pre></div><p>Chaque appel ajoute un en-tête <code>Link</code> correspondant à la réponse. Pour les applications derrière un proxy capable HTTP/2, ça peut déclencher un server push avant même que le navigateur ait parsé le HTML. On l&rsquo;active dans <code>config.yml</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">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">web_link</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><h2 id="des-dépréciations-auxquelles-on-peut-vraiment-faire-confiance">Des dépréciations auxquelles on peut vraiment faire confiance</h2>
<p>La compilation du conteneur générait des avertissements de dépréciation qui disparaissaient au prochain chargement de page parce que le conteneur mis en cache était déjà construit. 3.3 persiste ces messages sur disque et les affiche dans la barre de débogage web aux côtés des dépréciations de la phase de requête. Si une classe est dépréciée pendant la compilation des services, vous le verrez sans avoir à vider le cache d&rsquo;abord.</p>
<h2 id="ce-que-ça-a-signifié-pour-40">Ce que ça a signifié pour 4.0</h2>
<p>Les defaults d&rsquo;autowiring de 3.3 sont exactement ce que Symfony 4.0 a adopté comme nouvelle structure de projet standard. Le <code>services.yaml</code> de chaque nouveau projet Symfony 4 est essentiellement le snippet ci-dessus. Si on avait déjà assimilé ce que 3.3 introduisait, le &ldquo;nouveau mode&rdquo; de 4.0 semblait familier plutôt qu&rsquo;étranger.</p>
<p>La direction était claire : moins de configuration, plus de convention. Laisser PHP comprendre ce qu&rsquo;il faut câbler ensemble.</p>
]]></content:encoded></item></channel></rss>