<?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>Développement on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/categories/d%C3%A9veloppement/</link><description>Recent content in Développement on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Sun, 17 May 2026 15:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/categories/d%C3%A9veloppement/index.xml" rel="self" type="application/rss+xml"/><item><title>Onze sur douze</title><link>https://guillaumedelre.github.io/fr/2026/05/17/onze-sur-douze/</link><pubDate>Sun, 17 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/17/onze-sur-douze/</guid><description>Part 8 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Onze facteurs résolus proprement. Le douzième : des migrations Doctrine dans l&amp;#39;entrypoint, en attente d&amp;#39;une question de gouvernance que le code seul ne peut pas trancher.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Le <code>composer.json</code> de chaque service avait ça dans sa section <code>post-install-cmd</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-json" data-lang="json"><span style="display:flex;"><span><span style="color:#e6db74">&#34;post-install-cmd&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;bin/console cache:clear --env=prod&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;bin/console doctrine:migrations:migrate --no-interaction&#34;</span>
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p><code>post-install-cmd</code> s&rsquo;exécute pendant <code>composer install</code>, qui dans le Dockerfile de production tourne au moment du build de l&rsquo;image. Il n&rsquo;y a pas de base de données disponible pendant un build Docker. La commande de migration échouait silencieusement, se connectait à rien, ou était ignorée par Doctrine faute de schéma à comparer. Dans tous les cas, elle ne migrait rien.</p>
<p>C&rsquo;est une violation nette du <a href="https://12factor.net/admin-processes" target="_blank" rel="noopener noreferrer">Facteur XII</a>
 : les processus d&rsquo;administration — migrations, scripts ponctuels, commandes console — doivent s&rsquo;exécuter dans le même environnement que l&rsquo;application, contre les vraies données de production. Les faire tourner au build inverse la relation. L&rsquo;image ne devrait pas savoir qu&rsquo;il y a une base de données. La base devrait être là quand l&rsquo;image en a besoin.</p>
<h2 id="le-déplacement-vers-lentrypoint">Le déplacement vers l&rsquo;entrypoint</h2>
<p>La commande de migration a quitté <code>composer.json</code> pour <code>docker-entrypoint.sh</code>. Le changement semble petit dans un diff. Les implications ne le sont pas.</p>
<p>L&rsquo;entrypoint s&rsquo;exécute quand le container démarre, pas quand l&rsquo;image est construite. La base de données est accessible. L&rsquo;entrypoint l&rsquo;attend — jusqu&rsquo;à 60 secondes, une tentative par seconde — avant de faire quoi que ce soit :</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-sh" data-lang="sh"><span style="display:flex;"><span>ATTEMPTS_LEFT_TO_REACH_DATABASE<span style="color:#f92672">=</span><span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">until</span> <span style="color:#f92672">[</span> $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  DATABASE_ERROR<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>php bin/console dbal:run-sql -q <span style="color:#e6db74">&#34;SELECT 1&#34;</span> 2&gt;&amp;1<span style="color:#66d9ef">)</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    sleep <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    ATTEMPTS_LEFT_TO_REACH_DATABASE<span style="color:#f92672">=</span><span style="color:#66d9ef">$((</span>ATTEMPTS_LEFT_TO_REACH_DATABASE <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span><span style="color:#66d9ef">))</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</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> $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;</span>$DATABASE_ERROR<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><p>Si la base ne répond pas dans les 60 secondes, le container sort en erreur et Kubernetes le redémarre. Une fois la base prête, la migration tourne :</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-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span> find ./migrations -iname <span style="color:#e6db74">&#39;*.php&#39;</span> -print -quit <span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><p>Deux changements par rapport à la commande d&rsquo;origine : <code>--all-or-nothing</code> garantit que si une migration dans un batch échoue, le batch entier est annulé. Et le guard <code>find</code> passe la commande si aucun fichier de migration n&rsquo;existe — utile pour les services qui n&rsquo;utilisent pas les migrations Doctrine.</p>
<p>C&rsquo;est franchement mieux. La base est là. La migration tourne dans le vrai environnement. Le flag <code>--all-or-nothing</code> apporte une atomicité que la version au build n&rsquo;avait jamais eue.</p>
<h2 id="ce-que-ça-ne-résout-pas">Ce que ça ne résout pas</h2>
<p>Deux pods qui redéploient simultanément exécutent tous les deux l&rsquo;entrypoint. Tous les deux atteignent la base. Tous les deux trouvent des migrations en attente. Tous les deux appellent <code>doctrine:migrations:migrate</code>.</p>
<p>Doctrine a un mécanisme de verrouillage : une table <code>doctrine_migration_versions</code> qui enregistre quelles migrations ont tourné, et la commande la consulte avant d&rsquo;appliquer quoi que ce soit. Dans les conditions normales c&rsquo;est suffisant : le deuxième pod trouve la table à jour et sort proprement. Les cas de défaillance réels sont plus précis : une migration assez longue pour dépasser le timeout du verrou de base de données, ce qui laisse un deuxième runner démarrer la même migration avant que le premier ait terminé ; ou un pod qui se vautre à mi-migration avant d&rsquo;avoir enregistré la version dans la table, laissant le schéma dans un état appliqué-mais-non-enregistré que le pod suivant va tenter d&rsquo;appliquer à nouveau.</p>
<p>La position de l&rsquo;équipe est explicite : un downtime léger au déploiement est acceptable. Les versions d&rsquo;application ne sont pas nécessairement compatibles avec des versions de schéma plus anciennes, donc faire tourner N et N+1 simultanément contre la même base ne serait de toute façon pas sûr. La stratégie de déploiement est Recreate : tous les anciens pods sont terminés avant que les nouveaux ne démarrent. La migration tourne au premier démarrage, sans chevauchement entre les versions. Ça fonctionne.</p>
<p>Mais &ldquo;ça fonctionne&rdquo; et &ldquo;c&rsquo;est la bonne architecture&rdquo; sont deux réponses différentes.</p>
<h2 id="ce-que-feraient-les-alternatives">Ce que feraient les alternatives</h2>
<p>Le <a href="https://12factor.net/admin-processes" target="_blank" rel="noopener noreferrer">Facteur XII</a>
 dit que les processus d&rsquo;administration doivent tourner dans des &ldquo;processus ponctuels&rdquo;. Un processus qui tourne une fois, dans un but précis, contre l&rsquo;environnement de production. L&rsquo;entrypoint n&rsquo;est pas ponctuel — il tourne à chaque démarrage de container, y compris les redémarrages, les événements de scaling, et les déplacements de pods par Kubernetes.</p>
<p>Trois alternatives existent, chacune avec une réponse différente à la question de la propriété :</p>
<p><strong>Un init container Kubernetes</strong> tourne avant le container principal, dans le même pod. Il peut exécuter la migration, sortir, et laisser le container principal démarrer seulement après son succès. La migration est isolée du runtime applicatif. Le problème : l&rsquo;init container est une image supplémentaire à construire et maintenir, et il tourne à chaque démarrage de pod — une plateforme de 14 services démarrant simultanément a toujours une race potentielle.</p>
<p><strong>Un Kubernetes Job</strong> tourne une fois, à la demande ou déclenché par le pipeline de déploiement. Il peut être configuré pour s&rsquo;exécuter avant la mise à jour des pods — séquentiel, isolé, avec un signal clair de succès ou d&rsquo;échec. La race condition disparaît. La complexité se déplace vers le processus de déploiement : le Job doit se terminer avant que le rollout du Deployment commence, et le pipeline CI doit coordonner les deux.</p>
<p><strong>Un hook Helm</strong> est le même concept exprimé de façon déclarative dans le chart Helm. Un hook <code>pre-upgrade</code> exécute la migration avant la mise à jour des pods applicatifs. C&rsquo;est la réponse la plus idiomatique pour Kubernetes. Ça signifie aussi que le chart Helm est désormais responsable de l&rsquo;exécution des migrations — une décision qui appartient à qui possède le chart.</p>
<p>Cette dernière phrase explique pourquoi l&rsquo;entrypoint n&rsquo;a pas changé. Déplacer les migrations hors de l&rsquo;application signifie décider que l&rsquo;infrastructure de déploiement — pas l&rsquo;application elle-même — est responsable du schéma. C&rsquo;est une question de gouvernance autant que de technique, et les questions de gouvernance prennent plus de temps à résoudre que les changements de code.</p>
<h2 id="la-fin-honnête">La fin honnête</h2>
<p>Le bloc de migration dans l&rsquo;entrypoint, c&rsquo;est deux lignes. Littéralement : le guard <code>if [ &quot;$( find ./migrations... )&quot; ]</code>, et le <code>php bin/console doctrine:migrations:migrate</code> qui suit. Onze autres facteurs ont des résolutions nettes. Le cache est passé sur Redis. Les logs vont vers stdout. Le système de fichiers est un bucket S3. Le CI assemble les images de production depuis le même commit qu&rsquo;il teste. Les secrets ne voyagent plus dans les layers d&rsquo;image.</p>
<p>Le Facteur XII a une réponse. Ce n&rsquo;est juste pas la réponse finale.</p>
<p>Les migrations tournent au démarrage, avec une vraie base de données, avec atomicité, avec une fenêtre de retry bornée. C&rsquo;est mieux que de tourner au build contre rien. La question de savoir si elles finiront dans un Job ou un hook Helm est une conversation sur qui possède le schéma — une question à laquelle un <code>kubectl apply</code> ne peut pas répondre.</p>
]]></content:encoded></item><item><title>Démarré ne veut pas dire prêt</title><link>https://guillaumedelre.github.io/fr/2026/05/17/d%C3%A9marr%C3%A9-ne-veut-pas-dire-pr%C3%AAt/</link><pubDate>Sun, 17 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/17/d%C3%A9marr%C3%A9-ne-veut-pas-dire-pr%C3%AAt/</guid><description>Part 7 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Le script d&amp;#39;entrypoint qui fonctionne parfaitement sous Docker Compose a cinq responsabilités. Sur Kubernetes, chacune d&amp;#39;elles a sa place ailleurs.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Le rolling deploy avait l&rsquo;air propre. Un nouveau pod démarrait. Kubernetes voyait le healthcheck passer — <code>php -v</code> renvoyait zéro — et commençait à router du trafic vers le nouveau container.</p>
<p>Pendant les quarante secondes suivantes — sur les soixante possibles — ce container était en train de poller la base de données.</p>
<p>Les requêtes qui atterrissaient dessus pendant cette fenêtre récoltaient des erreurs. Pas beaucoup — la fenêtre était courte — mais assez pour apparaître comme du bruit dans le monitoring. Le genre de bruit qu&rsquo;on classe comme « problème réseau transitoire » et qu&rsquo;on ne signale nulle part. Le déploiement a réussi. Le pod a fini par devenir prêt. Le mécanisme qui en était la cause était toujours là, attendant le prochain déploiement.</p>
<p>Le script d&rsquo;entrypoint fait cinq choses avant que FrankenPHP démarre : copier un fichier de version, vérifier le répertoire vendor, attendre jusqu&rsquo;à soixante secondes la base de données, jouer les migrations en attente, installer les assets et configurer les permissions filesystem. Sous Docker Compose, c&rsquo;est invisible. Sur Kubernetes, l&rsquo;écart devient du trafic en erreur.</p>
<h2 id="lécart-entre-démarré-et-prêt">L&rsquo;écart entre démarré et prêt</h2>
<p>Kubernetes décide d&rsquo;envoyer du trafic à un pod en surveillant sa readiness probe. Un pod dont la readiness probe passe reçoit des requêtes. Un pod dont la readiness probe échoue est retiré de la rotation du load balancer jusqu&rsquo;à ce qu&rsquo;il récupère. C&rsquo;est le mécanisme qui rend les rolling deploys sûrs : Kubernetes ne bascule pas vers un nouveau pod tant que ce pod n&rsquo;indique pas qu&rsquo;il est prêt.</p>
<p>Le compose.yaml définit un healthcheck sur chaque service :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">healthcheck</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">test</span>: [ <span style="color:#e6db74">&#34;CMD&#34;</span>, <span style="color:#e6db74">&#34;php&#34;</span>, <span style="color:#e6db74">&#34;-v&#34;</span> ]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">interval</span>: <span style="color:#ae81ff">30s</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">10s</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">retries</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">start_period</span>: <span style="color:#ae81ff">10s</span>
</span></span></code></pre></div><p><code>php -v</code> réussit dès que le binaire PHP est présent — ce qui est vrai depuis la première milliseconde de vie du container. Le <code>start_period: 10s</code> donne dix secondes avant que les vérifications commencent. Mais la boucle de polling de l&rsquo;entrypoint tourne jusqu&rsquo;à soixante secondes avant que FrankenPHP démarre. À la dixième seconde, le healthcheck passe. L&rsquo;application attend toujours la base de données.</p>
<p>Le Dockerfile a un meilleur signal :</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-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">HEALTHCHECK</span> --start-period<span style="color:#f92672">=</span>60s CMD curl -f http://localhost:2019/metrics <span style="color:#f92672">||</span> exit <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Le port 2019 est le serveur de métriques intégré à Caddy, embarqué directement dans FrankenPHP. L&rsquo;endpoint est compatible Prometheus et ne répond qu&rsquo;une fois que la stack HTTP de Caddy est pleinement initialisée et que les workers PHP acceptent des connexions. <code>php -v</code> se termine en cinquante millisecondes quel que soit l&rsquo;état de l&rsquo;application — il vérifie le binaire, pas le serveur. <code>:2019/metrics</code> ne répond que quand le serveur sert vraiment. Ce n&rsquo;est pas non plus un endpoint ajouté exprès pour la probe : chaque service de la plateforme l&rsquo;a déjà scraped par Prometheus, donc le signal est actif indépendamment de toute configuration de healthcheck.</p>
<p>C&rsquo;est plus proche. Mais sur Kubernetes, l&rsquo;instruction <code>HEALTHCHECK</code> du Dockerfile est totalement ignorée. Kubernetes utilise sa propre configuration de probes. Sans définitions de probes explicites dans les manifests Kubernetes, il n&rsquo;y a aucune vérification de readiness — et un pod est considéré prêt dès que son container démarre.</p>
<p>Ce qui signifie : le pod démarre, l&rsquo;entrypoint commence à poller, Kubernetes route du trafic, l&rsquo;application n&rsquo;est pas encore en état de le traiter. Les requêtes arrivent sur un container qui n&rsquo;est pas prêt à les gérer.</p>
<h2 id="trois-signaux-trois-questions">Trois signaux, trois questions</h2>
<p>Kubernetes sépare le cycle de vie d&rsquo;un container en trois questions distinctes, chacune avec son propre type de probe :</p>
<p><strong>startupProbe</strong> — « L&rsquo;application a-t-elle fini de démarrer ? » Se déclenche à répétition jusqu&rsquo;à ce qu&rsquo;elle passe, puis passe la main à la liveness. Empêche la liveness probe de tuer un container qui est légitimement long à initialiser. Pour un container dont l&rsquo;entrypoint peut prendre soixante secondes, c&rsquo;est l&rsquo;outil adapté.</p>
<p><strong>readinessProbe</strong> — « L&rsquo;application est-elle prête à traiter des requêtes ? » Échoue et passe tout au long de la vie du container. Quand elle échoue, le pod est retiré du load balancer. C&rsquo;est ce qui rend un rolling deploy sûr.</p>
<p><strong>livenessProbe</strong> — « L&rsquo;application est-elle toujours vivante ? » Si elle échoue, Kubernetes redémarre le container. Conçue pour détecter les processus bloqués, pas les démarrages lents.</p>
<p>La boucle de polling de soixante secondes appartient à la patience de la startupProbe, pas au code applicatif :</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">startupProbe</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">httpGet</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/metrics</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">port</span>: <span style="color:#ae81ff">2019</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">failureThreshold</span>: <span style="color:#ae81ff">12</span>    <span style="color:#75715e"># 12 tentatives × 5s = 60s max</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">periodSeconds</span>: <span style="color:#ae81ff">5</span>
</span></span></code></pre></div><p>Une fois la startupProbe passée, une readinessProbe sur le même endpoint prend le relais — indiquant à Kubernetes quand le pod peut recevoir du trafic — et une livenessProbe surveille les processus bloqués. Mais c&rsquo;est la startupProbe qui absorbe le démarrage lent. La boucle de polling de l&rsquo;entrypoint devient redondante : son rôle était de maintenir le container en vie pendant que la base de données devenait disponible. Sans elle, l&rsquo;application tente de se connecter, échoue, et le container quitte — Kubernetes redémarre alors le pod, et la startupProbe maintient son cycle de tentatives jusqu&rsquo;à ce que la base réponde et que l&rsquo;application démarre proprement. La responsabilité du retry passe de l&rsquo;intérieur de l&rsquo;entrypoint à l&rsquo;orchestrateur, ce qui est exactement là où elle devrait être.</p>
<h2 id="le-problème-des-migrations">Le problème des migrations</h2>
<p>La boucle de polling est le problème le plus visible, mais les migrations en créent un plus subtil.</p>
<p>Avec un rolling deploy et deux replicas, Kubernetes démarre un nouveau pod pendant que l&rsquo;ancien sert encore du trafic. Les deux pods jouent le même entrypoint. Les deux atteignent <code>doctrine:migrations:migrate</code>.</p>
<p>La table de migrations de Doctrine trace quelles migrations ont déjà été exécutées, donc une migration complétée ne se jouera pas deux fois. Mais si deux pods démarrent simultanément et voient tous les deux une migration en attente, les deux tentent de la jouer en même temps. Si c&rsquo;est sûr ou non dépend de la migration : les changements de schéma additifs passent en général bien ; les destructifs moins. Et on ne choisit pas lesquels s&rsquo;exécutent lors d&rsquo;un déploiement qui n&rsquo;a pas prévu de se coordonner. <code>--all-or-nothing</code> enveloppe les migrations dans une transaction et fait un rollback si l&rsquo;une échoue — c&rsquo;est une question d&rsquo;atomicité au sein d&rsquo;une seule exécution, pas de coordination entre processus.</p>
<p>L&rsquo;approche plus propre sépare ces deux préoccupations en deux init containers : l&rsquo;un qui attend la base de données, l&rsquo;autre qui joue les migrations. Le container principal ne démarre qu&rsquo;une fois les deux terminé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">initContainers</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wait-for-db</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">image</span>: <span style="color:#ae81ff">authentication:latest</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">command</span>: [<span style="color:#e6db74">&#34;php&#34;</span>, <span style="color:#e6db74">&#34;bin/console&#34;</span>, <span style="color:#e6db74">&#34;dbal:run-sql&#34;</span>, <span style="color:#e6db74">&#34;-q&#34;</span>, <span style="color:#e6db74">&#34;SELECT 1&#34;</span>]
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">migrate</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">image</span>: <span style="color:#ae81ff">authentication:latest</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">command</span>: [<span style="color:#e6db74">&#34;php&#34;</span>, <span style="color:#e6db74">&#34;bin/console&#34;</span>, <span style="color:#e6db74">&#34;doctrine:migrations:migrate&#34;</span>, <span style="color:#e6db74">&#34;--no-interaction&#34;</span>, <span style="color:#e6db74">&#34;--all-or-nothing&#34;</span>]
</span></span></code></pre></div><p>Les deux init containers réutilisent la même image que l&rsquo;application. Ce n&rsquo;est pas du gaspillage : ils ont besoin du même binaire PHP et du même câblage d&rsquo;environnement pour atteindre la base de données et trouver les classes de migration. Une image dédiée plus légère réduirait le temps de démarrage, mais nécessiterait de maintenir une installation PHP séparée en synchronisation avec l&rsquo;image principale.</p>
<p>Même avec des init containers, plusieurs pods démarrant simultanément — déploiement initial, après une défaillance de nœud, ou sous pression d&rsquo;autoscaling — tenteront chacun de jouer les migrations. Le résoudre proprement — via un hook pre-upgrade Helm, une stratégie <code>maxSurge: 0</code>, ou un Job de migration séparé — est un sujet en soi. Ce qui compte ici, c&rsquo;est que l&rsquo;entrypoint est le mauvais endroit pour prendre cette décision : il ne peut pas se coordonner entre pods, et il lie l&rsquo;exécution des migrations au démarrage de l&rsquo;application d&rsquo;une façon difficile à démêler plus tard. La question de quelle alternative convient à cette codebase — et pourquoi l&rsquo;entrypoint n&rsquo;a pas encore été remplacé — fait l&rsquo;objet de <a href="/2026/05/17/eleven-out-of-twelve/">l&rsquo;article suivant dans cette série</a>
.</p>
<p>Le Facteur XII de la <a href="https://12factor.net/admin-processes" target="_blank" rel="noopener noreferrer">méthodologie twelve-factor</a> — les processus d&rsquo;administration tournent dans le même environnement que l&rsquo;application — est respecté dans les deux cas. La question est de savoir si « même environnement » signifie « même script d&rsquo;entrypoint » ou « même image, processus séparé ». Sur Kubernetes, le second est plus sûr.</p>
<h2 id="la-vraie-responsabilité-de-lentrypoint">La vraie responsabilité de l&rsquo;entrypoint</h2>
<p>Enlever l&rsquo;attente de la base de données (maintenant une startupProbe ou un init container), les migrations (maintenant un init container ou un Job), et l&rsquo;installation des assets (une opération de build-time qui appartient au Dockerfile), et l&rsquo;entrypoint n&rsquo;a plus qu&rsquo;une seule responsabilité : démarrer l&rsquo;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-sh" data-lang="sh"><span style="display:flex;"><span>exec docker-php-entrypoint <span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>Le Facteur IX de la twelve-factor app demande un démarrage rapide et un arrêt propre. Un container dont le démarrage prend soixante secondes parce qu&rsquo;il attend des dépendances externes n&rsquo;est pas rapide. Ça signifie des rolling deploys lents, une reprise après crash lente, et un scale-out horizontal qui crée une fenêtre de soixante secondes avant que chaque nouveau pod contribue.</p>
<p>Le démarrage rapide n&rsquo;est pas juste un confort. C&rsquo;est ce qui fait fonctionner le reste du modèle cloud. Quand un pod peut démarrer en secondes, l&rsquo;orchestrateur peut scaler agressivement et récupérer vite. Quand ça prend une minute, on ajoute des marges partout — timeouts de probes plus longs, fenêtres de déploiement plus larges, politiques de scaling plus conservatrices — et le système devient rigide.</p>
<h2 id="la-taxe-docker-compose">La taxe Docker Compose</h2>
<p>L&rsquo;entrypoint accumule ces responsabilités pour une raison. Sous Docker Compose, il n&rsquo;y a pas de concept d&rsquo;init container. Pas de startupProbe. Les services déclarent <code>depends_on</code>, mais sans conditions de santé, c&rsquo;est juste de l&rsquo;ordre de démarrage — pas de la readiness. L&rsquo;entrypoint comble le vide.</p>
<p>Ce n&rsquo;est pas un défaut de conception. C&rsquo;est une adaptation raisonnable aux contraintes de Docker Compose. Le script fonctionne. Il gère les cas limites (le timeout de la base, les erreurs irrécupérables, le répertoire de migrations absent). Quelqu&rsquo;un l&rsquo;a testé.</p>
<p>Le problème, c&rsquo;est l&rsquo;hypothèse que le même script fonctionne aussi bien sur Kubernetes. Il tourne. L&rsquo;application finit par démarrer. Mais il contourne le système de probes qui rend les déploiements Kubernetes fiables, et il place la responsabilité des migrations à un endroit où la coordination entre pods est difficile à raisonner.</p>
<p>Plusieurs des changements de cette série — <a href="/2026/05/14/the-ghost-of-the-ci-runner/">stockage des médias</a>
, <a href="/2026/05/14/what-survives-the-build/">secrets dans les images</a>
, <a href="/2026/05/15/no-witnesses/">handlers de logs</a>
, <a href="/2026/05/15/the-host-that-hid-the-graph/">dépendances de services</a>
, <a href="/2026/05/16/fifteen-minutes-before-the-first-test/">parité d&rsquo;environnement CI</a>
, <a href="/2026/05/16/the-cache-that-was-lying-to-us/">adaptateurs de cache</a>
 — étaient des changements au code applicatif ou à la configuration. Celui-ci est différent. Il demande à l&rsquo;infrastructure de comprendre ce que « prêt » signifie pour cette application, et il demande à l&rsquo;entrypoint de céder des responsabilités qu&rsquo;il détient actuellement.</p>
<p>C&rsquo;est une conversation plus difficile. Mais la startupProbe attend.</p>
]]></content:encoded></item><item><title>Le cache qui nous mentait</title><link>https://guillaumedelre.github.io/fr/2026/05/16/le-cache-qui-nous-mentait/</link><pubDate>Sat, 16 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/16/le-cache-qui-nous-mentait/</guid><description>Part 6 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Comment une seule ligne de config bloquait le scaling horizontal de 13 microservices Symfony, et ce que la méthodologie twelve-factor avait à dire là-dessus.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>La première fois qu&rsquo;on a lancé deux replicas du même service Symfony derrière un load balancer, tout avait l&rsquo;air d&rsquo;aller. Les health checks passaient. Le trafic se répartissait proprement. Les temps de réponse étaient bons.</p>
<p>Puis quelqu&rsquo;un a remarqué que le rate limiter se comportait bizarrement. Cinq appels à l&rsquo;API, accès bloqué. Cinq appels supplémentaires à la requête suivante, accès accordé. Selon quel pod répondait, on était une personne différente.</p>
<p>C&rsquo;était le cache qui parlait. Une ligne de config, répliquée sur treize services, bloquait le scaling horizontal dans sa totalité.</p>
<h2 id="un-fichier-de-config-treize-fois">Un fichier de config, treize fois</h2>
<p>On préparait une plateforme de treize microservices Symfony pour passer sur Kubernetes. La stack était déjà en bon état : FrankenPHP pour le serveur HTTP, des Dockerfiles multi-étapes, un GitLab CI qui poussait des images taguées vers un registre cloud. Les pièces étaient là. Il fallait juste vérifier que rien ne casserait quand on commencerait à scaler les pods horizontalement.</p>
<p>Une bonne checklist pour ce type d&rsquo;audit, c&rsquo;est la <a href="https://12factor.net" target="_blank" rel="noopener noreferrer">méthodologie twelve-factor app</a> — douze principes pour construire des logiciels qui tournent proprement dans des environnements cloud. La plupart des facteurs étaient déjà couverts sans qu&rsquo;on y ait pensé délibérément.</p>
<p>Le Facteur VII (port binding) était gratuit. FrankenPHP embarque Caddy directement dans le processus PHP. Le container expose son propre endpoint HTTP, sans Apache ni Nginx à ajouter. L&rsquo;image est autonome, ce que le facteur demande exactement :</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-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">HEALTHCHECK</span> --start-period<span style="color:#f92672">=</span>60s CMD curl -f http://localhost:2019/metrics <span style="color:#f92672">||</span> exit <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Le Facteur II (dépendances) était géré par <code>composer.json</code> et les extensions du Dockerfile. Le Facteur X (parité dev/prod) était suffisamment couvert pour notre périmètre : même image, mêmes backing services en local et en CI, ce qui est la partie qui compte vraiment pour l&rsquo;audit.</p>
<p>Puis j&rsquo;en suis arrivé au Facteur VI.</p>
<h2 id="le-problème-avec--ça-marche-sur-un-seul-serveur-">Le problème avec « ça marche sur un seul serveur »</h2>
<p>Le Facteur VI dit que les processus ne doivent rien partager. Rien d&rsquo;écrit sur disque entre les requêtes, rien en mémoire locale qu&rsquo;une autre instance ne puisse pas voir. Si on a besoin de persister de l&rsquo;état, on le met dans un backing service — une base de données, un cluster de cache, une queue. Le processus lui-même reste jetable.</p>
<p>J&rsquo;ai ouvert <code>authentication/config/packages/cache.yaml</code>. Puis <code>content/config/packages/cache.yaml</code>. Puis <code>media/config/packages/cache.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">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">cache.adapter.filesystem</span>
</span></span></code></pre></div><p>Treize services. Treize fois, mot pour mot.</p>
<p>Chaque instance de chaque service écrivait son cache sur le filesystem local. Ce qui signifiait que chaque pod avait son propre cache privé, invisible pour tous les autres pods. Quand le load balancer envoyait une requête au pod A, il obtenait la version mise en cache par le pod A. Le pod B avait construit la sienne. Elles pouvaient avoir été générées à des moments différents, depuis des données sources différentes, ou l&rsquo;une d&rsquo;elles pouvait ne pas encore avoir été construite du tout.</p>
<p>Le rate limiter était le symptôme le plus visible parce qu&rsquo;il avait un compteur. Mais la même divergence affectait chaque donnée qu&rsquo;on mettait en cache : métadonnées du sérialiseur, collections de routes, caches de résultats Doctrine. Deux utilisateurs envoyant des requêtes identiques pouvaient obtenir des réponses différentes selon quel nœud avait récupéré la connexion.</p>
<h2 id="redis-était-déjà-là">Redis était déjà là</h2>
<p>C&rsquo;est la partie qui pique un peu. Redis était déjà dans la stack. Chaque service l&rsquo;avait configuré via SncRedisBundle :</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/snc_redis.yaml — présent sur les 13 services</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">snc_redis</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">clients</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">type</span>: <span style="color:#e6db74">&#39;phpredis&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">alias</span>: <span style="color:#e6db74">&#39;default&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">dsn</span>: <span style="color:#e6db74">&#39;%env(IN_MEM_STORE__URI)%&#39;</span>
</span></span></code></pre></div><p>Le Facteur IV de la twelve-factor app dit que les backing services doivent être des ressources attachées, interchangeables via la configuration. Redis était exactement ça : joignable via une variable d&rsquo;environnement, prêt à être remplacé par une instance managée dans le cloud. La plomberie était faite. On ne s&rsquo;en servait juste pas pour le cache applicatif.</p>
<p>Certains services l&rsquo;avaient même juste pour des pools spécifiques. Le rate limiter dans le service d&rsquo;authentification :</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">pools</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">rate_limiter.cache</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">adapter</span>: <span style="color:#ae81ff">cache.adapter.redis</span>
</span></span></code></pre></div><p>Ce qui explique l&rsquo;incohérence qu&rsquo;on a vue en premier. Le <em>compteur</em> du rate limit allait vers Redis (partagé entre les pods). Le cache qui alimentait la <em>vérification</em> du rate limit allait vers le filesystem (local au pod). Deux sources de vérité, l&rsquo;une invisible à l&rsquo;autre.</p>
<p>La correction tenait en une ligne par service :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-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">cache</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">cache.adapter.redis</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default_redis_provider</span>: <span style="color:#ae81ff">snc_redis.default</span>
</span></span></code></pre></div><p>Treize fichiers. Treize changements identiques. Le genre de correction qui donne l&rsquo;impression qu&rsquo;on aurait dû la repérer avant, sauf qu&rsquo;elle est parfaitement invisible quand on tourne sur une seule instance.</p>
<h2 id="ce-qui-doit-migrer-vers-redis">Ce qui doit migrer vers Redis</h2>
<p>Le cache filesystem violait le Facteur VI (les processus portent de l&rsquo;état local qu&rsquo;ils ne devraient pas) et le Facteur VIII (on ne peut pas scaler sans partager cet état). C&rsquo;est le même problème vu sous deux angles : VI décrit ce qui ne va pas, VIII décrit ce qu&rsquo;on ne peut pas faire à cause de ça.</p>
<p>Avec un backend de cache partagé, un deuxième pod est sûr. Les deux pods construisent le même cache, voient les mêmes invalidations, s&rsquo;accordent sur les mêmes limites de rate. On peut ajouter un troisième pod sous charge et le retirer quand le trafic baisse. L&rsquo;orchestrateur s&rsquo;en occupe ; l&rsquo;application n&rsquo;a pas besoin de le savoir.</p>
<p>Sans ça, le scaling horizontal est un risque. Plus de pods, c&rsquo;est plus de divergence, plus de bugs « ça marche chez moi » qu&rsquo;il est impossible de reproduire en local parce qu&rsquo;en local on tourne avec un seul container.</p>
<p>Les sessions avaient le même problème — et potentiellement pire. Douze des treize services utilisaient <code>session.storage.factory.native</code> — qui écrit les sessions sur le filesystem par défaut. Un utilisateur dont la requête atterrit sur le pod A obtient une session liée au pod A. Sa requête suivante va sur le pod B. Session perdue, il est déconnecté. Un seul service avait <code>RedisSessionHandler</code> configuré.</p>
<p>La mitigation partielle : la plupart de la plateforme tourne sur des APIs stateless avec des JWT, donc l&rsquo;usage des sessions est limité. Mais « limité » n&rsquo;est pas « zéro ». Les services qui créent des sessions — flows d&rsquo;authentification, état temporaire pendant les handshakes OAuth — ont un mode de défaillance visible par l&rsquo;utilisateur qui attend le deuxième pod. Soit ces sessions migrent vers Redis, soit le code qui les crée est supprimé. Les laisser en l&rsquo;état est une décision qui attend le premier utilisateur dont la session disparaît sans explication.</p>
<h2 id="lautre-genre-détat">L&rsquo;autre genre d&rsquo;état</h2>
<p>Redis résout le problème cross-pod. FrankenPHP introduit un autre problème qu&rsquo;il vaut la peine de connaître.</p>
<p>Dans le modèle PHP-FPM standard, chaque requête forke un processus frais. Tout objet en mémoire — toute valeur mise en cache, tout résultat calculé — meurt avec la réponse. Le processus est stateless par construction.</p>
<p>FrankenPHP a un mode worker qui ne suit pas ce modèle. En mode worker, un seul processus PHP démarre une fois, charge le kernel, câble le container, et gère plusieurs requêtes successives sans redémarrer. Le débit de requêtes s&rsquo;améliore : pas de cold start de l&rsquo;autoloader, pas de rebuild du container par requête, moins d&rsquo;allocations. La contrepartie : le processus PHP a maintenant un cycle de vie qui enjambe les requêtes.</p>
<p>Pour le cache, ça ajoute une complexité. Un adaptateur <code>array</code> ou un pool APCu accumule des entrées à travers les requêtes sur le même worker. Une invalidation de cache poussée vers Redis atteint immédiatement les autres pods — mais ne vide pas ce qui est assis dans la mémoire du worker. Deux requêtes sur le même pod peuvent voir des choses différentes : l&rsquo;une touche une entrée en mémoire chaude, la suivante déclenche un fetch Redis après expiration de l&rsquo;entrée in-process.</p>
<p>La plateforme garde le mode worker désactivé (<code>APP__WORKER_MODE__ENABLED=false</code>). Il est disponible — l&rsquo;infrastructure est là, le flag est câblé — mais pas actif. Le gain de performance ne justifiait pas l&rsquo;audit. Chaque pool de cache aurait besoin d&rsquo;être vérifié contre la sémantique du mode worker ; chaque endroit où de l&rsquo;état fuit entre les requêtes deviendrait un bug potentiel.</p>
<p>La position conservatrice : garder PHP stateless au niveau du processus même quand le runtime ne l&rsquo;exige pas. Le principe shared-nothing du Facteur VI s&rsquo;applique non seulement au filesystem — il s&rsquo;applique au processus lui-même.</p>
<h2 id="ce-qui-fonctionnait-déjà">Ce qui fonctionnait déjà</h2>
<p>Pour être juste envers la codebase : le Scheduler Symfony utilisait déjà Redis pour les locks distribué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>$schedule<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">lock</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">lockFactory</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">createLock</span>(<span style="color:#e6db74">&#39;schedule_purge&#39;</span>));
</span></span></code></pre></div><p>Dans un environnement multi-pod, on ne veut pas cinq instances lancer le même job de purge simultanément. Le lock l&rsquo;empêche. Redis rend le lock visible entre les pods. Celui qui a écrit le scheduler savait exactement ce qu&rsquo;il faisait.</p>
<p>Le même raisonnement ne s&rsquo;était juste pas propagé à la configuration du cache — probablement parce qu&rsquo;en tournant sur une seule instance, <code>cache.adapter.filesystem</code> est invisible. Ça fonctionne, c&rsquo;est rapide, ça ne demande aucune configuration. Le problème n&rsquo;apparaît qu&rsquo;à deux.</p>
<h2 id="les-quatre-questions">Les quatre questions</h2>
<p>Le Facteur VI prend la plupart des applications par surprise lors d&rsquo;une migration cloud. Pas parce que les développeurs ne connaissent pas les processus stateless — ils le savent généralement — mais parce que le filesystem est toujours là, et le problème reste caché jusqu&rsquo;à ce qu&rsquo;on essaie de lancer une deuxième instance.</p>
<p>Avant de scaler un service Symfony horizontalement, quatre questions méritent une réponse :</p>
<ul>
<li>Où va le cache applicatif ? (<code>cache.adapter.filesystem</code> doit devenir <code>cache.adapter.redis</code>)</li>
<li>Où vont les sessions ? (<code>session.storage.factory.native</code> a besoin de Redis — ou supprimer les sessions entièrement si on est full JWT)</li>
<li>Est-ce que quelque chose écrit dans <code>var/</code> à l&rsquo;exécution qu&rsquo;un autre pod aurait besoin de lire ?</li>
<li>Est-ce qu&rsquo;il y a quelque chose dans le chemin de code qui doit être mutuellement exclusif entre pods ? (si oui, c&rsquo;est un job pour le <a href="https://symfony.com/doc/current/components/lock.html" target="_blank" rel="noopener noreferrer">composant Lock de Symfony</a> adossé à Redis, pas un mutex local)</li>
</ul>
<p>Si toutes les réponses pointent vers des backing services partagés, on est prêt. Si l&rsquo;une d&rsquo;elles pointe vers le filesystem local, la production finira par trouver le pod qui a construit son cache il y a trois heures et le servira à l&rsquo;utilisateur qui s&rsquo;y attend le moins.</p>
]]></content:encoded></item><item><title>Quinze minutes avant le premier test</title><link>https://guillaumedelre.github.io/fr/2026/05/16/quinze-minutes-avant-le-premier-test/</link><pubDate>Sat, 16 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/16/quinze-minutes-avant-le-premier-test/</guid><description>Part 5 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Comment un pipeline CI qui provisionnait une VM Azure par run — sans RabbitMQ, MinIO ni Varnish — est devenu un pipeline qui assemble l&amp;#39;environnement de production depuis les mêmes images qu&amp;#39;il livre.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Le pipeline avait deux stages qui n&rsquo;avaient rien à voir avec le code : <code>provision</code> et <code>deprovision</code>. Entre eux, dans l&rsquo;ordre : <code>phpunit</code>, <code>phpmetrics</code>, <code>behat</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">stages</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">provision</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">phpunit</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">phpmetrics</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">behat</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">deprovision</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">deploy</span>
</span></span></code></pre></div><p>Avant que la première assertion s&rsquo;exécute, quinze minutes s&rsquo;étaient écoulées. Terraform avait cloné un dépôt d&rsquo;infrastructure, s&rsquo;était authentifié sur Azure, avait appliqué une configuration de VM. Ansible s&rsquo;était connecté à la nouvelle VM, avait installé PHP, configuré l&rsquo;application, câblé une base de données et une instance Redis. Ensuite les tests tournaient. Ensuite Terraform détruisait ce qu&rsquo;Ansible avait construit.</p>
<p>Pour chaque pipeline. Depuis chaque branche. Pour chaque pull request, de l&rsquo;ouverture au merge.</p>
<h2 id="ce-que-ces-quinze-minutes-ne-contenaient-pas">Ce que ces quinze minutes ne contenaient pas</h2>
<p>Le stage <code>provision</code> mettait en place deux services : PostgreSQL et Redis. Trois services dont l&rsquo;application dépendait en production étaient absents : RabbitMQ, MinIO et Varnish.</p>
<p>RabbitMQ traitait tout le travail asynchrone — 56 consumers sur 14 microservices. MinIO gérait le stockage de médias. Varnish était devant le cache HTTP. En CI, aucun d&rsquo;eux n&rsquo;existait. Les tests qui couvraient les files de messages ou le stockage de fichiers avaient deux options : ignorer ces chemins, ou les laisser non testés jusqu&rsquo;au staging. Varnish est un cas à part : les tests tapent directement dans l&rsquo;application et contournent intentionnellement la couche de cache, son absence en CI est donc un choix délibéré plutôt qu&rsquo;un manque.</p>
<p>C&rsquo;est le problème que le <a href="https://12factor.net/dev-prod-parity" target="_blank" rel="noopener noreferrer">Facteur X</a>
 décrit comme l&rsquo;écart d&rsquo;environnement. L&rsquo;écart ici n&rsquo;était pas une question de configuration — il était structurel. La VM était construite par Ansible depuis un script dans un dépôt séparé. Ce n&rsquo;était pas une image de container. Elle n&rsquo;était pas versionnée aux côtés de l&rsquo;application. Si une branche modifiait la topologie de messages RabbitMQ, il n&rsquo;y avait aucun moyen de tester cette modification en CI. Le changement de topologie et le code qui en dépendait ne se rencontreraient qu&rsquo;en staging.</p>
<p>Le script de provisioning Ansible lui-même fait partie du problème :</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">launch_vm</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">stage</span>: <span style="color:#ae81ff">provision</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">script</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">git clone git@gitlab.internal/infra/ci-vm.git</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">cd ci-vm</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">az login --service-principal -u $ARM_CLIENT_ID ...</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">terraform apply -var &#34;prefix=${CI_PIPELINE_ID}-vm&#34; ...</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">sleep 45</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">ansible-playbook behat/test-env.yml ...</span>
</span></span></code></pre></div><p>Le <code>sleep 45</code> est là parce qu&rsquo;Ansible a besoin que la VM finisse de booter avant de pouvoir s&rsquo;y connecter. Ce n&rsquo;est pas un oubli — c&rsquo;est le délai minimum qu&rsquo;une VM fraîchement provisionnée nécessite avant que SSH fonctionne. C&rsquo;est inscrit dans le processus.</p>
<h2 id="ce-qui-la-remplacé">Ce qui l&rsquo;a remplacé</h2>
<p>Le nouveau pipeline n&rsquo;a pas de stage <code>provision</code>. Il n&rsquo;a pas de stage <code>deprovision</code>. L&rsquo;environnement, ce sont les images, et les images existent avant que les tests commencent.</p>
<p>Chaque job de test déclare ses dépendances comme des services Docker :</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">name</span>: <span style="color:#ae81ff">$REGISTRY_URL/platform/rabbitmq:$CI_COMMIT_REF_SLUG</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">rabbitmq</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">$REGISTRY_URL/platform/minio:$CI_COMMIT_REF_SLUG</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">minio</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">redis:7.4.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">redis</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">$ARTIFACTORY_URL/postgresql:13</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alias</span>: <span style="color:#ae81ff">postgresql</span>
</span></span></code></pre></div><p>Les services démarrent en parallèle quand le job commence. Avant que le script de test tourne, un <code>before_script</code> attend qu&rsquo;ils soient tous prêts :</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">before_script</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">$CI_PROJECT_DIR/dockerize</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://postgresql:5432</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://rabbitmq:5672</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://minio:9000</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">wait tcp://redis:6379</span>
</span></span><span style="display:flex;"><span>      -<span style="color:#ae81ff">timeout 120s</span>
</span></span></code></pre></div><p>Du démarrage du pipeline à la première assertion : quatre-vingt-dix secondes — en supposant que les images sont déjà dans le cache du runner ; un cold pull rallonge les choses, mais devient négligeable une fois que le pipeline a tourné une fois sur une branche donnée.</p>
<h2 id="ce-que-signifie-ci_commit_ref_slug">Ce que signifie <code>$CI_COMMIT_REF_SLUG</code></h2>
<p>Le timing est le résultat visible. Ce qui le produit est plus intéressant encore : les noms des images.</p>
<p><code>$REGISTRY_URL/platform/rabbitmq:$CI_COMMIT_REF_SLUG</code> n&rsquo;est pas l&rsquo;image officielle RabbitMQ de Docker Hub. C&rsquo;est une image construite par le même pipeline, depuis la même branche, au même commit que le code testé. L&rsquo;image RabbitMQ embarque la topologie : un <code>definitions.json</code> avec chaque exchange, chaque queue, chaque binding, chaque configuration de dead-letter — versionné dans git aux côtés de l&rsquo;application qui en dépend.</p>
<p>Si une branche modifie la topologie de messages, le pipeline CI construit une nouvelle image RabbitMQ qui inclut ces modifications, puis exécute les tests contre elle. Le changement de topologie et le code qui en dépend sont testés ensemble, au même commit, avant que quoi que ce soit n&rsquo;atteigne le staging.</p>
<p>La même logique s&rsquo;applique à MinIO, décrite dans le <a href="/2026/05/14/the-ghost-of-the-ci-runner/">premier article de cette série</a>
 : l&rsquo;image MinIO embarque des fixtures de test préchargées. L&rsquo;environnement CI n&rsquo;a pas besoin d&rsquo;une étape de setup pour peupler le stockage. L&rsquo;état est intégré à l&rsquo;artefact.</p>
<p>Le runner de tests lui-même suit le même pattern. Chaque job utilise une variante debug de l&rsquo;image applicative — construite depuis la même branche, au même commit — avec les dépendances de test incluses :</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">image</span>: <span style="color:#ae81ff">$REGISTRY_URL/platform/$service:$CI_COMMIT_REF_SLUG-debug</span>
</span></span></code></pre></div><p>Tout l&rsquo;environnement s&rsquo;assemble depuis des artefacts construits au même point de l&rsquo;historique git.</p>
<h2 id="ce-que-ça-a-demandé-dabandonner">Ce que ça a demandé d&rsquo;abandonner</h2>
<p>Behat et la VM provisionnée étaient couplés. La suite de tests Behat tournait contre un serveur HTTP sur la VM ; supprimer la VM signifiait supprimer Behat.</p>
<p>Ça s&rsquo;est révélé moins bloquant que ça n&rsquo;en avait l&rsquo;air. La suite Behat vivait dans un dépôt séparé, nécessitait la VM pour tourner, et avait accumulé une charge de maintenance significative. PHPUnit, tournant dans le container applicatif avec les services Docker, couvrait les mêmes scénarios par un chemin plus direct : tests fonctionnels qui exercent la couche HTTP, tests unitaires pour les composants individuels, suites organisées par domaine fonctionnel et générées dynamiquement en jobs CI parallèles.</p>
<p>La couche BDD a disparu. La couverture de tests est restée — et pouvait désormais tourner contre les vrais services.</p>
<h2 id="le-facteur-x-appliqué">Le Facteur X, appliqué</h2>
<p>Le <a href="https://12factor.net/dev-prod-parity" target="_blank" rel="noopener noreferrer">Facteur X</a>
 se lit souvent comme &ldquo;utilise la même base de données en local qu&rsquo;en production.&rdquo; C&rsquo;est la version la plus simple. La version plus profonde concerne l&rsquo;écart entre ce qu&rsquo;on teste et ce qu&rsquo;on livre.</p>
<p>L&rsquo;écart dans l&rsquo;ancien pipeline était large : une VM configurée manuellement, privée de services clés, reconstruite de zéro à chaque run. L&rsquo;écart dans le nouveau pipeline est étroit : le CI assemble l&rsquo;environnement depuis les mêmes images que la production, construites au même commit que le code sous test.</p>
<p>Les quinze minutes de Terraform et Ansible n&rsquo;étaient pas seulement lentes. Elles construisaient quelque chose qui n&rsquo;était pas ce que la production faisait tourner, à chaque fois, avant que le moindre test puisse commencer. Les quatre-vingt-dix secondes de <code>docker pull</code> construisent exactement ce que la production fait tourner — et les tests qui suivent testent ça, pas une approximation.</p>
]]></content:encoded></item><item><title>L'hôte qui cachait le graphe</title><link>https://guillaumedelre.github.io/fr/2026/05/15/lh%C3%B4te-qui-cachait-le-graphe/</link><pubDate>Fri, 15 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/15/lh%C3%B4te-qui-cachait-le-graphe/</guid><description>Part 4 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Treize services partageant six variables de gateway identiques. La config semblait simple. Le graphe de dépendances était invisible — jusqu&amp;#39;à ce que Kubernetes demande où chaque service habitait vraiment.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Chaque service de la plateforme avait ces six variables :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__PORT<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__SCHEME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__PORT<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__SCHEME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http&#34;</span>
</span></span></code></pre></div><p>Treize services, six variables chacun, une seule valeur. En lisant la config d&rsquo;un service quelconque, l&rsquo;architecture semblait plate. Tout parlait au même hôte. C&rsquo;était tout le tableau.</p>
<p>Ce ne l&rsquo;était pas.</p>
<h2 id="comment-fonctionnait-la-gateway">Comment fonctionnait la gateway</h2>
<p>La gateway se trouvait devant chaque service et gérait tout le trafic inter-services. Un service appelant l&rsquo;API content construisait une requête vers <code>http://platform.internal/content/api/</code> — la gateway la recevait, identifiait la cible depuis le chemin de l&rsquo;URL, et la transmettait au bon backend. Chaque client HTTP inter-service dans <code>framework.yaml</code> suivait le même schéma :</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">content.client</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">base_uri</span>: <span style="color:#e6db74">&#34;%http_client.gateway.base_uri%/content/api/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">Host</span>: <span style="color:#e6db74">&#34;%env(APP__GATEWAY__PRIVATE__HOST)%&#34;</span>
</span></span></code></pre></div><p>Le paramètre <code>http_client.gateway.base_uri</code> était assemblé depuis les variables GATEWAY. La gateway savait où tournait chaque service. Les services n&rsquo;avaient pas besoin de le savoir. De leur point de vue, tout était <code>platform.internal</code>.</p>
<p>Ça fonctionnait. Pendant des années, ça fonctionnait bien. Ajouter un service signifiait ajouter un alias DNS dans la config de la gateway, pas toucher treize fichiers <code>.env</code>. La gateway abstraisait la topologie. Les services restaient découplés du détail d&rsquo;infrastructure de qui tournait où.</p>
<h2 id="ce-que-la-gateway-absorbait">Ce que la gateway absorbait</h2>
<p>L&rsquo;abstraction avait un coût qui n&rsquo;apparaissait pas tant qu&rsquo;on n&rsquo;essayait pas de lire le système.</p>
<p>En regardant le fichier env de <code>content</code>, on voyait six variables de gateway et rien d&rsquo;autre sur la communication inter-services. Pour découvrir que <code>content</code> appelait <code>conversion</code>, <code>shorty</code> et <code>media</code>, il fallait lire <code>framework.yaml</code>. Pour découvrir que <code>pilot</code> appelait dix services externes, il fallait tracer les clients HTTP un par un et compter.</p>
<p>Le chiffre était dix. Authentication, bam, config, content, conversion, media, product, shorty, sitemap, social. Dix des treize services de la plateforme dont <code>pilot</code> dépendait à l&rsquo;exécution, aucun d&rsquo;eux visible depuis sa configuration. Six variables disaient : parle à la gateway. Elles ne disaient rien de la forme de ce qui se trouvait derrière.</p>
<p>Cette information existait — dans le code, dans la config framework, dans les têtes des gens qui avaient construit ces intégrations. Elle ne vivait juste nulle part où on pouvait la lire d&rsquo;un coup d&rsquo;œil.</p>
<h2 id="ce-que-kubernetes-a-rendu-explicite">Ce que Kubernetes a rendu explicite</h2>
<p>On-premise, la gateway était un seul hostname résolvable. Un enregistrement DNS, un jeu de variables, un seul endroit à mettre à jour. Kubernetes ne fonctionne pas comme ça. Chaque service obtient son propre nom DNS à l&rsquo;intérieur du cluster — <code>content.namespace.svc.cluster.local</code>, <code>conversion.namespace.svc.cluster.local</code>. Le trafic inter-services passe directement, service à service, sans gateway partagée.</p>
<p>Passer à Kubernetes signifiait que l&rsquo;abstraction de la gateway devait céder la place. Chaque service devait savoir, concrètement, où vivait chacune de ses dépendances. Les six variables génériques ne pouvaient pas exprimer ça.</p>
<p>Le refacto les a remplacées par des variables HOST par cible — une par dépendance de service, nommée d&rsquo;après la cible :</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><span style="color:#75715e"># content/.env — content appelle ces quatre services</span>
</span></span><span style="display:flex;"><span>APP__CONFIG__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONVERSION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__MEDIA__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SHORTY__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># pilot/.env — dix dépendances de service</span>
</span></span><span style="display:flex;"><span>APP__AUTHENTICATION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__BAM__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONFIG__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONTENT__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONVERSION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__MEDIA__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__PRODUCT__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SHORTY__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SITEMAP__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SOCIAL__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span></code></pre></div><p>Chaque client HTTP dans <code>framework.yaml</code> a reçu sa propre <code>base_uri</code> construite depuis la variable HOST de sa cible, et le header <code>Host</code> a cédé la place à un <code>User-Agent</code> qui identifie l&rsquo;appelant :</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">content.client</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">base_uri</span>: <span style="color:#e6db74">&#34;%env(APP__HTTP__SCHEME)%://%env(APP__CONTENT__HOST)%:%env(APP__HTTP__PORT)%/content/api/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">User-Agent</span>: <span style="color:#e6db74">&#34;Platform Content - %semver%&#34;</span>
</span></span></code></pre></div><p>Le changement n&rsquo;est pas cosmétique. Dans l&rsquo;ancienne configuration, le header <code>Host</code> explicite garantissait que les requêtes atteignaient le bon virtual host de la gateway quelle que soit la résolution DNS. Dans la nouvelle, chaque client pointe directement vers le nom DNS de sa cible — le <code>Host</code> correct est dérivé automatiquement de la <code>base_uri</code>. L&rsquo;emplacement du header ne reste pas vide : le <code>User-Agent</code> identifie désormais le service appelant, ce qui remonte dans les logs et le traçage distribué sans instrumentation supplémentaire.</p>
<h2 id="linconfort-de-la-lisibilité">L&rsquo;inconfort de la lisibilité</h2>
<p>Le fichier env de <code>pilot</code> est passé de neuf variables de gateway à dix variables HOST spécifiques par service. Le fichier est devenu plus long. L&rsquo;architecture n&rsquo;est pas devenue plus simple — les dix dépendances étaient là avant et elles sont toujours là. Ce qui a changé, c&rsquo;est qu&rsquo;elles sont lisibles.</p>
<p>Le <a href="https://12factor.net/config" target="_blank" rel="noopener noreferrer">Facteur III</a>
 dit de stocker la config dans l&rsquo;environnement. L&rsquo;ancienne approche satisfaisait ça à la lettre : six variables, toutes dans des fichiers env, aucune en dur dans le code. Mais des variables qui effondrent le graphe de dépendances entier dans un seul hostname opaque ne sont pas vraiment de la configuration — elles sont un raccourci qui échange la lisibilité contre la commodité. Le Facteur III ne demande pas seulement que la config soit externalisée — il suppose implicitement qu&rsquo;elle reste informative une fois externalisée.</p>
<p>Le refacto n&rsquo;a rien simplifié. Il a rendu la complexité visible. Les dix variables HOST de <code>pilot</code> documentent, dans le fichier <code>.env</code> lui-même, les dix services dont il dépend. Un nouveau membre d&rsquo;équipe qui lit ce fichier apprend quelque chose de réel sur l&rsquo;architecture. L&rsquo;ancien fichier lui apprenait qu&rsquo;il y avait une gateway.</p>
<p>Il y a une version de cette histoire où on lit l&rsquo;état final et on conclut que l&rsquo;équipe a fait un travail inutile — elle a remplacé six variables par dix, toutes pointant vers le même hôte de toute façon. En développement local, <code>platform.internal</code> résout toujours au même endroit. Le comportement fonctionnel n&rsquo;a pas changé.</p>
<p>Le changement est dans ce que la config communique. Dans Kubernetes, les valeurs HOST divergent : chaque cible obtient son propre nom DNS interne au cluster, différent par environnement. Les variables portent maintenant une vraie information. Le refacto a préparé la config à être honnête sur une topologie qu&rsquo;elle simplifiait silencieusement depuis des années.</p>
]]></content:encoded></item><item><title>Aucun témoin</title><link>https://guillaumedelre.github.io/fr/2026/05/15/aucun-t%C3%A9moin/</link><pubDate>Fri, 15 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/15/aucun-t%C3%A9moin/</guid><description>Part 3 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Un service s&amp;#39;est crashé en production sans laisser de logs. Pourquoi fingers_crossed et les déploiements cloud ne font pas bon ménage.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Le service s&rsquo;était crashé. On avait l&rsquo;alerte. On avait le timestamp à la seconde. On avait <a href="https://grafana.com/oss/loki/" target="_blank" rel="noopener noreferrer">Loki</a> ouvert avec une requête prête.</p>
<p>Ce qu&rsquo;on n&rsquo;avait pas, c&rsquo;était les logs des cinq minutes précédant le crash.</p>
<p>Promtail tournait. Il était healthy. Il collectait les logs de tous les autres services sans problème. Mais pour celui-ci, dans la fenêtre qui comptait, il n&rsquo;y avait rien. Le service s&rsquo;était crashé sans laisser de trace.</p>
<h2 id="le-setup-qui-semblait-correct">Le setup qui semblait correct</h2>
<p>La stack de logging était raisonnable. Chaque service écrivait du JSON structuré vers stdout avec le formatter logstash de Monolog :</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">stdout</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stdout&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">level</span>: <span style="color:#e6db74">&#34;%env(MONOLOG_LEVEL__DEFAULT)%&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span></code></pre></div><p><a href="https://grafana.com/docs/loki/latest/" target="_blank" rel="noopener noreferrer">Promtail</a> collectait la sortie des containers via la socket Docker, parsait le JSON, extrayait des labels, poussait vers Loki :</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">scrape_configs</span>:
</span></span><span style="display:flex;"><span>    -
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">job_name</span>: <span style="color:#ae81ff">docker</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">docker_sd_configs</span>:
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">host</span>: <span style="color:#ae81ff">unix:///var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">refresh_interval</span>: <span style="color:#ae81ff">5s</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pipeline_stages</span>:
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">drop</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">older_than</span>: <span style="color:#ae81ff">168h</span>
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">json</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">expressions</span>:
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">level</span>: <span style="color:#ae81ff">level</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">msg</span>: <span style="color:#ae81ff">message</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">service</span>: <span style="color:#ae81ff">service</span>
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">level</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">relabel_configs</span>:
</span></span><span style="display:flex;"><span>            -
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">source_labels</span>: [ <span style="color:#e6db74">&#39;__meta_docker_container_log_stream&#39;</span> ]
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">target_label</span>: <span style="color:#ae81ff">stream</span>
</span></span></code></pre></div><p>Deux stages font plus de travail que les autres. Le stage <code>json</code> extrait <code>level</code> et <code>service</code> de chaque ligne de log ; le stage <code>labels</code> qui suit immédiatement les promeut en labels d&rsquo;index Loki, ce qui fait de <code>{service=&quot;content&quot;, level=&quot;error&quot;}</code> une lookup directe plutôt qu&rsquo;un scan plein texte sur les lignes stockées. Le relabeling <code>stream</code> conserve si une ligne venait de stdout ou stderr — une distinction requêtable dès que Monolog envoie les erreurs vers stderr et le reste vers stdout. Le stage <code>drop older_than: 168h</code> est une soupape de sécurité : si Promtail redémarre après une longue interruption et rejoue des lignes bufferisées, tout ce qui est plus vieux de sept jours est écarté avant d&rsquo;atteindre Loki.</p>
<p>En théorie : les logs vont vers stdout, Promtail lit stdout, les logs apparaissent dans Loki. La <a href="https://12factor.net/logs" target="_blank" rel="noopener noreferrer">méthodologie twelve-factor</a> décrit exactement ce modèle pour le Facteur XI — traiter les logs comme des flux d&rsquo;événements, écrire vers stdout, laisser l&rsquo;environnement gérer la collecte et le routage.</p>
<p>L&rsquo;application avait stdout. Promtail lisait stdout. Qu&rsquo;est-ce qui pouvait mal tourner.</p>
<h2 id="ce-que-fingers_crossed-emporte-avec-lui">Ce que fingers_crossed emporte avec lui</h2>
<p>En production, le bloc <code>when@prod</code> remplaçait le simple handler <code>stream</code> par quelque chose de plus sophistiqué :</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">when@prod</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">monolog</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">handlers</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">type</span>: <span style="color:#ae81ff">fingers_crossed</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">action_level</span>: <span style="color:#ae81ff">error</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">handler</span>: <span style="color:#ae81ff">main_group</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">excluded_http_codes</span>: [<span style="color:#ae81ff">404</span>]
</span></span></code></pre></div><p>La ligne <code>excluded_http_codes: [404]</code> est elle-même révélatrice : sans elle, chaque 404 d&rsquo;un scanner ou d&rsquo;un crawler déclenche un flush complet du buffer, déversant des mégaoctets de logs debug pour des URLs malformées. Quelqu&rsquo;un avait déjà appris ça à ses dépens.</p>
<p><code>fingers_crossed</code> est un pattern Monolog bien connu. L&rsquo;idée est élégante : ne pas noyer les logs de production dans le bruit debug, mais si quelque chose tourne mal, retrouver rétrospectivement ce qui s&rsquo;est passé avant l&rsquo;erreur. Le handler bufferise chaque entrée de log en mémoire. Au moment où il voit une <code>error</code>, il flush le buffer entier vers le handler imbriqué — en donnant le contexte complet qui a précédé la défaillance.</p>
<p>Le problème, c&rsquo;est ce qui se passe quand la défaillance n&rsquo;est pas une erreur loguée. C&rsquo;est un OOM kill. Un SIGKILL de l&rsquo;orchestrateur. Un segfault. Un process qui arrête de répondre et est tué de force.</p>
<p>Dans ces cas, <code>fingers_crossed</code> n&rsquo;atteint jamais son <code>action_level</code>. Le buffer existe, plein des cinq dernières minutes d&rsquo;activité, et il disparaît avec le process. Les logs étaient là. Ils étaient en mémoire. Ils sont morts avant d&rsquo;atteindre stdout.</p>
<p>Le Facteur IX du twelve-factor parle de disposabilité : les processus doivent démarrer vite et s&rsquo;arrêter proprement. Sur un arrêt normal (SIGTERM), un processus bien élevé finit son travail en cours et quitte. Mais les crashes ne sont pas des arrêts propres, et les buffers mémoire ne sont pas résistants aux crashes. Le service était disposable au sens où on pouvait le redémarrer ; il ne l&rsquo;était pas au sens où sa sortie était transparente.</p>
<h2 id="les-fichiers-que-personne-ne-lisait">Les fichiers que personne ne lisait</h2>
<p>Il y avait un deuxième problème, plus silencieux mais tout aussi persistant.</p>
<p>Chaque service avait un handler <code>main_group</code> qui routait les logs vers deux destinations en parallèle :</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">main_group</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">group</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">members</span>: [<span style="color:#ae81ff">main_file, stdout]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">main_file</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;%kernel.logs_dir%/%kernel.environment%.log&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#34;monolog.formatter.logstash&#34;</span>
</span></span></code></pre></div><p><code>var/log/prod.log</code> était écrit sur chaque service, dans chaque environnement, y compris en production. Le même contenu qui allait vers stdout allait aussi vers un fichier à l&rsquo;intérieur du container. Le fichier grossissait sans rotation. Le fichier n&rsquo;était pas accessible à Promtail (qui lisait depuis la socket Docker, pas depuis le filesystem du container). Le fichier consommait de l&rsquo;espace disque. Personne ne le lisait.</p>
<p>Le channel audit était pire :</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">audit_file</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;%kernel.logs_dir%/audit.log&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.line&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">audit</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">group</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">members</span>: [<span style="color:#ae81ff">audit_file, stderr]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">channels</span>: [<span style="color:#e6db74">&#39;audit&#39;</span>]
</span></span></code></pre></div><p>Les logs d&rsquo;audit allaient vers <code>stderr</code> (visible par Promtail) et vers <code>audit.log</code> (invisible à Promtail). Le format dans le fichier était une ligne brute, pas le JSON structuré qu&rsquo;attendait Promtail. En pratique, la piste d&rsquo;audit existait à deux endroits : l&rsquo;une requêtable, l&rsquo;autre enfouie dans un répertoire de container qui ne survivait que le temps du container.</p>
<h2 id="ce-que-le-facteur-xi-demande-vraiment">Ce que le Facteur XI demande vraiment</h2>
<p>Le onzième facteur est direct là-dessus : une application ne doit pas se soucier du routage ou du stockage de son flux de sortie. Elle écrit vers stdout. Tout le reste est le job de l&rsquo;environnement.</p>
<p>Ça veut dire pas de handlers de fichiers en production. Pas en backup. Pas pour les pistes d&rsquo;audit. Pas &ldquo;au cas où&rdquo;. Du moment qu&rsquo;une application se met à gérer des fichiers, elle prend en charge la rotation, la rétention, l&rsquo;espace disque, et l&rsquo;accessibilité — rien de tout ça n&rsquo;appartient à l&rsquo;intérieur d&rsquo;un container.</p>
<p>La correction pour les handlers de fichiers est directe. Dans <code>when@prod</code>, supprimer chaque handler <code>*_file</code> et chaque group qui en inclut un. Le channel audit reçoit le même traitement : stderr uniquement, JSON structuré, pas de fichier :</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">when@prod</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">monolog</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">handlers</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">stdout</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stdout&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e"># défaut &#34;warning&#34; — configurable par déploiement via variable d&#39;env pour du debug ciblé</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">level</span>: <span style="color:#e6db74">&#34;%env(default:default_log_level:MONOLOG_LEVEL__DEFAULT)%&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">stderr</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stderr&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">level</span>: <span style="color:#ae81ff">error</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span><span style="display:flex;"><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">type</span>: <span style="color:#ae81ff">group</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">members</span>: [<span style="color:#ae81ff">stdout]</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">channels</span>: [<span style="color:#e6db74">&#39;!event&#39;</span>, <span style="color:#e6db74">&#39;!http_client&#39;</span>, <span style="color:#e6db74">&#39;!doctrine&#39;</span>, <span style="color:#e6db74">&#39;!deprecation&#39;</span>, <span style="color:#e6db74">&#39;!audit&#39;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">audit</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">type</span>: <span style="color:#ae81ff">stream</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;php://stderr&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">level</span>: <span style="color:#ae81ff">debug</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">formatter</span>: <span style="color:#e6db74">&#39;monolog.formatter.logstash&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">channels</span>: [<span style="color:#e6db74">&#39;audit&#39;</span>]
</span></span></code></pre></div><p>stdout pour le channel principal. stderr pour les erreurs et l&rsquo;audit. Rien d&rsquo;autre. Promtail récupère les deux via la socket Docker. Le container n&rsquo;écrit rien sur disque. Et les logs d&rsquo;audit sont maintenant du JSON structuré, requêtable dans Loki avec tout le reste.</p>
<h2 id="la-question-plus-dure-sur-fingers_crossed">La question plus dure sur fingers_crossed</h2>
<p>Les handlers de fichiers, c&rsquo;était simple. <code>fingers_crossed</code> est plus nuancé.</p>
<p>Le pattern résout un vrai problème : dans un service de production actif, tout logger en debug crée du bruit et des coûts. <code>fingers_crossed</code> permet de capturer le contexte sans le payer sauf si quelque chose tourne vraiment mal. C&rsquo;est un compromis raisonnable quand le mode de défaillance contre lequel on protège est une erreur applicative (une exception, une 500, une requête lente).</p>
<p>Ce n&rsquo;est pas un compromis raisonnable quand le mode de défaillance est un crash de process. Et dans un environnement Kubernetes, les crashes de process arrivent : évictions OOM, échecs de liveness probe, pression sur les nodes. Exactement les cas où on a le plus besoin des logs.</p>
<p>Une approche : garder <code>fingers_crossed</code> mais réduire la taille du buffer. Par défaut il garde tout depuis le dernier reset. Mettre <code>buffer_size: 50</code> plafonne l&rsquo;usage mémoire, ce qui limite aussi ce qui se perd lors d&rsquo;un crash. On n&rsquo;aura pas le contexte complet, mais on aura les cinquante dernières entrées. Cette voie réduit le périmètre de perte sans supprimer la cause : l&rsquo;opacité dépend toujours d&rsquo;un seuil d&rsquo;erreur qui peut ne jamais se déclencher.</p>
<p>Une autre approche : accepter que les logs debug soient coûteux et monter le niveau par défaut en production. Alors on n&rsquo;a plus besoin de <code>fingers_crossed</code> du tout — si info et au-dessus vont directement vers stdout, rien n&rsquo;est jamais bufferisé.</p>
<p>L&rsquo;approche retenue : supprimer <code>fingers_crossed</code>, monter le niveau par défaut à <code>warning</code>, garder un override debug disponible via variable d&rsquo;env pour les investigations ciblées. Les logs qui comptent apparaissent immédiatement. Ceux qui ne comptent pas ne sont jamais écrits. Rien n&rsquo;est bufferisé.</p>
<h2 id="les-crashes-ne-flushent-pas">Les crashes ne flushent pas</h2>
<p>Le Facteur XI et le Facteur IX se rejoignent au même point : un process qui meurt en plein milieu d&rsquo;une requête. <a href="/2026/05/16/the-cache-that-was-lying-to-us/">un autre article de cette série</a>
 décrivait l&rsquo;illusion d&rsquo;un service qui fonctionnait parfaitement sur un pod mais se comportait silencieusement mal sur deux. C&rsquo;est la même illusion, un niveau au-dessus : un service qui semblait logger correctement, jusqu&rsquo;au moment où il en avait le plus besoin.</p>
<p>La règle pour Monolog en production est sans appel : si ça n&rsquo;atteint pas stdout ou stderr avant que le process quitte, ça n&rsquo;existe pas. Un handler de fichier à l&rsquo;intérieur d&rsquo;un container est invisible pour le collecteur de logs et meurt avec le pod. Un buffer <code>fingers_crossed</code> est invisible pour le collecteur de logs et meurt avec le process.</p>
<p>La production tend à créer les conditions où on a le plus besoin des logs — pression OOM, défaillances en cascade, mauvais déploiements — et c&rsquo;est exactement les conditions où ces deux patterns échouent simultanément. Écrire vers stdout, adopter un niveau par défaut qui ne nécessite pas de bufferisation, et rendre l&rsquo;override disponible pour quand on en a vraiment besoin. Les logs seront là. Ils n&rsquo;attendront pas un seuil d&rsquo;erreur qui ne se déclenche jamais.</p>
]]></content:encoded></item><item><title>Ce qui survit au build</title><link>https://guillaumedelre.github.io/fr/2026/05/14/ce-qui-survit-au-build/</link><pubDate>Thu, 14 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/14/ce-qui-survit-au-build/</guid><description>Part 2 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Comment des fichiers .env committés alimentent un build qui grave les credentials dans les layers Docker — et ce qu&amp;#39;il faut pour vider le fichier jusqu&amp;#39;à quatre lignes.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>À un moment de l&rsquo;audit de migration cloud, quelqu&rsquo;un a lancé ç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-bash" data-lang="bash"><span style="display:flex;"><span>docker run --rm &lt;image&gt; php -r <span style="color:#e6db74">&#34;var_dump(require &#39;.env.local.php&#39;);&#34;</span>
</span></span></code></pre></div><p>La sortie montrait tout ce que <code>composer dump-env prod</code> avait compilé dans l&rsquo;image au moment du build. Ce qui voulait dire tout ce qui se trouvait dans le fichier <code>.env</code> quand l&rsquo;image avait été construite. Ce qui voulait dire, entre autres, ça :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">INFLUXDB_INIT_ADMIN_TOKEN=&lt;influxdb-admin-token&gt;
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=admin123
BLACKFIRE_CLIENT_ID=&lt;blackfire-client-id&gt;
BLACKFIRE_CLIENT_TOKEN=&lt;blackfire-client-token&gt;
BLACKFIRE_SERVER_ID=&lt;blackfire-server-id&gt;
BLACKFIRE_SERVER_TOKEN=&lt;blackfire-server-token&gt;
NGROK_AUTHTOKEN=replace-me-optionnal
</code></pre><p>Vingt-cinq variables au total. Chaque credential accumulé dans le <code>.env</code> racine sur trois ans, désormais permanent dans un layer d&rsquo;image.</p>
<h2 id="comment-dump-env-fonctionne">Comment <code>dump-env</code> fonctionne</h2>
<p><code>composer dump-env prod</code> est une optimisation Symfony légitime. Au lieu de parser les fichiers <code>.env</code> à chaque requête, le runtime charge un tableau PHP pré-compilé depuis <code>.env.local.php</code>. Plus rapide et plus simple.</p>
<p>Le problème, c&rsquo;est ce qu&rsquo;il lit. Le Dockerfile copie le dépôt dans l&rsquo;image avec <code>COPY . ./</code>, <code>.env</code> inclus. Ensuite <code>dump-env prod</code> lit ce fichier et compile chaque variable dans <code>.env.local.php</code>. L&rsquo;image est livrée avec une capture figée des credentials qui se trouvaient dans <code>.env</code> au moment du build.</p>
<p>Les layers Docker sont des archives immuables. Même si une étape ultérieure supprimait <code>.env</code> du système de fichiers du container, le layer qui le contient existerait toujours dans l&rsquo;image. <code>docker save &lt;image&gt;</code> produit une archive tar de chaque layer ; extraire un fichier spécifique de n&rsquo;importe quel point de l&rsquo;historique de build est une opération simple. Les credentials sont invisibles à l&rsquo;exécution. Ils ne sont pas partis.</p>
<p>Le <a href="https://12factor.net/build-release-run" target="_blank" rel="noopener noreferrer">Facteur V</a>
 est explicite là-dessus : un artefact de build doit être agnostique à l&rsquo;environnement, la config arrivant à l&rsquo;étape de release depuis l&rsquo;extérieur. Dès que des credentials sont compilés dedans, l&rsquo;image n&rsquo;est plus portable. On ne peut plus la promouvoir entre environnements. On builde deux fois en espérant que le deuxième se comporte comme le premier.</p>
<h2 id="comment-vingt-cinq-variables-saccumulent">Comment vingt-cinq variables s&rsquo;accumulent</h2>
<p>Avant de voir comment on a réparé ça, il vaut la peine de comprendre comment on en est arrivé là.</p>
<p>Les tokens <code>BLACKFIRE_*</code> sont le cas facile à comprendre. Un membre de l&rsquo;équipe configure le profiling, a besoin de partager la configuration, et le dépôt est déjà ouvert à tout le monde. Une ligne dans <code>.env</code> est la voie de moindre résistance. Les credentials InfluxDB et Grafana suivent la même logique — outillage partagé, dépôt partagé, un commit.</p>
<p>Puis il y a les variables qui révèlent une autre dérive. Dans certains <code>.env</code> de services :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__RATINGS__SERIALS=&#39;{&#34;marque1&#34;:{&#34;fr&#34;:&#34;12345&#34;},...}&#39;  # ~40 lignes de JSON
APP__YOUTUBE__CREDENTIALS=&#39;{&#34;marque1&#34;:{&#34;client_id&#34;:&#34;xxx&#34;,&#34;refresh_token&#34;:&#34;yyy&#34;},...}&#39;
</code></pre><p>Des numéros de série pour la mesure d&rsquo;audience. Des refresh tokens YouTube par marque. Ce ne sont pas des secrets au sens des tokens Blackfire. Ce sont des données métier — le genre de valeurs qui varient entre marques et environnements, que quelqu&rsquo;un a décidé de versionner dans <code>.env</code> parce qu&rsquo;elles se comportaient comme de la configuration et que <code>.env</code> était l&rsquo;endroit où vivait la configuration.</p>
<p>Vingt-cinq variables, c&rsquo;est la somme de décisions incrémentales, dont aucune ne semblait fausse isolément. Le problème est structurel : quand <code>.env</code> est la seule réponse disponible, tout finit par y ressembler.</p>
<h2 id="où-les-choses-appartiennent-vraiment">Où les choses appartiennent vraiment</h2>
<p>Vider le fichier exigeait de répondre à une question pour chaque variable : <em>où est-ce que ça appartient vraiment ?</em></p>
<p>Les réponses ont révélé trois catégories que l&rsquo;équipe n&rsquo;avait jamais explicitement nommées :</p>
<p><strong>La config statique</strong> vit dans le code. Règles métier, logique de routing, fichiers de paramètres Symfony — tout ce qui ne varie pas entre les déploiements. Un changement exige un rebuild. Les blocs JSON de numéros de série se sont révélés ne pas être de la config statique du tout : ils étaient interrogés depuis un service Config dédié à l&rsquo;exécution. Ils n&rsquo;avaient rien à faire dans un fichier.</p>
<p><strong>La config environnementale</strong> varie entre les déploiements : hostnames, chaînes de connexion, credentials de services tiers. C&rsquo;est ce que le <a href="https://12factor.net/config" target="_blank" rel="noopener noreferrer">Facteur III</a>
 désigne par &ldquo;config dans les variables d&rsquo;environnement&rdquo; — de vraies variables au niveau OS, injectées à l&rsquo;exécution, jamais des fichiers qui voyagent avec le code. Dans Kubernetes, c&rsquo;est un ConfigMap pour les valeurs non sensibles et un Kubernetes Secret pour les credentials. Le choix retenu pour les secrets a été SOPS — les credentials sont chiffrés et committés dans git, plutôt que stockés dans un coffre-fort externe comme Azure Key Vault ou HashiCorp Vault. Un coffre-fort échange la simplicité contre l&rsquo;auditabilité : rotation automatique, logs d&rsquo;audit centralisés, accès via workload identity sans clé à protéger. SOPS échange ces capacités contre un modèle opérationnel plus simple — pas de service externe à interroger au déploiement, les secrets transitent par le processus de review normal du code, l&rsquo;historique git fait office de piste d&rsquo;audit. Les contreparties acceptées sont la rotation manuelle et la responsabilité de protéger la clé de déchiffrement elle-même. Pour la taille de l&rsquo;équipe, le compromis était délibéré.</p>
<p><strong>La config dynamique</strong> change sans déploiement : paramètres éditoriaux, seuils par marque, configuration de modération de contenu. Elle appartient à une base de données, gérée via le service Config de l&rsquo;application. Une partie de ce qui s&rsquo;était accumulé dans les <code>.env</code> de services était cette catégorie depuis le début, passant pour des valeurs par défaut statiques parce qu&rsquo;elle changeait assez rarement pour que personne ne le remarque.</p>
<p>Une fois les catégories nommées, les variables se sont triées. Le <code>.env</code> racine est arrivé à quatre lignes :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">DOMAIN=platform.127.0.0.1.sslip.io
XDEBUG_MODE=off
SERVER_NAME=:80
APP_ENV=dev
</code></pre><p>Des valeurs par défaut sûres. Rien de sensible. <code>dump-env prod</code> compile maintenant des chaînes vides ; les vraies valeurs arrivent à l&rsquo;exécution depuis Kubernetes.</p>
<h2 id="limage-postgresql">L&rsquo;image PostgreSQL</h2>
<p>L&rsquo;image PostgreSQL utilisée en CI a un mot de passe codé en dur :</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-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">postgres:15</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">ENV</span> POSTGRES_PASSWORD<span style="color:#f92672">=</span>admin123
</span></span></code></pre></div><p>Ça ressemble au même problème. Ce n&rsquo;en est pas un, parce que le modèle de menace est différent. La base CI est éphémère — elle existe le temps d&rsquo;un run de pipeline, ne contient pas de vraies données, tourne dans un réseau isolé. Un mot de passe codé en dur sur une base de test jetable est un risque acceptable, pas une entorse à la règle.</p>
<p>En production, la question ne se pose pas : la plateforme utilise Azure Flexible Server, un service PostgreSQL managé. Il n&rsquo;y a pas d&rsquo;image Docker. Les credentials arrivent via injection dans les charts Helm, sans jamais toucher un layer.</p>
<h2 id="ce-qui-survit-au-build-maintenant">Ce qui survit au build maintenant</h2>
<p>L&rsquo;image qui part en production contient maintenant une garantie : <code>var_dump(require '.env.local.php')</code> ne retourne que des chaînes vides et des valeurs par défaut sûres. Les credentials ne sont pas là parce qu&rsquo;ils n&rsquo;y ont jamais été mis — ils arrivent à l&rsquo;exécution, depuis l&rsquo;extérieur.</p>
<p>C&rsquo;est la frontière de responsabilité que <code>dump-env</code> avait silencieusement effacée : l&rsquo;image est l&rsquo;application, le runtime est l&rsquo;environnement. Ils ne devraient pas connaître les secrets de l&rsquo;autre.</p>
]]></content:encoded></item><item><title>Le fantôme du runner CI</title><link>https://guillaumedelre.github.io/fr/2026/05/14/le-fant%C3%B4me-du-runner-ci/</link><pubDate>Thu, 14 May 2026 10:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/05/14/le-fant%C3%B4me-du-runner-ci/</guid><description>Part 1 of 8 in &amp;quot;Symfony vers le Cloud : Douze Facteurs, Treize Services&amp;quot;: Comment un chemin pointant vers un runner CI dans la config de production a révélé une dette de stockage — et comment les lazy adapters Flysystem l&amp;#39;ont résolue.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__COLD_STORAGE__FILESYSTEM_PATH=&#34;/home/jenkins-slave/share_media/media&#34;
APP__COLD_STORAGE__FILESYSTEM_PATH_CACHE=&#34;/home/jenkins-slave/share_media/media/cache&#34;
APP__COLD_STORAGE__RAW_IMAGE_PATH=&#34;/home/jenkins-slave/share_media/media_raw&#34;
APP__SHARE_STORAGE__FILESYSTEM_PATH=&#34;/home/jenkins-slave/share_storage&#34;
</code></pre><p>Ces lignes se trouvaient dans le <code>.env</code> de production du service media. Pas le staging. Pas un override local. La production, committée dans le dépôt, lue à chaque démarrage.</p>
<p>Les chemins se terminent là où on s&rsquo;y attendrait : <code>/media</code>, <code>/share_storage</code>. Ils commencent ailleurs : <code>/home/jenkins-slave</code>, le répertoire home d&rsquo;un runner CI issu d&rsquo;une ancienne installation Jenkins.</p>
<h2 id="comment-le-home-dun-runner-atterrit-dans-la-config-de-production">Comment le home d&rsquo;un runner atterrit dans la config de production</h2>
<p>La plateforme avait grandi depuis une seule machine. Un serveur faisait tout tourner — l&rsquo;application, le runner CI, la base de données, le stockage de fichiers. Les fichiers transitaient entre l&rsquo;app et le système CI via NFS : un répertoire monté sur le même hôte, accessible aux containers comme au runner.</p>
<p>Le chemin <code>/home/jenkins-slave/share_media</code> était là où le partage NFS atterrissait sur cette machine. Quand l&rsquo;équipe a migré vers Docker Compose, les containers ont hérité du montage NFS. Le chemin est entré dans le <code>.env</code> parce que l&rsquo;application devait savoir où trouver les fichiers. Personne ne l&rsquo;a changé parce que ça marchait. Le montage était toujours là. Le chemin était valide. L&rsquo;application démarrait. Les fichiers apparaissaient où ils devaient.</p>
<p>Trois ans plus tard, personne n&rsquo;y pensait plus du tout. C&rsquo;était juste comme ça que le chemin media était configuré.</p>
<h2 id="ce-que-kubectl-apply-a-trouvé">Ce que kubectl apply a trouvé</h2>
<p>Le premier <code>kubectl apply</code> du service media s&rsquo;est terminé avec un pod bloqué en CrashLoopBackOff. Le container démarrait. L&rsquo;entrypoint tournait. L&rsquo;application essayait d&rsquo;accéder à <code>/home/jenkins-slave/share_media/media</code>. Fichier ou répertoire inexistant. Pas de montage NFS. Pas de runner.</p>
<p>Le chemin ne documentait pas une décision de design. Il documentait la machine qui tournait par hasard au moment où le <code>.env</code> avait été écrit.</p>
<p>C&rsquo;est exactement le problème que le <a href="https://12factor.net/backing-services" target="_blank" rel="noopener noreferrer">Facteur IV</a>
 de l&rsquo;application twelve-factor décrit. Les backing services — stockage, files, bases de données — doivent être des ressources attachées, configurées via URL ou chaîne de connexion, interchangeables entre environnements sans toucher au code. Un chemin de fichier sur un hôte partagé n&rsquo;est pas un backing service. C&rsquo;est une hypothèse physique sur la machine. Quand la machine change, l&rsquo;hypothèse lâche.</p>
<h2 id="le-chemin-était-le-symptôme">Le chemin était le symptôme</h2>
<p>La première étape évidente était de supprimer la référence au runner :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__COLD_STORAGE__FILESYSTEM_PATH=&#34;/share_media/media&#34;
APP__SHARE_STORAGE__FILESYSTEM_PATH=&#34;/share_storage&#34;
</code></pre><p>Plus propre. Plus de références CI dans une config de production. Toujours incorrect. L&rsquo;application supposait encore un système de fichiers POSIX — soit un volume monté, soit un répertoire sur le nœud. Dans Kubernetes, un volume partagé entre plusieurs pods nécessite un PersistentVolumeClaim en mode <code>ReadWriteMany</code>. La plupart des fournisseurs de stockage ne le supportent pas. Ceux qui le font ont tendance à être lents et coûteux. Et même là où ça fonctionne, on a juste remplacé une hypothèse sur le système de fichiers par une autre.</p>
<p>Renommer le chemin gagnait du temps. Ça ne réglait pas le problème.</p>
<p>Le problème, c&rsquo;est qu&rsquo;environ douze téraoctets d&rsquo;images — originaux et déclinaisons pré-générées dans différents formats — pour plusieurs marques éditoriales — étaient traités comme un répertoire. Un répertoire ne se monte pas proprement sur plusieurs pods. Un backing service, si.</p>
<h2 id="flysystem-comme-forme-de-la-solution">Flysystem comme forme de la solution</h2>
<p>Le service media avait déjà Flysystem de configuré. Trois adaptateurs concrets — système de fichiers local, AWS S3, Azure Blob — et un adaptateur lazy par-dessus :</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/flysystem.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">flysystem</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storages</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">media.storage.local</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">adapter</span>: <span style="color:#e6db74">&#39;local&#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">directory</span>: <span style="color:#e6db74">&#34;/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">media.storage.aws</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">adapter</span>: <span style="color:#e6db74">&#39;aws&#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">client</span>: <span style="color:#e6db74">&#39;aws_client_service&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">bucket</span>: <span style="color:#e6db74">&#39;media&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">streamReads</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">media.storage</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">adapter</span>: <span style="color:#e6db74">&#39;lazy&#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">source</span>: <span style="color:#e6db74">&#39;%env(APP__FLYSYSTEM_MEDIA_STORAGE)%&#39;</span>
</span></span></code></pre></div><p>Tout le code de l&rsquo;application dépend de <code>media.storage</code>. Il ne sait pas si les fichiers vivent sur le système de fichiers ou dans un bucket cloud. Une variable d&rsquo;environnement détermine quel backend est actif :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__FLYSYSTEM_MEDIA_STORAGE=media.storage.aws   # production
APP__FLYSYSTEM_MEDIA_STORAGE=media.storage.local  # fallback local toujours disponible
</code></pre><p>Le chemin est parti. L&rsquo;hypothèse sur le système de fichiers est partie. Ce qui reste, c&rsquo;est un nom de service — une ressource attachée au sens twelve-factor, configurable sans rebuilder l&rsquo;image.</p>
<p>Le même pattern s&rsquo;étend au cache de vignettes. <a href="https://github.com/liip/LiipImagineBundle" target="_blank" rel="noopener noreferrer">LiipImagine</a>
 génère des images redimensionnées à la demande ; les originaux et le cache généré passent par des adaptateurs Flysystem séparé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">liip_imagine</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">loaders</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">flysystem</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">filesystem_service</span>: <span style="color:#e6db74">&#39;media.storage&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default_cache</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">flysystem</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">filesystem_service</span>: <span style="color:#e6db74">&#39;media.cache.storage&#39;</span>
</span></span></code></pre></div><p>Deux variables d&rsquo;environnement, deux buckets. Toute la chaîne — recevoir l&rsquo;upload, stocker l&rsquo;original, générer la vignette, la mettre en cache — est portable vers le cloud sans toucher une ligne de PHP.</p>
<p>Ce que l&rsquo;article ne couvre pas, c&rsquo;est le déplacement des données. Le lazy adapter change une variable d&rsquo;environnement. Faire passer douze téraoctets d&rsquo;un montage NFS vers un bucket S3, c&rsquo;est un autre projet — une fenêtre de migration, une double-écriture pendant le cutover, une vérification qu&rsquo;il ne manque rien.</p>
<h2 id="ce-que-minio-rend-possible-en-ci">Ce que Minio rend possible en CI</h2>
<p>La production utilise S3. Le développement local utilise <a href="https://min.io/" target="_blank" rel="noopener noreferrer">Minio</a>
, un stockage objet compatible S3 qui tourne dans un container Docker. L&rsquo;adaptateur AWS parle à Minio en local et à S3 en production. L&rsquo;application ne voit pas la différence :</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv"># local/CI
APP__FLYSYSTEM_MEDIA_STORAGE=media.storage.aws
APP__MINIO_ENDPOINT=http://minio:9000
APP__MINIO_ACCESS_KEY=minioadmin
APP__MINIO_SECRET_KEY=minioadmin
</code></pre><p>Le même code, le même adaptateur, un endpoint différent. Pas de mock, pas de chemins de test spéciaux, pas de branches conditionnelles par environnement.</p>
<p>Mais la configuration CI va un cran plus loin. L&rsquo;image Minio utilisée dans le pipeline n&rsquo;est pas l&rsquo;image officielle upstream — c&rsquo;est une image custom buildée avec des fixtures de test préchargées :</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-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">minio/minio:latest</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">COPY</span> tests/fixtures/ /fixtures_media/<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Chaque run CI démarre avec une instance Minio qui contient déjà les données attendues par la suite de tests. Pas de script de setup, pas de commande de seed, pas d&rsquo;étape &ldquo;attendre le chargement des fixtures&rdquo; avant que les tests commencent. L&rsquo;état initial de l&rsquo;environnement de test fait partie de l&rsquo;artefact de build.</p>
<p>Le <a href="https://12factor.net/build-release-run" target="_blank" rel="noopener noreferrer">Facteur V</a>
 appliqué à l&rsquo;infrastructure de test : l&rsquo;état de l&rsquo;environnement est buildé, versionné, immuable. Le pipeline CI construit l&rsquo;image Minio depuis la même source et au même commit que l&rsquo;image applicative. Les fixtures de test et le code qui les exploite sont toujours synchronisés.</p>
<h2 id="le-compromis-s3-honnêtement">Le compromis S3, honnêtement</h2>
<p>S3 introduit un coût de latence que le stockage local n&rsquo;a pas. Les premières données d&rsquo;un fichier prennent 10 à 30 millisecondes à arriver depuis S3 — c&rsquo;est la latence first-byte documentée du service, pas une mesure sur ce trafic spécifique.</p>
<p>À 300 requêtes par seconde, le raisonnement pour accepter ce compromis était le suivant : la majorité des lectures touche des vignettes déjà générées dans le cache S3, pas les fichiers originaux. Une image fraîchement uploadée paie la pénalité du cold miss une fois, à la première demande de vignette. Tout ce qui suit est un cache hit. Savoir si la latence de queue sous charge réelle confirmait ce raisonnement nécessitait des tests de charge suivis séparément — la décision d&rsquo;architecture et la validation étaient découplées.</p>
<p>Le compromis a été accepté : comportement prévisible sur plusieurs pods, pas de problèmes d&rsquo;état partagé, une couche de stockage qui scale sans coordination. L&rsquo;histoire complète des mesures appartient au rapport de tests de performance, pas ici.</p>
<h2 id="le-fantôme-sen-va">Le fantôme s&rsquo;en va</h2>
<p>Le chemin <code>/home/jenkins-slave</code> n&rsquo;apparaît plus dans la configuration. Mais ce à quoi il pointait était un couplage qui précédait Docker, précédait les microservices, précédait n&rsquo;importe quelle conversation sur la migration cloud. Le runner CI et l&rsquo;application de production partageaient un système de fichiers parce qu&rsquo;ils vivaient sur la même machine. Personne ne l&rsquo;avait conçu comme ça. Ça s&rsquo;était accumulé.</p>
<p>Une erreur <code>kubectl apply</code> sur un chemin qui n&rsquo;aurait pas dû exister a forcé la question : pourquoi cette application suppose-t-elle qu&rsquo;un runner CI spécifique est présent sur l&rsquo;hôte ? La réponse était &ldquo;parce que ça a toujours été comme ça.&rdquo; Ce n&rsquo;est pas une raison. C&rsquo;est une histoire.</p>
<p>Renommer le chemin était un correctif en carton. L&rsquo;adaptateur lazy de Flysystem était la vraie réponse — pas parce qu&rsquo;il est plus élégant, mais parce qu&rsquo;il fait du backend de stockage une décision qui appartient à l&rsquo;environnement, pas à l&rsquo;application. Le container démarre, lit une variable, se connecte à ce qui est à l&rsquo;autre bout. Il ne sait pas si c&rsquo;est un bucket dans un datacenter ou un container sur un laptop.</p>
<p>Le répertoire home du runner a disparu de la config. Ce qui l&rsquo;a remplacé, c&rsquo;est un nom de service. C&rsquo;est la différence.</p>
]]></content:encoded></item><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>PHP 8.5 : l'opérateur pipe, une bibliothèque URI et beaucoup de nettoyage</title><link>https://guillaumedelre.github.io/fr/2026/01/04/php-8.5-lop%C3%A9rateur-pipe-une-biblioth%C3%A8que-uri-et-beaucoup-de-nettoyage/</link><pubDate>Sun, 04 Jan 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/01/04/php-8.5-lop%C3%A9rateur-pipe-une-biblioth%C3%A8que-uri-et-beaucoup-de-nettoyage/</guid><description>Part 11 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 8.5 ajoute un opérateur pipe pour des pipelines fonctionnels lisibles et une classe URI native qui met fin au parsing fragile de strings.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 8.5 est sorti le 20 novembre. Deux fonctionnalités définissent cette version : l&rsquo;opérateur pipe et l&rsquo;extension URI. Elles résolvent des problèmes différents, mais partagent la même motivation : rendre les opérations courantes moins maladroites à exprimer.</p>
<h2 id="lopérateur-pipe">L&rsquo;opérateur pipe</h2>
<p>Les pipelines fonctionnels en PHP ont toujours été un bazar. Enchaîner des transformations nécessitait soit d&rsquo;imbriquer les appels de fonctions à l&rsquo;envers, soit de les découper en variables intermédiaires :</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 — se lit de droite à gauche
</span></span></span><span style="display:flex;"><span>$result <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_sum</span>(<span style="color:#a6e22e">array_map</span>(<span style="color:#e6db74">&#39;strlen&#39;</span>, <span style="color:#a6e22e">array_filter</span>($strings, <span style="color:#e6db74">&#39;strlen&#39;</span>)));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ou verbeux mais lisible
</span></span></span><span style="display:flex;"><span>$filtered   <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_filter</span>($strings, <span style="color:#e6db74">&#39;strlen&#39;</span>);
</span></span><span style="display:flex;"><span>$lengths    <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_map</span>(<span style="color:#e6db74">&#39;strlen&#39;</span>, $filtered);
</span></span><span style="display:flex;"><span>$result     <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_sum</span>($lengths);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// après — se lit de gauche à droite
</span></span></span><span style="display:flex;"><span>$result <span style="color:#f92672">=</span> $strings
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">array_filter</span>(<span style="color:#f92672">?</span>, <span style="color:#e6db74">&#39;strlen&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">array_map</span>(<span style="color:#e6db74">&#39;strlen&#39;</span>, <span style="color:#f92672">?</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">array_sum</span>(<span style="color:#f92672">?</span>);
</span></span></code></pre></div><p>L&rsquo;opérateur <code>|&gt;</code> passe la valeur de gauche dans l&rsquo;expression de droite. Le placeholder <code>?</code> marque où elle va. Les pipelines se lisent maintenant dans l&rsquo;ordre où les opérations se produisent : gauche à droite, haut en bas.</p>
<p>Ça se marie bien avec les callables de première classe de PHP 8.1. Les deux fonctionnalités se composent bien :</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>$result <span style="color:#f92672">=</span> $input <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">trim</span>(<span style="color:#f92672">...</span>) <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">strtolower</span>(<span style="color:#f92672">...</span>) <span style="color:#f92672">|&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">normalize</span>(<span style="color:#f92672">...</span>);
</span></span></code></pre></div><h2 id="lextension-uri">L&rsquo;extension URI</h2>
<p>Gérer les URIs en PHP a toujours signifié soit se tourner vers une bibliothèque tierce, soit assembler <code>parse_url()</code> (retourne un tableau, pas un objet), <code>http_build_query()</code>, et de la concaténation manuelle de strings.</p>
<p>La nouvelle extension <code>Uri</code> donne une vraie API orientée objet :</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>$uri <span style="color:#f92672">=</span> <span style="color:#a6e22e">Uri\Uri</span><span style="color:#f92672">::</span><span style="color:#a6e22e">parse</span>(<span style="color:#e6db74">&#39;https://example.com/path?query=value#fragment&#39;</span>);
</span></span><span style="display:flex;"><span>$modified <span style="color:#f92672">=</span> $uri<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">withPath</span>(<span style="color:#e6db74">&#39;/new-path&#39;</span>)<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">withQuery</span>(<span style="color:#e6db74">&#39;key=val&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $modified; <span style="color:#75715e">// https://example.com/new-path?key=val#fragment
</span></span></span></code></pre></div><p>Des objets-valeur immutables, un parsing conforme aux RFC, modifier des composants individuels sans parser et reconstruire la string entière. Attendu depuis longtemps.</p>
<h2 id="nodiscard">#[\NoDiscard]</h2>
<p>Un nouvel attribut qui génère un avertissement quand la valeur de retour est ignorée :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[\NoDiscard(&#34;Use the returned collection, the original is unchanged&#34;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">filter</span>(<span style="color:#a6e22e">callable</span> $fn)<span style="color:#f92672">:</span> <span style="color:#66d9ef">static</span> { <span style="color:#f92672">...</span> }
</span></span></code></pre></div><p>Utile pour les méthodes immutables où ignorer la valeur de retour est presque certainement un bug. Courant dans d&rsquo;autres langages depuis des années, maintenant en PHP où ça appartient.</p>
<h2 id="clone-with">clone with</h2>
<p>Cloner un objet avec des propriétés modifiées sans utiliser de property hooks ni une méthode <code>with()</code> personnalisée :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$updated <span style="color:#f92672">=</span> <span style="color:#66d9ef">clone</span>($point) <span style="color:#a6e22e">with</span> { <span style="color:#a6e22e">x</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">10</span>, <span style="color:#a6e22e">y</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">20</span> };
</span></span></code></pre></div><p>Syntaxe propre pour un pattern que les objets readonly nécessitaient : on clone pour &ldquo;modifier&rdquo; puisque la mutation directe n&rsquo;est pas permise.</p>
<p>PHP 8.5 a une tendance fonctionnelle. L&rsquo;opérateur pipe et l&rsquo;extension URI ensemble rendent le code de transformation de données significativement plus lisible. Le langage continue dans une direction cohérente.</p>
<h2 id="les-closures-dans-les-expressions-constantes">Les closures dans les expressions constantes</h2>
<p>Une contrainte qui existait depuis PHP 5 : les expressions constantes (arguments d&rsquo;attributs, valeurs par défaut de propriétés, valeurs par défaut de paramètres, déclarations <code>const</code>) ne pouvaient pas contenir de closures ni de callables de première classe. 8.5 supprime ç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:#75715e">#[Validate(fn($v) =&gt; $v &gt; 0)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#a6e22e">int</span> $count <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">NORMALIZER</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">strtolower</span>(<span style="color:#f92672">...</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">Config</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">Closure</span> $transform <span style="color:#f92672">=</span> <span style="color:#a6e22e">trim</span>(<span style="color:#f92672">...</span>),
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>C&rsquo;est la pièce manquante qui rend les attributs vraiment expressifs pour les règles de validation et de transformation. Avant 8.5, il fallait passer des noms de classes ou des références string aux attributs et laisser le framework les retrouver. Maintenant le callable vit directement dans l&rsquo;attribut.</p>
<h2 id="les-attributs-sur-les-constantes">Les attributs sur les constantes</h2>
<p>L&rsquo;attribut <code>#[\Deprecated]</code> de 8.4 ne pouvait pas être appliqué aux déclarations <code>const</code>. 8.5 ajoute le support des attributs pour les constantes en général :</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">const</span> <span style="color:#66d9ef">OLD_LIMIT</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">100</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[\Deprecated(&#39;Use RATE_LIMIT instead&#39;, since: &#39;3.0&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">API_TIMEOUT</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">30</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">RATE_LIMIT</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">60</span>;
</span></span></code></pre></div><p><code>ReflectionConstant</code>, une nouvelle classe de réflexion dans 8.5, expose <code>getAttributes()</code> pour que les outils puissent les lire. Combiné avec les closures dans les expressions constantes, les attributs sur les constantes deviennent une vraie couche de métadonnées pour les valeurs à la compilation.</p>
<h2 id="override-sétend-aux-propriétés">#[\Override] s&rsquo;étend aux propriétés</h2>
<p>PHP 8.3 a apporté <code>#[\Override]</code> pour les méthodes. 8.5 l&rsquo;étend aux propriété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">Base</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;default&#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">class</span> <span style="color:#a6e22e">Derived</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Base</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</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;derived&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Si la propriété n&rsquo;existe pas dans le parent, PHP lève une erreur. Particulièrement utile avec les property hooks de 8.4 : on peut maintenant signaler qu&rsquo;une propriété hookée surcharge intentionnellement celle d&rsquo;un parent.</p>
<h2 id="la-visibilité-asymétrique-statique">La visibilité asymétrique statique</h2>
<p>8.4 a introduit la visibilité asymétrique (<code>public private(set)</code>) pour les propriétés d&rsquo;instance. 8.5 l&rsquo;apporte aussi aux propriétés <code>static</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">class</span> <span style="color:#a6e22e">Registry</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">private</span>(<span style="color:#a6e22e">set</span>) <span style="color:#66d9ef">array</span> $items <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">register</span>(<span style="color:#a6e22e">string</span> $key, <span style="color:#a6e22e">mixed</span> $value)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">self</span><span style="color:#f92672">::</span>$items[$key] <span style="color:#f92672">=</span> $value;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">Registry</span><span style="color:#f92672">::</span>$items[<span style="color:#e6db74">&#39;foo&#39;</span>]; <span style="color:#75715e">// lisible
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">Registry</span><span style="color:#f92672">::</span>$items[<span style="color:#e6db74">&#39;bar&#39;</span>] <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; <span style="color:#75715e">// Error: cannot write outside class
</span></span></span></code></pre></div><p>Pattern direct : exposer une collection statique en lecture, bloquer la mutation externe.</p>
<h2 id="la-promotion-de-constructeur-pour-les-propriétés-final">La promotion de constructeur pour les propriétés final</h2>
<p>La promotion de propriétés dans les constructeurs existe depuis PHP 8.0. Le modificateur <code>final</code> sur les propriétés promuées était la pièce manquante, 8.5 l&rsquo;ajoute :</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">ValueObject</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:#66d9ef">final</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $id,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">final</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $name,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Une sous-classe ne peut pas surcharger <code>$id</code> ni <code>$name</code> avec une propriété du même nom. La combinaison <code>final readonly</code> sur les propriétés promuées rend les objets-valeur aussi verrouillés que possible sans sceller la classe entière.</p>
<h2 id="les-casts-dans-les-expressions-constantes">Les casts dans les expressions constantes</h2>
<p>Autre lacune dans les expressions constantes : pas de casts de type. 8.5 les permet :</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">const</span> <span style="color:#66d9ef">PRECISION</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">int</span>) <span style="color:#ae81ff">3.7</span>;      <span style="color:#75715e">// 3
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">THRESHOLD</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">float</span>) <span style="color:#e6db74">&#39;1.5&#39;</span>;  <span style="color:#75715e">// 1.5
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">FLAG</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">bool</span>) <span style="color:#ae81ff">1</span>;            <span style="color:#75715e">// true
</span></span></span></code></pre></div><p>Ça semble mineur jusqu&rsquo;à ce qu&rsquo;on ait des constantes de configuration dérivées de variables d&rsquo;environnement qui nécessitent une coercition de type directement à la déclaration.</p>
<h2 id="les-erreurs-fatales-incluent-les-backtraces">Les erreurs fatales incluent les backtraces</h2>
<p>Avant 8.5, une erreur fatale (mémoire insuffisante, dépassement de pile, erreur de type dans certains contextes) produisait un message sans contexte sur où dans le code ça s&rsquo;est passé. Trouver la cause signifiait insérer des logs de debug et reproduire.</p>
<p>8.5 ajoute des backtraces à la stack aux messages d&rsquo;erreur fatale, dans le même format que les backtraces d&rsquo;exceptions. Une nouvelle directive INI, <code>fatal_error_backtraces</code>, contrôle le comportement. Elle est activée par défaut.</p>
<h2 id="array_first-et-array_last">array_first() et array_last()</h2>
<p>PHP avait <code>reset()</code> et <code>end()</code> pour accéder au premier et dernier élément d&rsquo;un tableau depuis PHP 3. Les deux mutent le pointeur interne du tableau (pas sûr à appeler sur une référence), et ils retournent <code>false</code> pour les tableaux vides d&rsquo;une façon indiscernable d&rsquo;une valeur <code>false</code> stockée.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$values <span style="color:#f92672">=</span> [<span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">20</span>, <span style="color:#ae81ff">30</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$first <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_first</span>($values);  <span style="color:#75715e">// 10
</span></span></span><span style="display:flex;"><span>$last  <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_last</span>($values);   <span style="color:#75715e">// 30
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$first <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_first</span>([]);       <span style="color:#75715e">// null
</span></span></span></code></pre></div><p>Les nouvelles fonctions retournent <code>null</code> pour les tableaux vides, ne touchent pas le pointeur interne, et fonctionnent sur n&rsquo;importe quelle expression de tableau sans avoir besoin d&rsquo;une variable. <code>reset($this-&gt;getItems())</code> était un avertissement de dépréciation en attente de se produire.</p>
<h2 id="get_error_handler-et-get_exception_handler">get_error_handler() et get_exception_handler()</h2>
<p>PHP a <code>set_error_handler()</code> et <code>set_exception_handler()</code>. Obtenir le handler courant nécessitait soit de le stocker soi-même avant de le définir, soit d&rsquo;appeler <code>set_error_handler(null)</code> et de capturer ce qui revenait, ce qui effaçait aussi le handler au passage.</p>
<p>8.5 ajoute :</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>$current <span style="color:#f92672">=</span> <span style="color:#a6e22e">get_error_handler</span>();
</span></span><span style="display:flex;"><span>$current <span style="color:#f92672">=</span> <span style="color:#a6e22e">get_exception_handler</span>();
</span></span></code></pre></div><p>Pratique dans les chaînes de middleware où on veut envelopper le handler existant sans le perdre, ou dans les tests où on veut vérifier qu&rsquo;un handler a bien été installé.</p>
<h2 id="intllistformatter">IntlListFormatter</h2>
<p>Formater une liste avec les conjonctions appropriées à la locale nécessitait toujours un assemblage manuel de strings. 8.5 ajoute <code>IntlListFormatter</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>$formatter <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">IntlListFormatter</span>(<span style="color:#e6db74">&#39;en_US&#39;</span>, <span style="color:#a6e22e">IntlListFormatter</span><span style="color:#f92672">::</span><span style="color:#a6e22e">TYPE_AND</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $formatter<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">format</span>([<span style="color:#e6db74">&#39;apples&#39;</span>, <span style="color:#e6db74">&#39;oranges&#39;</span>, <span style="color:#e6db74">&#39;pears&#39;</span>]);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// &#34;apples, oranges, and pears&#34;
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$formatter <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">IntlListFormatter</span>(<span style="color:#e6db74">&#39;fr_FR&#39;</span>, <span style="color:#a6e22e">IntlListFormatter</span><span style="color:#f92672">::</span><span style="color:#a6e22e">TYPE_OR</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $formatter<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">format</span>([<span style="color:#e6db74">&#39;rouge&#39;</span>, <span style="color:#e6db74">&#39;bleu&#39;</span>, <span style="color:#e6db74">&#39;vert&#39;</span>]);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// &#34;rouge, bleu ou vert&#34;
</span></span></span></code></pre></div><p>La classe enveloppe le <code>ListFormatter</code> d&rsquo;ICU. Trois types : <code>TYPE_AND</code>, <code>TYPE_OR</code>, <code>TYPE_UNITS</code>. Les constantes de largeur contrôlent si on obtient &ldquo;et&rdquo; ou &ldquo;&amp;&rdquo;. Gestion de la virgule d&rsquo;Oxford, placement de conjonction spécifique à la locale, tout géré par ICU.</p>
<h2 id="filter_throw_on_failure-pour-filter_var">FILTER_THROW_ON_FAILURE pour filter_var()</h2>
<p><code>filter_var()</code> retourne <code>false</code> en cas d&rsquo;échec de validation, ce qui produit l&rsquo;ambiguïté classique <code>false vs null vs 0</code> quand on filtre des entrées non fiables. Un nouveau flag change ç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">try</span> {
</span></span><span style="display:flex;"><span>    $email <span style="color:#f92672">=</span> <span style="color:#a6e22e">filter_var</span>($input, <span style="color:#a6e22e">FILTER_VALIDATE_EMAIL</span>, <span style="color:#a6e22e">FILTER_THROW_ON_FAILURE</span>);
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">Filter\FilterFailedException</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// explicitement invalide, pas faussement false
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Les classes <code>Filter\FilterFailedException</code> et <code>Filter\FilterException</code> sont nouvelles dans 8.5. Le flag ne peut pas être combiné avec <code>FILTER_NULL_ON_FAILURE</code> : les comportements sont mutuellement exclusifs.</p>
<h2 id="les-dépréciations-qui-nettoient-des-années-de-dette-technique">Les dépréciations qui nettoient des années de dette technique</h2>
<p>L&rsquo;opérateur backtick (<code>`commande`</code> comme alias de <code>shell_exec()</code>) est déprécié. C&rsquo;est une syntaxe obscure qui surprend quiconque lit le code et est incohérente avec tous les autres appels de fonctions PHP.</p>
<p>Les noms de cast non canoniques (<code>(boolean)</code>, <code>(integer)</code>, <code>(double)</code>, <code>(binary)</code>) sont dépréciés au profit de leurs formes courtes : <code>(bool)</code>, <code>(int)</code>, <code>(float)</code>, <code>(string)</code>. Les formes longues sont non documentées depuis des années ; 8.5 commence la suppression formelle.</p>
<p>Les instructions <code>case</code> terminées par un point-virgule sont dépréciées :</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">// déprécié
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">switch</span> ($x) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">break</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">// correct
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">switch</span> ($x) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">break</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La forme avec point-virgule est syntaxiquement valide depuis PHP 4 mais personne ne l&rsquo;utilise intentionnellement. C&rsquo;est une faute de frappe que PHP acceptait.</p>
<p><code>__sleep()</code> et <code>__wakeup()</code> sont dépréciés au profit de <code>__serialize()</code> et <code>__unserialize()</code>, qui retournent et reçoivent des tableaux et se composent correctement avec l&rsquo;héritage. Les anciennes méthodes avaient une sémantique confuse autour de la visibilité des propriétés.</p>
<h2 id="max_memory_limit-plafonne-les-allocations-incontrôlées">max_memory_limit plafonne les allocations incontrôlées</h2>
<p>Une nouvelle directive INI accessible seulement au démarrage : <code>max_memory_limit</code>. Elle définit un plafond que <code>memory_limit</code> ne peut pas dépasser à l&rsquo;exécution. Si un script appelle <code>ini_set('memory_limit', '10G')</code> et que <code>max_memory_limit</code> est à <code>512M</code>, PHP avertit et plafonne la valeur.</p>
<p>Utile dans les environnements d&rsquo;hébergement partagé, ou partout où on veut s&rsquo;assurer qu&rsquo;un bug ou un payload malveillant ne peut pas convaincre PHP d&rsquo;élever sa propre limite et de dévorer toute la RAM de la machine.</p>
<h2 id="opcache-est-toujours-présent">Opcache est toujours présent</h2>
<p>Dans 8.5, Opcache est toujours compilé dans le binaire PHP et toujours chargé. L&rsquo;ancienne situation (Opcache comme extension chargeable qui pouvait ou non être présente selon la configuration de build) est révolue.</p>
<p>On peut toujours le désactiver : <code>opcache.enable=0</code> fonctionne bien. Ce qui change, c&rsquo;est la garantie que l&rsquo;API Opcache (<code>opcache_get_status()</code>, <code>opcache_invalidate()</code>, etc.) est toujours disponible, quelle que soit la façon dont PHP a été compilé. Tout code qui vérifie <code>extension_loaded('opcache')</code> avant d&rsquo;appeler les fonctions Opcache peut supprimer la vérification.</p>
]]></content:encoded></item><item><title>API Platform 4.2 : JSON streamer, ObjectMapper, et autoconfigure</title><link>https://guillaumedelre.github.io/fr/2025/09/18/api-platform-4.2-json-streamer-objectmapper-et-autoconfigure/</link><pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2025/09/18/api-platform-4.2-json-streamer-objectmapper-et-autoconfigure/</guid><description>Part 8 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 4.2 diffuse les grandes collections JSON sans buffering, remplace le câblage DTO manuel par ObjectMapper, et autoconfigure les ressources depuis leurs attributs de classe.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.2 est arrivé en septembre 2025. Trois changements se distinguent : un JSON streamer pour les grandes collections qui évite de bufferiser toute la réponse en mémoire, un ObjectMapper qui remplace le câblage manuel dans les flux DTO basés sur <code>stateOptions</code>, et l&rsquo;autoconfiguration de <code>#[ApiResource]</code> sans enregistrement de service explicite.</p>
<h2 id="json-streamer-pour-les-grandes-collections">JSON streamer pour les grandes collections</h2>
<p>Le serializer Symfony par défaut construit la réponse complète en mémoire avant de l&rsquo;écrire dans la sortie. Pour une collection de 10 000 éléments, cela signifie allouer un tableau PHP, le sérialiser en string, et garder les deux en mémoire jusqu&rsquo;au flush de la réponse. À grande échelle, c&rsquo;est la source des erreurs OOM qui forcent à ajouter de la pagination partout.</p>
<p>La 4.2 ajoute un encodeur JSON en streaming qui écrit la réponse de façon incrémentale :</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">api_platform</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">enable_json_streamer</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Avec le streaming activé, la réponse est écrite directement dans le buffer de sortie au fur et à mesure que chaque élément est sérialisé. L&rsquo;utilisation mémoire reste approximativement constante quelle que soit la taille de la collection. La contrepartie : on ne peut pas définir de headers de réponse après le début du streaming, et le code de statut HTTP doit être validé avant que le premier octet soit écrit.</p>
<h2 id="objectmapper-remplace-le-câblage-dto-manuel">ObjectMapper remplace le câblage DTO manuel</h2>
<p>La 3.1 avait introduit <code>stateOptions</code> avec <code>DoctrineOrmOptions</code> pour séparer la ressource API de l&rsquo;entité Doctrine. Le provider recevait des objets entité et le serializer les mappait sur le DTO. Ça fonctionnait, mais le mapping était implicite — le serializer utilisait les noms de propriétés pour faire correspondre les champs, et tout ce qui ne correspondait pas était soit ignoré soit causait une erreur de normalisation.</p>
<p>La 4.2 introduit <code>ObjectMapper</code>, une couche de mapping déclarative entre entités et DTOs :</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\ObjectMapper\Attribute\Map</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Map(source: BookEntity::class)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $title;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $authorName;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;attribut <code>#[Map]</code> indique à ObjectMapper que <code>BookDto</code> peut être peuplé depuis <code>BookEntity</code>. Les noms de champs sont mis en correspondance par convention ; les inadéquations sont déclarées explicitement au niveau de chaque proprié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">use</span> <span style="color:#a6e22e">Symfony\Component\ObjectMapper\Attribute\Map</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Map(source: BookEntity::class)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Map(source: &#39;author.fullName&#39;)]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $authorName;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La notation pointée traverse les objets imbriqués. Le mapping s&rsquo;exécute avant la sérialisation et remplace le comportement implicite de correspondance de propriétés du serializer. Les champs non mappés lèvent une erreur au moment de la configuration, pas à l&rsquo;exécution.</p>
<p>ObjectMapper fonctionne avec <code>stateOptions</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">ApiPlatform\Doctrine\Orm\State\Options</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\ObjectMapper\Attribute\Map</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">stateOptions</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Options</span>(<span style="color:#a6e22e">entityClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookEntity</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><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Map(source: BookEntity::class)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span> {}
</span></span></code></pre></div><p>Le provider récupère les objets <code>BookEntity</code> depuis Doctrine. ObjectMapper les convertit en instances <code>BookDto</code>. Le serializer écrit le DTO. Trois étapes distinctes, chacune avec un contrat clair.</p>
<h2 id="intégration-typeinfo-dans-toute-la-pile">Intégration TypeInfo dans toute la pile</h2>
<p>Symfony 7.1 a introduit le <a href="https://symfony.com/doc/current/components/type_info.html" target="_blank" rel="noopener noreferrer">composant TypeInfo</a>
, une couche unifiée d&rsquo;introspection des types qui comprend les types union, intersection, les collections génériques et les types nullable à travers la reflection, PHPDoc et la syntaxe PHP 8.x.</p>
<p>La 4.2 remplace la résolution de types interne d&rsquo;API Platform par TypeInfo. Cela affecte la génération de schémas de filtres, l&rsquo;inférence de schéma OpenAPI, et la coercition de types du serializer. Le bénéfice visible est que les types qui généraient auparavant des schémas OpenAPI incorrects ou manquants — <code>Collection&lt;int, Book&gt;</code>, <code>list&lt;string&gt;</code>, types intersection — produisent maintenant des schémas précis sans annotations <code>@ApiProperty</code> manuelles.</p>
<h2 id="autoconfigure-apiresource">Autoconfigure <code>#[ApiResource]</code></h2>
<p>Avant la 4.2, ajouter <code>#[ApiResource]</code> à une classe suffisait pour qu&rsquo;Hugo la découvre uniquement si la classe était dans un chemin scanné par le chargeur de ressources d&rsquo;API Platform. En dehors de ce chemin, une configuration de service explicite était nécessaire.</p>
<p>La 4.2 se branche sur le système d&rsquo;autoconfigure de Symfony. Toute classe taguée avec <code>#[ApiResource]</code> est automatiquement enregistrée comme ressource indépendamment de son emplacement, tant qu&rsquo;elle est dans un répertoire couvert par le scan de composants de Symfony. Aucune entrée dans <code>config/services.yaml</code> nécessaire.</p>
<p>Pour Laravel, l&rsquo;équivalent utilise l&rsquo;autoloading du service provider de Laravel — les modèles Eloquent avec <code>#[ApiResource]</code> sont récupérés automatiquement sans enregistrement manuel.</p>
<h2 id="doctrine-existsfilter">Doctrine ExistsFilter</h2>
<p>L&rsquo;<code>ExistsFilter</code> contraint une collection selon qu&rsquo;une relation ou un champ nullable est défini :</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">#[ApiFilter(ExistsFilter::class, properties: [&#39;publishedAt&#39;])]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p><code>GET /books?exists[publishedAt]=true</code> retourne les livres où <code>publishedAt</code> n&rsquo;est pas null. <code>exists[publishedAt]=false</code> retourne les livres où il l&rsquo;est.</p>
]]></content:encoded></item><item><title>API Platform 4.1 : paramètres de requête stricts, OpenAPI multi-spec, et limites GraphQL</title><link>https://guillaumedelre.github.io/fr/2025/02/28/api-platform-4.1-param%C3%A8tres-de-requ%C3%AAte-stricts-openapi-multi-spec-et-limites-graphql/</link><pubDate>Fri, 28 Feb 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2025/02/28/api-platform-4.1-param%C3%A8tres-de-requ%C3%AAte-stricts-openapi-multi-spec-et-limites-graphql/</guid><description>Part 7 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 4.1 formalise la validation stricte des paramètres de requête, introduit x-apiplatform-tag pour découper une API en plusieurs specs OpenAPI, et ajoute des limites de profondeur et complexité pour GraphQL.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.1 est arrivé en février 2025 avec un lot de fonctionnalités moins axées sur de nouvelles capacités que sur la mise en production des existantes. La validation stricte des paramètres de requête gagne une propriété de première classe. OpenAPI gagne un mécanisme pour découper les grandes APIs en specs séparées. GraphQL obtient les contrôles de prévention des abus qui lui manquaient.</p>
<h2 id="validation-stricte-des-paramètres-de-requête">Validation stricte des paramètres de requête</h2>
<p>La 3.3 avait introduit la validation des paramètres de requête en opt-in. La 3.4 avait déprécié le comportement permissif. La 4.1 la formalise avec une propriété native <code>strictQueryParameterValidation</code> sur les ressources et opérations : quand elle est à <code>true</code>, les paramètres de requête inconnus renvoient 400.</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">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\QueryParameter</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">strictQueryParameterValidation</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">parameters</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">QueryParameter</span>(<span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;utm_source&#39;</span>, <span style="color:#a6e22e">required</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">schema</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;type&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;string&#39;</span>]),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">QueryParameter</span>(<span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;feature_flag&#39;</span>, <span style="color:#a6e22e">required</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">schema</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;type&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;string&#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">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>Les paramètres déclarés passent ; les paramètres non déclarés sont rejetés. Pour désactiver la validation stricte sur une opération spécifique quand elle est activée au niveau de la ressource, passer <code>strictQueryParameterValidation: false</code> sur cette opération.</p>
<h2 id="x-apiplatform-tag-pour-openapi-multi-spec"><code>x-apiplatform-tag</code> pour OpenAPI multi-spec</h2>
<p>Les grandes APIs ont souvent besoin de plusieurs specs OpenAPI : une par équipe, une par version d&rsquo;API, une interne et une publique. Avant la 4.1, la spec générée était un seul document, et la diviser nécessitait un post-traitement ou des instances API Platform séparées.</p>
<p>La 4.1 ajoute une extension vendor <code>x-apiplatform-tag</code> (sans <code>s</code> final). On tague les opérations avec des noms de groupes logiques via les <code>extensionProperties</code> d&rsquo;un objet <code>Operation</code> OpenAPI, puis on demande la spec filtrée sur un ou plusieurs groupes :</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">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Factory\OpenApiFactory</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\Operation</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">openapi</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Operation</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">extensionProperties</span><span style="color:#f92672">:</span> [<span style="color:#a6e22e">OpenApiFactory</span><span style="color:#f92672">::</span><span style="color:#a6e22e">API_PLATFORM_TAG</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;public&#39;</span>, <span style="color:#e6db74">&#39;v2&#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">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>Demander <code>/api/docs.json?filter_tags[]=public</code> retourne uniquement les opérations taguées <code>public</code>. La spec complète reste disponible sans filtre. Les groupes n&rsquo;affectent pas le comportement réel de l&rsquo;API — c&rsquo;est uniquement une préoccupation de couche documentation.</p>
<p>Cela rend faisable de maintenir une configuration API Platform unique tout en servant différentes vues de spec à différents consommateurs : un Swagger UI public, un portail partenaire, et un outil interne qui expose les endpoints d&rsquo;administration.</p>
<h2 id="authentification-http-dans-swagger-ui">Authentification HTTP dans Swagger UI</h2>
<p>Avant la 4.1, le Swagger UI fourni avec API Platform supportait l&rsquo;authentification par token Bearer via son dialogue &ldquo;Authorize&rdquo;. L&rsquo;authentification par clé API et HTTP Basic n&rsquo;étaient pas câblées.</p>
<p>La 4.1 ajoute le support de plusieurs schémas de sécurité dans le document OpenAPI généré. Les schémas de sécurité sont ajoutés en décorant l&rsquo;<code>OpenApiFactory</code> et en modifiant l&rsquo;objet <code>components.securitySchemes</code> de la spec. Chaque schéma déclaré apparaît ensuite dans le dialogue &ldquo;Authorize&rdquo; de Swagger UI et est appliqué aux requêtes faites depuis l&rsquo;UI. C&rsquo;est une amélioration de la documentation et de l&rsquo;expérience développeur — la logique d&rsquo;authentification réelle dans votre application n&rsquo;est pas affectée.</p>
<h2 id="limites-de-profondeur-et-complexité-des-requêtes-graphql">Limites de profondeur et complexité des requêtes GraphQL</h2>
<p>La structure de requête récursive de GraphQL rend triviale la construction d&rsquo;une requête petite en octets mais énorme en coût d&rsquo;exécution. Sans limites, une requête imbriquée sur quatre niveaux à travers une relation many-to-many peut frapper la base de données des centaines de fois.</p>
<p>La 4.1 ajoute des limites configurables de profondeur et complexité :</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">api_platform</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">graphql</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">max_query_depth</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">max_query_complexity</span>: <span style="color:#ae81ff">100</span>
</span></span></code></pre></div><p><code>max_query_depth</code> est le niveau d&rsquo;imbrication maximum. <code>max_query_complexity</code> assigne un coût à chaque champ et rejette les requêtes dont le coût total dépasse le seuil. Les requêtes qui dépassent l&rsquo;une ou l&rsquo;autre limite sont rejetées avant exécution avec une réponse 400.</p>
<p>Il n&rsquo;y a pas de valeur universellement correcte pour ces limites — elles dépendent de la forme de votre schéma et des patterns de requête attendus. Les valeurs par défaut sont intentionnellement permissives pour éviter de casser des APIs existantes lors de la mise à jour. Les resserrer est un choix de configuration délibéré.</p>
<h2 id="formats-de-sortie-au-niveau-opération">Formats de sortie au niveau opération</h2>
<p>La 4.0 et les versions antérieures configuraient les types de contenu acceptés et retournés au niveau de l&rsquo;API. La 4.1 permet de les restreindre par opération :</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">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">outputFormats</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;jsonld&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;application/ld+json&#39;</span>]],
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">inputFormats</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;json&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;application/json&#39;</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">Book</span> {}
</span></span></code></pre></div><p>Les opérations qui ne spécifient pas de formats héritent de la configuration au niveau de l&rsquo;API. C&rsquo;est utile pour les endpoints qui doivent retourner un format spécifique (une export CSV, un flux binaire) sans changer les défauts pour le reste de l&rsquo;API.</p>
]]></content:encoded></item><item><title>La recherche full-text PostgreSQL avec Doctrine, sans une ligne de SQL brut</title><link>https://guillaumedelre.github.io/fr/2025/02/10/la-recherche-full-text-postgresql-avec-doctrine-sans-une-ligne-de-sql-brut/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2025/02/10/la-recherche-full-text-postgresql-avec-doctrine-sans-une-ligne-de-sql-brut/</guid><description>Comment nous avons superposé des types DBAL personnalisés et des wrappers DQL sur postgresql-for-doctrine pour intégrer la recherche full-text PostgreSQL dans un projet Symfony API Platform.</description><content:encoded><![CDATA[<p>Le champ de recherche de la médiathèque renvoyait des résultats en 800 millisecondes en staging. En production, il y avait quarante fois plus de lignes. Le plan d&rsquo;exécution révélait un sequential scan: aucun index sollicité, aucune façon d&rsquo;y remédier avec un B-tree classique. L&rsquo;équipe produit voulait aussi une recherche multi-mots: taper &ldquo;interview président&rdquo;, obtenir des résultats contenant les deux termes. Une requête <code>LIKE</code> avec des wildcards n&rsquo;a pas de manière propre d&rsquo;exprimer ça sans conditions indépendantes multiples, chacune nécessitant son propre scan.</p>
<p>PostgreSQL embarque une recherche full-text depuis plus de quinze ans. La plateforme tournait déjà sous PostgreSQL. Le hic: le projet utilise Doctrine ORM, et Doctrine ne sait pas nativement ce qu&rsquo;est un <code>tsvector</code>.</p>
<p>Une bibliothèque communautaire, <a href="https://github.com/martin-georgiev/postgresql-for-doctrine" target="_blank" rel="noopener noreferrer">postgresql-for-doctrine</a>, couvre une partie de cette lacune. Elle enregistre des fonctions DQL basiques comme <code>TO_TSQUERY</code>, <code>TO_TSVECTOR</code>, et l&rsquo;opérateur de correspondance <code>@@</code> en tant que pièces atomiques séparées. La fondation était là. Trois choses restaient à construire par-dessus.</p>
<h2 id="le-type-que-doctrine-na-jamais-vu">Le type que Doctrine n&rsquo;a jamais vu</h2>
<p><a href="https://www.postgresql.org/docs/current/datatype-textsearch.html" target="_blank" rel="noopener noreferrer">La recherche full-text de PostgreSQL</a> repose sur deux types: <code>tsvector</code> (une liste pré-traitée de tokens normalisés) et <code>tsquery</code> (une expression de recherche). On maintient une colonne <code>tsvector</code>, on l&rsquo;indexe avec GIN, et on interroge avec l&rsquo;opérateur <code>@@</code>.</p>
<p>Le DBAL de Doctrine ne livre aucun type <code>tsvector</code>. Déclarer <code>#[ORM\Column(type: 'tsvector')]</code> sans l&rsquo;enregistrer au préalable lève une <code>UnknownColumnTypeException</code>. La solution: un type DBAL personnalisé:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TsVector</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Type</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">final</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">string</span> <span style="color:#a6e22e">DBAL_TYPE</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;tsvector&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getSQLDeclaration</span>(<span style="color:#66d9ef">array</span> $column, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">::</span><span style="color:#a6e22e">DBAL_TYPE</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">getName</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">::</span><span style="color:#a6e22e">DBAL_TYPE</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">convertToDatabaseValueSQL</span>(<span style="color:#a6e22e">string</span> $sqlExpr, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">sprintf</span>(<span style="color:#e6db74">&#34;to_tsvector(&#39;simple&#39;, %s)&#34;</span>, $sqlExpr);
</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">convertToDatabaseValue</span>(<span style="color:#a6e22e">mixed</span> $value, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">mixed</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">is_array</span>($value) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">isset</span>($value[<span style="color:#e6db74">&#39;data&#39;</span>])) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> $value[<span style="color:#e6db74">&#39;data&#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">return</span> <span style="color:#a6e22e">is_string</span>($value) <span style="color:#f92672">?</span> $value <span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getMappedDatabaseTypes</span>(<span style="color:#a6e22e">AbstractPlatform</span> $platform)<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 style="color:#a6e22e">self</span><span style="color:#f92672">::</span><span style="color:#a6e22e">DBAL_TYPE</span>];
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La méthode intéressante est <code>convertToDatabaseValueSQL()</code>. Doctrine l&rsquo;appelle pour envelopper le placeholder SQL avant que la valeur n&rsquo;atteigne la base de données. La valeur écrite devient automatiquement <code>to_tsvector('simple', ?)</code> à la frontière DBAL, sans étape supplémentaire côté appelant.</p>
<p>On enregistre le type dans <code>doctrine.yaml</code>, puis on mappe la colonne sur l&rsquo;entité:</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">doctrine</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dbal</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">types</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">tsvector</span>: <span style="color:#ae81ff">App\Doctrine\DBAL\Types\TsVector</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:#75715e">#[ORM\Column(type: &#39;tsvector&#39;, nullable: true)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">protected</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span> $textSearch <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
</span></span></code></pre></div><p>Côté PHP, la valeur est une simple chaîne. La conversion en vrai <code>tsvector</code> se fait invisiblement au niveau DBAL.</p>
<p>Nous avons utilisé le dictionnaire <code>'simple'</code>, qui tokenise sur les espaces et la ponctuation sans stemming spécifique à une langue. La plateforme gère plusieurs langues, et les règles de stemming français auraient cassé l&rsquo;espagnol. Simple suffit largement pour la phonétique.</p>
<h2 id="garder-la-colonne-à-jour">Garder la colonne à jour</h2>
<p>Une colonne <code>tsvector</code> est une donnée dérivée: elle doit rester synchronisée avec les champs source chaque fois que l&rsquo;entité change. Un event listener Doctrine s&rsquo;en charge:</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">#[AsDoctrineListener(event: Events::prePersist)]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[AsDoctrineListener(event: Events::preUpdate)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MediaTsVectorSubscriber</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">prePersist</span>(<span style="color:#a6e22e">PrePersistEventArgs</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">getObject</span>() <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">Media</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</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">updateTextSearch</span>($event<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getObject</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">preUpdate</span>(<span style="color:#a6e22e">PreUpdateEventArgs</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">getObject</span>() <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">Media</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</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">updateTextSearch</span>($event<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getObject</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">function</span> <span style="color:#a6e22e">updateTextSearch</span>(<span style="color:#a6e22e">Media</span> $entity)<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>        $entity<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setTextSearch</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">sprintf</span>(<span style="color:#e6db74">&#39;%s %s&#39;</span>, $entity<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getTitle</span>(), $entity<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getCaption</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>Avant chaque persist et update, le subscriber concatène les champs qui doivent être recherchables dans <code>textSearch</code>. Doctrine flush la chaîne combinée, le type DBAL l&rsquo;enveloppe dans <code>to_tsvector('simple', ...)</code>, et PostgreSQL stocke la forme tokenisée.</p>
<p>Une subtilité: la valeur côté PHP est <code>&quot;title caption&quot;</code>, pas la sortie tsvector réelle. La base affiche <code>'caption' 'title'</code> (tokens triés), mais l&rsquo;entité contient une chaîne brute. C&rsquo;est attendu: la conversion est une responsabilité DBAL, pas PHP. Ça peut dérouter le débogage jusqu&rsquo;à ce qu&rsquo;on se souvienne où se situe la frontière.</p>
<h2 id="étendre-dql-avec-les-opérateurs-fts">Étendre DQL avec les opérateurs FTS</h2>
<p>Le DQL de Doctrine couvre les opérations SQL courantes, mais tout ce qui est spécifique à PostgreSQL est hors périmètre. C&rsquo;est là que <code>postgresql-for-doctrine</code> entre en jeu: il enregistre <code>TO_TSQUERY</code>, <code>TO_TSVECTOR</code>, et <code>TSMATCH</code> comme fonctions DQL individuelles. Écrire une requête full-text en DQL sans lui signifierait basculer en SQL natif.</p>
<p>Les fonctions de la bibliothèque sont atomiques, cependant. Chacune correspond à un appel SQL. Exprimer une vérification de correspondance complète en DQL ressemble à <code>TSMATCH(o.textSearch, TO_TSQUERY(:term))</code>. Assez lisible, mais l&rsquo;équipe voulait quelque chose de plus compact: une seule fonction DQL encodant à la fois l&rsquo;opérateur de correspondance et le type de requête, y compris <code>websearch_to_tsquery</code> que <code>postgresql-for-doctrine</code> ne fournissait pas.</p>
<p>La solution: des <a href="https://www.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/dql-user-defined-functions.html" target="_blank" rel="noopener noreferrer">fonctions DQL personnalisées</a> via <code>FunctionNode</code>. On parse la syntaxe DQL, puis on émet du SQL. Toutes les fonctions FTS partagent la même signature à deux arguments, donc une classe abstraite de base gère le parsing:</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">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TsFunction</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">FunctionNode</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">PathExpression</span><span style="color:#f92672">|</span><span style="color:#a6e22e">Node</span><span style="color:#f92672">|</span><span style="color:#66d9ef">null</span> $ftsField <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">PathExpression</span><span style="color:#f92672">|</span><span style="color:#a6e22e">Node</span><span style="color:#f92672">|</span><span style="color:#66d9ef">null</span> $queryString <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 style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">Parser</span> $parser)<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>        $parser<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">match</span>(<span style="color:#a6e22e">TokenType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">T_IDENTIFIER</span>);
</span></span><span style="display:flex;"><span>        $parser<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">match</span>(<span style="color:#a6e22e">TokenType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">T_OPEN_PARENTHESIS</span>);
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">ftsField</span> <span style="color:#f92672">=</span> $parser<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">StringPrimary</span>();
</span></span><span style="display:flex;"><span>        $parser<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">match</span>(<span style="color:#a6e22e">TokenType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">T_COMMA</span>);
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">queryString</span> <span style="color:#f92672">=</span> $parser<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">StringPrimary</span>();
</span></span><span style="display:flex;"><span>        $parser<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">match</span>(<span style="color:#a6e22e">TokenType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">T_CLOSE_PARENTHESIS</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Chaque classe concrète implémente <code>getSql()</code> pour émettre son expression PostgreSQL:</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">// e.textSearch @@ websearch_to_tsquery(&#39;simple&#39;, :term)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TsWebsearchQueryFunction</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">TsFunction</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">getSql</span>(<span style="color:#a6e22e">SqlWalker</span> $sqlWalker)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">ftsField</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($sqlWalker)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">.</span><span style="color:#e6db74">&#34; @@ websearch_to_tsquery(&#39;simple&#39;, &#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">.</span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">queryString</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($sqlWalker)<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></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ts_rank(e.textSearch, to_tsquery(:term)) pour le tri par pertinence
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TsRankFunction</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">TsFunction</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">getSql</span>(<span style="color:#a6e22e">SqlWalker</span> $sqlWalker)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;ts_rank(&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">.</span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">ftsField</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($sqlWalker)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">.</span><span style="color:#e6db74">&#39;, to_tsquery(&#39;</span><span style="color:#f92672">.</span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">queryString</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($sqlWalker)<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></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">doctrine</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">orm</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">entity_managers</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">default</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">dql</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">string_functions</span>:
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">tswebsearchquery</span>: <span style="color:#ae81ff">App\Doctrine\ORM\Query\AST\Functions\TsWebsearchQueryFunction</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">tsrank</span>: <span style="color:#ae81ff">App\Doctrine\ORM\Query\AST\Functions\TsRankFunction</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">tsquery</span>: <span style="color:#ae81ff">App\Doctrine\ORM\Query\AST\Functions\TsQueryFunction</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">tsplainquery</span>: <span style="color:#ae81ff">App\Doctrine\ORM\Query\AST\Functions\TsPlainQueryFunction</span>
</span></span></code></pre></div><p><code>websearch_to_tsquery</code> est le bon choix pour la recherche côté utilisateur: les espaces deviennent des AND, les chaînes entre guillemets deviennent des phrases, <code>-mot</code> exclut un terme. Inutile d&rsquo;apprendre aux utilisateurs à taper <code>interview &amp; président</code>. C&rsquo;est disponible depuis PostgreSQL 11. Sur les versions antérieures, <code>plainto_tsquery</code> est l&rsquo;équivalent le plus proche.</p>
<h2 id="le-filtre-api-platform-et-lindex-gin">Le filtre API Platform et l&rsquo;index GIN</h2>
<p>Avec les fonctions DQL enregistrées, le filtre API Platform est simple. Un <code>AbstractFilter</code> personnalisé appelle directement la fonction DQL dans le <code>QueryBuilder</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">class</span> <span style="color:#a6e22e">TextSearchFilter</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">AbstractFilter</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">filterProperty</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">string</span> $property,
</span></span><span style="display:flex;"><span>        $value,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">QueryBuilder</span> $queryBuilder,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">QueryNameGeneratorInterface</span> $queryNameGenerator,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">string</span> $resourceClass,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">?</span><span style="color:#a6e22e">Operation</span> $operation <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">array</span> $context <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    )<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#e6db74">&#39;textSearch&#39;</span> <span style="color:#f92672">!==</span> $property <span style="color:#f92672">||</span> <span style="color:#66d9ef">empty</span>($value)) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        $queryBuilder
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">andWhere</span>(<span style="color:#e6db74">&#39;tswebsearchquery(o.textSearch, :value) = true&#39;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setParameter</span>(<span style="color:#e6db74">&#39;:value&#39;</span>, $value);
</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">getDescription</span>(<span style="color:#a6e22e">string</span> $resourceClass)<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></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>On l&rsquo;applique sur l&rsquo;entité avec la déclaration d&rsquo;index:</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\Index(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">columns</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;text_search&#39;</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;media_text_search_idx_gin&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">options</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;USING&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;gin (text_search)&#39;</span>]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiFilter(TextSearchFilter::class, properties: [&#39;textSearch&#39; =&gt; &#39;partial&#39;])]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Media</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:#75715e">#[ORM\Column(type: &#39;tsvector&#39;, nullable: true)]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span> $textSearch <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;option <code>USING gin</code> n&rsquo;est pas négociable. Un index B-tree standard sur une colonne <code>tsvector</code> est inutile: PostgreSQL ne peut pas l&rsquo;utiliser pour les requêtes <code>@@</code>. GIN (Generalized Inverted Index) fonctionne différemment: il indexe chaque token individuellement, donc les recherches par n&rsquo;importe quel token sont en <code>O(log n)</code> plutôt que <code>O(n)</code>. Sans ça, on a construit un système qui donne l&rsquo;impression d&rsquo;être rapide mais qui fait quand même un full table scan.</p>
<p>Un <code>GET /media?textSearch=interview+president</code> touche maintenant l&rsquo;index GIN et répond en quelques millisecondes quel que soit la taille de la table.</p>
<h2 id="ce-que-la-répartition-ressemblait-vraiment">Ce que la répartition ressemblait vraiment</h2>
<p>La bibliothèque couvrait les fonctions atomiques bas niveau. Le code personnalisé couvrait les lacunes: un type DBAL <code>tsvector</code> que la bibliothèque ne fournissait pas, des wrappers DQL pratiques combinant <code>@@</code> et <code>websearch_to_tsquery</code> en un seul appel, et la colle applicative reliant tout ça au système d&rsquo;événements de Doctrine et à API Platform. Aucune requête native n&rsquo;a été nécessaire.</p>
<p>La répartition vaut d&rsquo;être notée en général: <code>postgresql-for-doctrine</code> donne les briques atomiques PostgreSQL, mais il faut quand même les composer en quelque chose que le reste du code peut utiliser sans y penser. Le pattern <code>FunctionNode</code> et le hook <code>convertToDatabaseValueSQL()</code> sont les deux points d&rsquo;extension qui rendent cette composition propre. Les deux valent d&rsquo;être connus, quelle que soit la bibliothèque de départ.</p>
]]></content:encoded></item><item><title>PHP 8.4 : les property hooks et la fin de la cérémonie getter/setter</title><link>https://guillaumedelre.github.io/fr/2025/01/05/php-8.4-les-property-hooks-et-la-fin-de-la-c%C3%A9r%C3%A9monie-getter/setter/</link><pubDate>Sun, 05 Jan 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2025/01/05/php-8.4-les-property-hooks-et-la-fin-de-la-c%C3%A9r%C3%A9monie-getter/setter/</guid><description>Part 10 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 8.4 apporte les property hooks : logique get/set directement sur les propriétés, remplaçant vingt ans de boilerplate getter/setter.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 8.4 est sorti le 21 novembre. Les property hooks sont la fonctionnalité. Tout le reste, et il y en a beaucoup, est secondaire.</p>
<h2 id="les-property-hooks">Les property hooks</h2>
<p>Pendant vingt ans, si on voulait du comportement à l&rsquo;accès d&rsquo;une propriété en PHP, il fallait écrire des getters et setters :</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">User</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span> $_name;
</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">getName</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> { <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">_name</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">setName</span>(<span style="color:#a6e22e">string</span> $name)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">_name</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">strtoupper</span>($name);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>PHP 8.4 ajoute des hooks directement sur la proprié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">class</span> <span style="color:#a6e22e">User</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $name {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">set</span>(<span style="color:#a6e22e">string</span> $name) {
</span></span><span style="display:flex;"><span>            $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">strtoupper</span>($name);
</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>On peut définir les hooks <code>get</code> et <code>set</code> indépendamment. Une propriété avec seulement un hook <code>get</code> est calculée à l&rsquo;accè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">Circle</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">float</span> $area {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">get</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">M_PI</span> <span style="color:#f92672">*</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">radius</span> <span style="color:#f92672">**</span> <span style="color:#ae81ff">2</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">__construct</span>(<span style="color:#66d9ef">public</span> <span style="color:#a6e22e">float</span> $radius) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Pas de stockage backing, pas de méthode getter explicite, support IDE complet. Les interfaces peuvent aussi déclarer des propriétés avec des hooks, ce qui signifie que les contrats peuvent maintenant spécifier un comportement à l&rsquo;accès aux propriétés, quelque chose qui était tout simplement impossible avant.</p>
<h2 id="la-visibilité-asymétrique">La visibilité asymétrique</h2>
<p>Une option plus légère pour quand on veut juste une lecture publique et une écriture privée :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Version</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">private</span>(<span style="color:#a6e22e">set</span>) <span style="color:#a6e22e">string</span> $value <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;1.0.0&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$v <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Version</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $v<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">value</span>;      <span style="color:#75715e">// fonctionne
</span></span></span><span style="display:flex;"><span>$v<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;2.0&#39;</span>;  <span style="color:#75715e">// Error
</span></span></span></code></pre></div><p>Élimine le pattern <code>private $x</code> + <code>public getX()</code> pour les propriétés publiques en lecture seule sans avoir besoin de la sémantique readonly complète.</p>
<h2 id="array_find-et-amis">array_find() et amis</h2>
<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>$first <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_find</span>($users, <span style="color:#a6e22e">fn</span>($u) <span style="color:#f92672">=&gt;</span> $u<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">isActive</span>());
</span></span><span style="display:flex;"><span>$any   <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_any</span>($users, <span style="color:#a6e22e">fn</span>($u) <span style="color:#f92672">=&gt;</span> $u<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">isPremium</span>());
</span></span><span style="display:flex;"><span>$all   <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_all</span>($users, <span style="color:#a6e22e">fn</span>($u) <span style="color:#f92672">=&gt;</span> $u<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">isVerified</span>());
</span></span></code></pre></div><p>Ces fonctions existent dans la bibliothèque standard de chaque autre langage depuis des décennies. En PHP, il fallait utiliser <code>array_filter()</code> + accès par index ou écrire une boucle manuelle. Elles existent maintenant : <code>array_find()</code>, <code>array_find_key()</code>, <code>array_any()</code>, <code>array_all()</code>.</p>
<h2 id="instanciation-sans-parenthèses-supplémentaires">Instanciation sans parenthèses supplémentaires</h2>
<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:#66d9ef">new</span> <span style="color:#a6e22e">MyClass</span>())<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">method</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:#66d9ef">new</span> <span style="color:#a6e22e">MyClass</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">method</span>();
</span></span></code></pre></div><p>Une restriction syntaxique qui était toujours agaçante et jamais justifiée est supprimée.</p>
<h2 id="les-objets-paresseux">Les objets paresseux</h2>
<p>Des objets dont l&rsquo;initialisation est différée jusqu&rsquo;au premier accès à une proprié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>$user <span style="color:#f92672">=</span> $reflector<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">newLazyProxy</span>(<span style="color:#a6e22e">fn</span>() <span style="color:#f92672">=&gt;</span> $repository<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">find</span>($id));
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Pas d&#39;appel en base encore
</span></span></span><span style="display:flex;"><span>$user<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">name</span>; <span style="color:#75715e">// Maintenant le proxy s&#39;initialise
</span></span></span></code></pre></div><p>Le public direct est les auteurs de frameworks ORM et de conteneurs DI, pas les développeurs d&rsquo;applications. Mais l&rsquo;effet se fait sentir dans chaque application qui utilise Doctrine ou Symfony : le lazy loading implémenté au niveau du langage plutôt qu&rsquo;à travers la génération de code.</p>
<p>PHP 8.4 est un langage qui ressemble à peine au PHP 5 avec lequel la plupart d&rsquo;entre nous avons commencé. Les property hooks en particulier : ce ne sont pas des contournements, ce sont une fonctionnalité de conception.</p>
<h2 id="deprecated-pour-son-propre-code">#[\Deprecated] pour son propre code</h2>
<p>PHP émet des notices de dépréciation pour les fonctions intégrées depuis des années. 8.4 permet de câbler le même mécanisme dans son propre 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">class</span> <span style="color:#a6e22e">ApiClient</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Deprecated(
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">message</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Use fetchJson() instead&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">since</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2.0&#39;</span>,
</span></span><span style="display:flex;"><span>    )]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">get</span>(<span style="color:#a6e22e">string</span> $url)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> { <span style="color:#f92672">...</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Appeler une méthode dépréciée émet maintenant <code>E_USER_DEPRECATED</code>, exactement comme appeler <code>mysql_connect()</code>. Les IDEs le détectent, les analyseurs statiques le signalent, le log d&rsquo;erreurs le capture. Avant ça, la seule option était un commentaire PHPDoc <code>@deprecated</code> : bien pour les IDEs, complètement invisible pour le moteur.</p>
<h2 id="bcmathnumber-rend-la-précision-arbitraire-utilisable">BcMath\Number rend la précision arbitraire utilisable</h2>
<p>Les fonctions <code>bcmath</code> existent en PHP depuis toujours, mais leur API procédurale rend tout chaînage pénible. 8.4 ajoute <code>BcMath\Number</code>, un wrapper objet avec surcharge d&rsquo;opérateurs :</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>$a <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">BcMath\Number</span>(<span style="color:#e6db74">&#39;10.5&#39;</span>);
</span></span><span style="display:flex;"><span>$b <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">BcMath\Number</span>(<span style="color:#e6db74">&#39;3.2&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$result <span style="color:#f92672">=</span> $a <span style="color:#f92672">+</span> $b;             <span style="color:#75715e">// BcMath\Number(&#39;13.7&#39;)
</span></span></span><span style="display:flex;"><span>$result <span style="color:#f92672">=</span> $a <span style="color:#f92672">*</span> $b <span style="color:#f92672">-</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">BcMath\Number</span>(<span style="color:#e6db74">&#39;1&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $result;                  <span style="color:#75715e">// 32.6
</span></span></span></code></pre></div><p>Les opérateurs <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>**</code>, <code>%</code> fonctionnent tous. L&rsquo;objet est immutable. L&rsquo;échelle se propage automatiquement à travers les opérations. Les calculs financiers, qui nécessitaient des chaînes de <code>bcadd(bcmul(...), ...)</code>, se lisent maintenant comme de l&rsquo;arithmétique.</p>
<p>De nouvelles fonctions procédurales complètent le tableau : <code>bcceil()</code>, <code>bcfloor()</code>, <code>bcround()</code>, <code>bcdivmod()</code>.</p>
<h2 id="lenum-roundingmode-remplace-les-constantes-php_round_">L&rsquo;enum RoundingMode remplace les constantes PHP_ROUND_*</h2>
<p><code>round()</code> a toujours pris un <code>$mode</code> entier depuis un ensemble de constantes <code>PHP_ROUND_*</code>. 8.4 les remplace par un enum <code>RoundingMode</code> avec des noms plus propres et quatre modes supplémentaires qui n&rsquo;étaient pas disponibles avant :</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">round</span>(<span style="color:#ae81ff">2.5</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">HalfAwayFromZero</span>);  <span style="color:#75715e">// 3
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">round</span>(<span style="color:#ae81ff">2.5</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">HalfTowardsZero</span>);   <span style="color:#75715e">// 2
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">round</span>(<span style="color:#ae81ff">2.5</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">HalfEven</span>);          <span style="color:#75715e">// 2 (arrondi du banquier)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">round</span>(<span style="color:#ae81ff">2.5</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">HalfOdd</span>);           <span style="color:#75715e">// 3
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Les quatre nouveaux modes (disponibles uniquement via l&#39;enum)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">round</span>(<span style="color:#ae81ff">2.3</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">TowardsZero</span>);       <span style="color:#75715e">// 2
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">round</span>(<span style="color:#ae81ff">2.7</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">AwayFromZero</span>);      <span style="color:#75715e">// 3
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">round</span>(<span style="color:#ae81ff">2.3</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">PositiveInfinity</span>);  <span style="color:#75715e">// 3
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">round</span>(<span style="color:#ae81ff">2.3</span>, <span style="color:#a6e22e">mode</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">RoundingMode</span><span style="color:#f92672">::</span><span style="color:#a6e22e">NegativeInfinity</span>);  <span style="color:#75715e">// 2
</span></span></span></code></pre></div><p>Les anciennes constantes <code>PHP_ROUND_*</code> fonctionnent encore. L&rsquo;enum est la voie à suivre.</p>
<h2 id="les-fonctions-de-string-multibyte-qui-auraient-dû-exister">Les fonctions de string multibyte qui auraient dû exister</h2>
<p><code>mb_trim()</code>, <code>mb_ltrim()</code>, <code>mb_rtrim()</code> : des fonctions de trim qui respectent les frontières de caractères multibyte, pas juste les espaces ASCII. Aussi nouvelles : <code>mb_ucfirst()</code> et <code>mb_lcfirst()</code> pour la mise en majuscule correcte des strings multibyte.</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>$s <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;\u{200B}hello\u{200B}&#34;</span>; <span style="color:#75715e">// Espaces de largeur nulle
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">mb_trim</span>($s);              <span style="color:#75715e">// &#34;hello&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">mb_ucfirst</span>(<span style="color:#e6db74">&#39;über&#39;</span>);       <span style="color:#75715e">// &#34;Über&#34;
</span></span></span></code></pre></div><p>Ces fonctions comblent des lacunes présentes depuis que <code>mbstring</code> a été introduit.</p>
<h2 id="request_parse_body-pour-les-requêtes-non-post">request_parse_body() pour les requêtes non-POST</h2>
<p>PHP parse automatiquement <code>application/x-www-form-urlencoded</code> et <code>multipart/form-data</code> dans <code>$_POST</code> et <code>$_FILES</code>, mais seulement pour les requêtes POST. Les requêtes PATCH et PUT avec les mêmes types de contenu nécessitaient un parsing manuel avec <code>file_get_contents('php://input')</code> et du code personnalisé.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// Dans un handler PATCH
</span></span></span><span style="display:flex;"><span>[$_POST, $_FILES] <span style="color:#f92672">=</span> <span style="color:#a6e22e">request_parse_body</span>();
</span></span></code></pre></div><p>La fonction retourne un tuple. Même logique de parsing que PHP utilise pour POST, maintenant disponible pour n&rsquo;importe quelle méthode HTTP.</p>
<h2 id="une-nouvelle-api-dom-qui-suit-la-spec">Une nouvelle API DOM qui suit la spec</h2>
<p>L&rsquo;API <code>DOMDocument</code> existante était construite sur une spec DOM level 3 plus ancienne avec des spécificités PHP superposées. 8.4 ajoute un namespace <code>Dom\</code> parallèle qui implémente le WHATWG Living Standard :</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>$doc <span style="color:#f92672">=</span> <span style="color:#a6e22e">Dom\HTMLDocument</span><span style="color:#f92672">::</span><span style="color:#a6e22e">createFromString</span>(<span style="color:#e6db74">&#39;&lt;p class=&#34;lead&#34;&gt;Hello&lt;/p&gt;&#39;</span>);
</span></span><span style="display:flex;"><span>$p <span style="color:#f92672">=</span> $doc<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;p&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $p<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">classList</span>;  <span style="color:#75715e">// &#34;lead&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $p<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">id</span>;         <span style="color:#75715e">// &#34;&#34;
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$doc2 <span style="color:#f92672">=</span> <span style="color:#a6e22e">Dom\HTMLDocument</span><span style="color:#f92672">::</span><span style="color:#a6e22e">createFromFile</span>(<span style="color:#e6db74">&#39;page.html&#39;</span>);
</span></span></code></pre></div><p><code>Dom\HTMLDocument</code> parse correctement HTML5, tag soup inclus. <code>Dom\XMLDocument</code> gère le XML strict. Les nouvelles classes sont strictes sur les types, retournent les bons types de nœuds, et exposent des propriétés modernes comme <code>classList</code>, <code>id</code>, <code>className</code>. L&rsquo;ancien <code>DOMDocument</code> reste, inchangé, pour la compatibilité ascendante.</p>
<h2 id="pdo-reçoit-des-sous-classes-spécifiques-au-driver">PDO reçoit des sous-classes spécifiques au driver</h2>
<p><code>PDO::connect()</code> et l&rsquo;instanciation directe retournent maintenant des sous-classes spécifiques au driver quand elles sont disponibles :</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>$pdo <span style="color:#f92672">=</span> <span style="color:#a6e22e">PDO</span><span style="color:#f92672">::</span><span style="color:#a6e22e">connect</span>(<span style="color:#e6db74">&#39;mysql:host=localhost;dbname=test&#39;</span>, <span style="color:#e6db74">&#39;user&#39;</span>, <span style="color:#e6db74">&#39;pass&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// $pdo est maintenant une instance Pdo\Mysql
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$pdo <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Pdo\Pgsql</span>(<span style="color:#e6db74">&#39;pgsql:host=localhost;dbname=test&#39;</span>, <span style="color:#e6db74">&#39;user&#39;</span>, <span style="color:#e6db74">&#39;pass&#39;</span>);
</span></span></code></pre></div><p>Chaque sous-classe driver (<code>Pdo\Mysql</code>, <code>Pdo\Pgsql</code>, <code>Pdo\Sqlite</code>, <code>Pdo\Firebird</code>, <code>Pdo\Odbc</code>, <code>Pdo\DbLib</code>) peut exposer des méthodes spécifiques au driver sans polluer l&rsquo;interface <code>PDO</code> de base. Doctrine, Laravel et autres ORMs similaires peuvent maintenant type-hinter contre la classe de driver spécifique quand ils ont besoin d&rsquo;un comportement spécifique au driver.</p>
<h2 id="openssl-reçoit-le-support-des-clés-modernes">OpenSSL reçoit le support des clés modernes</h2>
<p><code>openssl_pkey_new()</code> et les fonctions associées supportent maintenant Curve25519 et Curve448, les courbes elliptiques modernes qui ont remplacé les anciennes courbes NIST dans la plupart des recommandations de sécurité :</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>$key <span style="color:#f92672">=</span> <span style="color:#a6e22e">openssl_pkey_new</span>([<span style="color:#e6db74">&#39;curve_name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;ed25519&#39;</span>, <span style="color:#e6db74">&#39;private_key_type&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">OPENSSL_KEYTYPE_EC</span>]);
</span></span><span style="display:flex;"><span>$details <span style="color:#f92672">=</span> <span style="color:#a6e22e">openssl_pkey_get_details</span>($key);
</span></span></code></pre></div><p><code>x25519</code> et <code>x448</code> pour l&rsquo;échange de clés, <code>ed25519</code> et <code>ed448</code> pour les signatures. Les quatre fonctionnent maintenant avec <code>openssl_sign()</code> et <code>openssl_verify()</code>.</p>
<h2 id="pcre--lookbehind-de-longueur-variable">PCRE : lookbehind de longueur variable</h2>
<p>La mise à jour de la bibliothèque PCRE2 embarquée (10.44) apporte les assertions lookbehind de longueur variable, quelque chose que les moteurs regex de Perl et Python avaient et que PHP ne pouvait pas faire :</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">// Correspondre &#34;bar&#34; seulement quand précédé de &#34;foo&#34; ou &#34;foooo&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">preg_match</span>(<span style="color:#e6db74">&#39;/(?&lt;=foo+)bar/&#39;</span>, <span style="color:#e6db74">&#39;foooobar&#39;</span>, $matches);
</span></span></code></pre></div><p>Les assertions lookbehind nécessitaient auparavant un pattern de largeur fixe. Elles peuvent maintenant correspondre à des patterns de longueur variable. Le modificateur <code>r</code> (<code>PCRE2_EXTRA_CASELESS_RESTRICT</code>) est aussi nouveau : il empêche le mélange de caractères ASCII et non-ASCII dans les correspondances insensibles à la casse, fermant une classe d&rsquo;attaques de confusion Unicode.</p>
<h2 id="datetime-reçoit-les-microsecondes-et-une-factory-de-timestamp">DateTime reçoit les microsecondes et une factory de timestamp</h2>
<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>$dt <span style="color:#f92672">=</span> <span style="color:#a6e22e">DateTimeImmutable</span><span style="color:#f92672">::</span><span style="color:#a6e22e">createFromTimestamp</span>(<span style="color:#ae81ff">1700000000.5</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $dt<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getMicrosecond</span>(); <span style="color:#75715e">// 500000
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$with_micros <span style="color:#f92672">=</span> $dt<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setMicrosecond</span>(<span style="color:#ae81ff">123456</span>);
</span></span></code></pre></div><p><code>createFromTimestamp()</code> accepte un float pour une précision sous-seconde. <code>getMicrosecond()</code> et <code>setMicrosecond()</code> complètent l&rsquo;API pour le composant microseconde qui était à l&rsquo;intérieur de <code>DateTime</code> mais inaccessible directement.</p>
<h2 id="fpow-pour-la-conformité-ieee-754">fpow() pour la conformité IEEE 754</h2>
<p><code>pow(0, -2)</code> en PHP retournait historiquement une valeur définie par l&rsquo;implémentation. 8.4 déprécie <code>pow()</code> avec une base zéro et un exposant négatif et introduit <code>fpow()</code>, qui suit strictement IEEE 754 : <code>fpow(0, -2)</code> retourne <code>INF</code>, comme le standard le définit :</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">echo</span> <span style="color:#a6e22e">fpow</span>(<span style="color:#ae81ff">2.0</span>, <span style="color:#ae81ff">3.0</span>);   <span style="color:#75715e">// 8.0
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">fpow</span>(<span style="color:#ae81ff">0.0</span>, <span style="color:#f92672">-</span><span style="color:#ae81ff">1.0</span>);  <span style="color:#75715e">// INF
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">fpow</span>(<span style="color:#f92672">-</span><span style="color:#ae81ff">1.0</span>, <span style="color:#a6e22e">INF</span>);  <span style="color:#75715e">// 1.0
</span></span></span></code></pre></div><p>À retenir dans tout code faisant des calculs mathématiques où la conformité IEEE compte.</p>
<h2 id="le-coût-de-bcrypt-augmente">Le coût de bcrypt augmente</h2>
<p>Le coût par défaut pour <code>password_hash()</code> avec <code>PASSWORD_BCRYPT</code> est passé de <code>10</code> à <code>12</code>. Ça impacte tout code appelant <code>password_hash($pass, PASSWORD_BCRYPT)</code> sans coût explicite. L&rsquo;objectif est de maintenir le défaut grossièrement à &ldquo;quelques centaines de millisecondes sur du matériel moderne&rdquo; à mesure que le matériel devient plus rapide.</p>
<p>Si on stocke des hash bcrypt et qu&rsquo;on monte sur 8.4, les hash existants restent valides : <code>password_verify()</code> lit le coût depuis le hash lui-même. Les nouveaux hash utilisent le coût 12. <code>password_needs_rehash()</code> retourne true pour les anciens hash si on passe <code>['cost' =&gt; 12]</code>, donc on peut les mettre à jour à la prochaine connexion.</p>
<h2 id="les-dépréciations-qui-comptent">Les dépréciations qui comptent</h2>
<p>Les paramètres implicitement nullable sont dépréciés. Si un paramètre a un défaut de <code>null</code>, le type doit le dire 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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// Déprécié dans 8.4
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">foo</span>(<span style="color:#a6e22e">string</span> $s <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 style="color:#75715e">// Correct
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">foo</span>(<span style="color:#f92672">?</span><span style="color:#a6e22e">string</span> $s <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>) {}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">foo</span>(<span style="color:#a6e22e">string</span><span style="color:#f92672">|</span><span style="color:#66d9ef">null</span> $s <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>) {}
</span></span></code></pre></div><p><code>trigger_error()</code> avec <code>E_USER_ERROR</code> est déprécié : le remplacer par une exception ou <code>exit()</code>. Le niveau <code>E_USER_ERROR</code> a toujours été un hybride maladroit entre une erreur récupérable et une erreur fatale, et personne n&rsquo;était sûr lequel.</p>
<p><code>lcg_value()</code> est aussi déprécié. Utiliser <code>Random\Randomizer::getFloat()</code> à la place. Le générateur LCG avait de mauvaises propriétés d&rsquo;aléatoire et aucun contrôle de graine.</p>
]]></content:encoded></item><item><title>API Platform 4.0 : support Laravel et PUT repensé</title><link>https://guillaumedelre.github.io/fr/2024/09/27/api-platform-4.0-support-laravel-et-put-repens%C3%A9/</link><pubDate>Fri, 27 Sep 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/09/27/api-platform-4.0-support-laravel-et-put-repens%C3%A9/</guid><description>Part 6 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 4.0 apporte le support Laravel de première classe avec Eloquent et les policies, et supprime PUT des opérations par défaut pour corriger une ambiguïté sémantique persistante.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.0 est sorti neuf jours après la 3.4, fin septembre 2024. Le numéro de version est honnête : il n&rsquo;y a pas de nouvelle architecture, et la migration depuis la 3.4 est courte si on a résolu les dépréciations. Ce qui en fait un majeur, c&rsquo;est le changement de scope — API Platform n&rsquo;est plus un framework uniquement Symfony — et un défaut d&rsquo;opinion qui inverse six ans de comportement PUT.</p>
<h2 id="laravel-comme-cible-de-première-classe">Laravel comme cible de première classe</h2>
<p>Depuis sa première release, API Platform était construit sur Symfony. La couche HTTP, les métadonnées, le serializer et le bridge Doctrine supposaient tous le container de Symfony, l&rsquo;event dispatcher et le cycle de vie des requêtes. Les utilisateurs Laravel pouvaient faire tourner API Platform via un adaptateur léger, mais les filtres, la sécurité et l&rsquo;intégration Doctrine ne fonctionnaient pas avec Eloquent.</p>
<p>La 4.0 livre un bridge Laravel dédié. Il mappe la couche d&rsquo;état d&rsquo;API Platform sur le cycle de vie des requêtes de Laravel, s&rsquo;intègre directement avec les modèles Eloquent, et se branche sur le système d&rsquo;autorisation de Laravel :</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Illuminate\Database\Eloquent\Model</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(), <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Get</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">Book</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Model</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> $fillable <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;title&#39;</span>, <span style="color:#e6db74">&#39;author&#39;</span>];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;autorisation utilise les policies et gates de Laravel plutôt que les security voters de Symfony. Les opérations exposent un paramètre <code>policy</code> dédié qui correspond à un nom de méthode de policy :</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">#[Get(policy: &#39;view&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Model</span> {}
</span></span></code></pre></div><p>API Platform mappe la valeur de <code>policy</code> vers <code>Gate::allows()</code> de Laravel avec l&rsquo;instance du modèle. Les policies peuvent aussi être auto-détectées : si un modèle a une classe de policy enregistrée, API Platform infère la bonne méthode (<code>view</code>, <code>viewAny</code>, <code>create</code>, <code>update</code>, <code>delete</code>) selon le type d&rsquo;opération. Les filtres pour les collections Eloquent couvrent le même périmètre que leurs homologues Doctrine : <code>PartialSearchFilter</code>, <code>EqualsFilter</code>, <code>RangeFilter</code>, <code>OrderFilter</code>, <code>DateFilter</code>, et des variantes de recherche (<code>StartSearchFilter</code>, <code>EndSearchFilter</code>). La pagination, le tri et la validation fonctionnent via les mécanismes natifs de Laravel.</p>
<p>Ce n&rsquo;est pas un shim de compatibilité. Le bridge Laravel est maintenu aux côtés du bridge Symfony et est couvert par la même suite de tests. Les projets utilisant l&rsquo;un ou l&rsquo;autre framework ont la même API de définition des ressources.</p>
<h2 id="put-supprimé-des-opérations-par-défaut">PUT supprimé des opérations par défaut</h2>
<p>Depuis API Platform 1.0, <code>#[ApiResource]</code> sans tableau <code>operations</code> explicite générait des opérations CRUD incluant PUT. Le handler PUT mettait à jour les ressources existantes et, depuis la 3.1, pouvait aussi les créer via <code>allowCreate: true</code>.</p>
<p>La 4.0 supprime PUT de l&rsquo;ensemble par défaut. <code>#[ApiResource]</code> génère maintenant GET, POST, PATCH et DELETE. Pour utiliser PUT, il faut le déclarer 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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Put</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ... autres opérations
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Put</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">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>La motivation est la clarté sémantique. PATCH remplace PUT pour la plupart des cas d&rsquo;usage de mise à jour partielle. La sémantique de PUT — remplacer la représentation entière de la ressource — est rarement ce qu&rsquo;une API implémente réellement, mais le défaut le faisait apparaître dans toutes les APIs à moins de l&rsquo;enlever activement. Rendre PUT opt-in aligne les défauts sur la façon dont la sémantique HTTP est réellement utilisée en pratique.</p>
<h2 id="php-82-minimum">PHP 8.2 minimum</h2>
<p>La 4.0 abandonne PHP 8.0 et 8.1. PHP 8.2 est le nouveau minimum. La syntaxe de classe readonly, <code>AllowDynamicProperties</code> et les types DNF<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> introduits en 8.2 sont disponibles dans toute la codebase. Aucune fonctionnalité spécifique de 8.2 n&rsquo;est structurante pour la 4.0 — le bump de version concerne principalement l&rsquo;abandon du fardeau de maintenance plus ancien.</p>
<h2 id="symfony-64-et-doctrine-orm-217-minimum">Symfony 6.4+ et Doctrine ORM 2.17+ minimum</h2>
<p>Côté Symfony, la 4.0 requiert Symfony 6.4 ou 7.x et Doctrine ORM 2.17 ou 3.x. Les deux étaient déjà supportés en 3.4. La migration de la 3.4 vers la 4.0 sur la piste Symfony est : résoudre les dépréciations 3.4, vérifier qu&rsquo;on est sur Symfony 6.4+ et ORM 2.17+, puis mettre à jour. Aucun travail de migration supplémentaire n&rsquo;est nécessaire si c&rsquo;est déjà en place.</p>
<h2 id="ce-que-la-40-nest-pas">Ce que la 4.0 n&rsquo;est pas</h2>
<p>La 4.0 n&rsquo;est pas une nouvelle architecture. Les state providers, processors et le modèle de métadonnées de ressources de la 3.0 sont inchangés. Le bridge Laravel ajoute un nouveau contexte d&rsquo;exécution mais ne change pas la façon dont les ressources ou les opérations sont déclarées. La séparation est intentionnelle : si la 3.0 était le &ldquo;quoi&rdquo;, la 4.0 est le &ldquo;où&rdquo;.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Types en Forme Normale Disjonctive : types intersection combinés avec union, comme <code>(A&amp;B)|null</code>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>API Platform 3.4 : BackedEnum comme ressources et support DBAL 4</title><link>https://guillaumedelre.github.io/fr/2024/09/18/api-platform-3.4-backedenum-comme-ressources-et-support-dbal-4/</link><pubDate>Wed, 18 Sep 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/09/18/api-platform-3.4-backedenum-comme-ressources-et-support-dbal-4/</guid><description>Part 5 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.4 fait des BackedEnum des ressources API complètes, ajoute un BackedEnumFilter, supporte les expressions de sécurité sur les paramètres, et ajoute le support DBAL 4.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.4 est arrivé en septembre 2024 comme dernier mineur avant le saut vers la 4.0. La fonctionnalité principale est le BackedEnum comme ressource complète — pas juste un champ typé, mais un enum qui est lui-même un endpoint API.</p>
<h2 id="backedenum-comme-ressources-api">BackedEnum comme ressources API</h2>
<p>Depuis PHP 8.1, les classes BackedEnum ont un ensemble fixe de cas avec des valeurs de support string ou integer. API Platform 3.4 permet de mettre <code>#[ApiResource]</code> directement sur un BackedEnum :</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>()]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">enum</span> <span style="color:#a6e22e">BookStatus</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Draft</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;draft&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Published</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;published&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Archived</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;archived&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Un endpoint <code>GET /book_statuses</code> retourne la liste des cas. Chaque cas est sérialisé avec son nom et sa valeur. L&rsquo;endpoint est en lecture seule — les enums sont immuables par nature.</p>
<p>C&rsquo;est surtout utile pour les consommateurs frontend qui veulent une liste lisible par machine des valeurs valides sans les coder en dur. L&rsquo;alternative était un contrôleur personnalisé ou une ressource DTO dédiée listant manuellement les valeurs de l&rsquo;enum.</p>
<h2 id="backedenumfilter">BackedEnumFilter</h2>
<p>Le compagnon des ressources enum est <code>BackedEnumFilter</code>, un nouveau filtre pour les collections Doctrine qui contraint une requête par une propriété BackedEnum :</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">ApiPlatform\Doctrine\Orm\Filter\BackedEnumFilter</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiFilter</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiFilter(BackedEnumFilter::class, properties: [&#39;status&#39;])]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">BookStatus</span> $status;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>GET /books?status=published</code> filtre la collection aux livres où <code>status</code> est égal à <code>BookStatus::Published</code>. Les valeurs d&rsquo;enum invalides retournent une réponse 400. Avant ce filtre, on devait soit écrire un filtre personnalisé, soit utiliser <code>SearchFilter</code> et valider la valeur manuellement.</p>
<h2 id="expressions-de-sécurité-sur-les-paramètres">Expressions de sécurité sur les paramètres</h2>
<p>La 3.3 avait ajouté la sécurité aux liens et aux propriétés. La 3.4 étend cela aux paramètres de requête. Un paramètre peut déclarer une expression de sécurité qui contrôle s&rsquo;il est accepté :</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">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\QueryParameter</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">parameters</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">QueryParameter</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;includeDeleted&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">security</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;is_granted(&#39;ROLE_ADMIN&#39;)&#34;</span>
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>Quand l&rsquo;expression de sécurité est false, le paramètre est rejeté avec un 403, pas silencieusement ignoré. C&rsquo;est plus explicite que vérifier le rôle de l&rsquo;utilisateur dans le provider après avoir reçu le paramètre.</p>
<h2 id="support-dbal-4-ajouté">Support DBAL 4 ajouté</h2>
<p>La 3.4 ajoute le support de Doctrine DBAL 4, qui apporte des changements au système de types qui affectent la façon dont les types personnalisés et le SQL spécifique à la plateforme fonctionnent. Les filtres Doctrine Orm et les extensions de requête dans API Platform ont été mis à jour pour fonctionner avec la nouvelle API de types DBAL 4.</p>
<p>DBAL 3 (<code>^3.4.0</code>) et DBAL 4 sont supportés simultanément en 3.4. C&rsquo;est la release à adopter pour migrer vers DBAL 4 tout en restant sur une branche stable API Platform 3.x.</p>
<h2 id="validateur-de-paramètres-de-requête-déprécié">Validateur de paramètres de requête déprécié</h2>
<p>La 3.3 avait ajouté le validateur strict de paramètres de requête en opt-in. La 3.4 déprécie l&rsquo;ancien comportement (paramètres inconnus silencieusement ignorés) en préparation pour rendre la validation stricte la valeur par défaut en 4.0. Les projets qui s&rsquo;appuient sur des paramètres de requête pass-through ont une release de plus pour les déclarer explicitement.</p>
<h2 id="dernier-arrêt-avant-la-40">Dernier arrêt avant la 4.0</h2>
<p>La 3.4 est la dernière release 3.x avec de nouvelles fonctionnalités. Tout ce qui était déprécié d&rsquo;ici la 3.4 disparaît en 4.0. Le chemin de migration de la 3.4 vers la 4.0 est intentionnellement court : résoudre les dépréciations, puis mettre à jour.</p>
]]></content:encoded></item><item><title>API Platform 3.3 : headers, sécurité des liens, et webhooks OpenAPI</title><link>https://guillaumedelre.github.io/fr/2024/04/29/api-platform-3.3-headers-s%C3%A9curit%C3%A9-des-liens-et-webhooks-openapi/</link><pubDate>Mon, 29 Apr 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/04/29/api-platform-3.3-headers-s%C3%A9curit%C3%A9-des-liens-et-webhooks-openapi/</guid><description>Part 4 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.3 ajoute la configuration déclarative des headers, la sécurité fine sur les liens de sous-ressources, et le support des webhooks OpenAPI.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.3 est sorti en avril 2024 avec un ensemble d&rsquo;ajouts ciblés. Aucun d&rsquo;eux ne remodèle l&rsquo;architecture — la 3.2 avait déjà clos ce chapitre. Ce que la 3.3 apporte, c&rsquo;est du contrôle sur des choses qui étaient soit codées en dur soit nécessitaient un contournement : les headers de réponse, la visibilité des liens sur les sous-ressources, et les webhooks dans la spec générée.</p>
<h2 id="configuration-déclarative-des-headers">Configuration déclarative des headers</h2>
<p>Avant la 3.3, définir des headers de réponse personnalisés nécessitait soit un processor personnalisé qui modifiait l&rsquo;objet réponse, soit un event listener Symfony sur <code>kernel.response</code>. Les deux approches fonctionnaient mais vivaient en dehors de la définition de la ressource.</p>
<p>La 3.3 ajoute un paramètre <code>parameters</code> aux métadonnées d&rsquo;opération :</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">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\HeaderParameter</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Get(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">parameters</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;X-Custom-Header&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">HeaderParameter</span>(<span style="color:#a6e22e">description</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;A custom header&#39;</span>),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span></code></pre></div><p>Pour les headers qui varient par réponse (comme <code>Cache-Control</code> avec un max-age calculé), le processor peut encore les définir directement sur l&rsquo;objet réponse. Le paramètre <code>parameters</code> sert principalement à documenter les headers attendus dans la spec OpenAPI et pour les valeurs de headers statiques.</p>
<h2 id="sécurité-des-liens-sur-les-sous-ressources">Sécurité des liens sur les sous-ressources</h2>
<p>Quand une ressource expose des liens vers des ressources liées, ces liens apparaissent dans la sortie sérialisée indépendamment du fait que l&rsquo;utilisateur courant puisse accéder à la ressource liée. Cela crée un problème de divulgation : un utilisateur qui peut lire un livre mais pas le profil de son auteur voit quand même l&rsquo;URI de l&rsquo;auteur dans la réponse.</p>
<p>La 3.3 ajoute des expressions de sécurité au descripteur <code>Link</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Link</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[Get]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Link(
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">toClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Author</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">security</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;is_granted(&#39;ROLE_ADMIN&#39;)&#34;</span>
</span></span><span style="display:flex;"><span>    )]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Author</span> $author;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le lien est omis de la réponse quand l&rsquo;expression de sécurité évalue à false. La ressource liée elle-même n&rsquo;est pas affectée — seulement le fait que la réponse courante inclue la référence à elle.</p>
<h2 id="apipropertysecurity"><code>ApiProperty::security</code></h2>
<p>Le même mécanisme d&rsquo;expression de sécurité est disponible au niveau propriété via <code>ApiProperty::security</code>. Cela permet de cacher des champs individuels selon l&rsquo;utilisateur courant sans écrire un normalizer personnalisé :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiProperty</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">Book</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[ApiProperty(security: &#34;is_granted(&#39;ROLE_ADMIN&#39;)&#34;)]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $internalNote;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La propriété est exclue de la sérialisation quand l&rsquo;expression est false. C&rsquo;est plus propre qu&rsquo;un normalizer pour le cas courant de champs conditionnels par rôle.</p>
<h2 id="webhooks-openapi">Webhooks OpenAPI</h2>
<p><a href="https://spec.openapis.org/oas/v3.1.0" target="_blank" rel="noopener noreferrer">OpenAPI 3.1</a>
 supporte les webhooks — des appels HTTP sortants que votre API fait à des listeners enregistrés — dans le document spec lui-même. Avant la 3.3, il n&rsquo;y avait pas de moyen de les documenter dans la spec générée par API Platform.</p>
<p>La 3.3 ajoute une classe <code>Webhook</code> à passer au paramètre <code>openapi</code> d&rsquo;une opération. On déclare une classe PHP dédiée avec <code>#[ApiResource]</code> et on utilise <code>Webhook</code> sur chaque opération pour décrire la forme de l&rsquo;appel sortant :</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Post</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Attributes\Webhook</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\Operation</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\PathItem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Post</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">openapi</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Webhook</span>(
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;bookCreated&#39;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">pathItem</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PathItem</span>(
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">post</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Operation</span>(<span style="color:#a6e22e">summary</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Un livre a été créé&#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><span style="display:flex;"><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">BookWebhook</span> {}
</span></span></code></pre></div><p>Les définitions de webhooks apparaissent dans la spec générée sous la clé <code>webhooks</code> aux côtés des paths réguliers. Swagger UI les affiche dans une section séparée.</p>
<h2 id="deep-linking-dans-swagger-ui">Deep linking dans Swagger UI</h2>
<p>Swagger UI supporte le deep linking — des URLs mémorisables qui ouvrent directement sur une opération spécifique dans l&rsquo;interface. Avant la 3.3, l&rsquo;intégration API Platform n&rsquo;activait pas cela. La 3.3 active l&rsquo;option <code>deepLinking</code> de Swagger UI, configurable via <code>swagger_ui_extra_configuration</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">api_platform</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">openapi</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">swagger_ui_extra_configuration</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">deepLinking</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Avec cette option activée, le fragment d&rsquo;URL se met à jour pendant la navigation dans l&rsquo;UI, et coller ou partager l&rsquo;URL ouvre la même opération. Utile quand on écrit de la doc qui pointe directement vers un endpoint spécifique.</p>
<h2 id="validation-stricte-des-paramètres-de-requête">Validation stricte des paramètres de requête</h2>
<p>La 3.3 renforce le validateur de paramètres de requête : les paramètres non déclarés sur l&rsquo;opération renvoient maintenant une réponse 400 au lieu d&rsquo;être silencieusement ignorés. Ce comportement est opt-in :</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">api_platform</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">validator</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">query_parameter_validation</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>L&rsquo;intention est de détecter les fautes de frappe et les mauvaises utilisations de l&rsquo;API tôt. Si vous vous appuyez sur des paramètres de requête pass-through pour une logique personnalisée (logging, feature flags), vous devez les déclarer explicitement sur l&rsquo;opération avant d&rsquo;activer cela.</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>PHP 8.3 : les constantes typées et les petites victoires qui restent</title><link>https://guillaumedelre.github.io/fr/2024/01/07/php-8.3-les-constantes-typ%C3%A9es-et-les-petites-victoires-qui-restent/</link><pubDate>Sun, 07 Jan 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/01/07/php-8.3-les-constantes-typ%C3%A9es-et-les-petites-victoires-qui-restent/</guid><description>Part 9 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 8.3 ajoute les constantes de classe typées, une fonction json_validate, et une façon plus propre d&amp;#39;accéder dynamiquement aux constantes de classe.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 8.3 est sorti le 23 novembre. Une version discrète par les standards PHP : pas de bouleversement à la taille des enums, pas de JIT. Ce qu&rsquo;elle apporte, c&rsquo;est un ensemble ciblé d&rsquo;améliorations qui comblent des lacunes de longue date dans le système de types et ajoutent des fonctions qui auraient dû exister depuis des années.</p>
<h2 id="les-constantes-de-classe-typées">Les constantes de classe typées</h2>
<p>Les constantes de classe n&rsquo;ont jamais été typées depuis leur introduction. PHP 8.3 corrige ç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">interface</span> <span style="color:#a6e22e">HasVersion</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">string</span> <span style="color:#a6e22e">VERSION</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">class</span> <span style="color:#a6e22e">App</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">HasVersion</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">string</span> <span style="color:#a6e22e">VERSION</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;1.0.0&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Sans constantes typées, une constante d&rsquo;interface pouvait être redéfinie avec un type complètement différent dans une classe implémentante sans que rien ne se plaigne. Les constantes typées comblent ce trou, et sur les bases de code pilotées par les interfaces, l&rsquo;impact est immédiat.</p>
<h2 id="laccès-dynamique-aux-constantes-de-classe">L&rsquo;accès dynamique aux constantes de classe</h2>
<p>Une lacune qui nécessitait un contournement depuis que les constantes existent :</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>$name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;STATUS&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">MyClass</span><span style="color:#f92672">::</span>{$name}; <span style="color:#75715e">// ça marche maintenant
</span></span></span></code></pre></div><p>Avant, accéder à une constante avec un nom dynamique signifiait appeler <code>constant('MyClass::STATUS')</code>. La nouvelle syntaxe est cohérente avec la façon dont PHP gère déjà les variables variables et les appels de méthodes dynamiques.</p>
<h2 id="readonly-peut-maintenant-être-modifié-dans-clone">readonly peut maintenant être modifié dans clone</h2>
<p>Une limitation spécifique mais vraiment agaçante de 8.1 avec readonly : on ne pouvait pas cloner un objet et changer une propriété readonly. 8.3 ajoute la possibilité de réinitialiser les propriétés readonly pendant le clonage, ce qui rend les value objects immuables utilisables dans beaucoup plus de patterns.</p>
<h2 id="json_validate">json_validate()</h2>
<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">if</span> (<span style="color:#a6e22e">json_validate</span>($string)) {
</span></span><span style="display:flex;"><span>    $data <span style="color:#f92672">=</span> <span style="color:#a6e22e">json_decode</span>($string);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Avant 8.3, la seule façon de valider une chaîne JSON était de la décoder et de vérifier les erreurs. <code>json_validate()</code> vérifie sans allouer la structure décodée, ce qui compte quand on a juste besoin de savoir si la chaîne est du JSON valide, pas ce qu&rsquo;elle contient.</p>
<h2 id="améliorations-du-randomizer">Améliorations du Randomizer</h2>
<p><code>getBytesFromString()</code> génère une chaîne aléatoire composée uniquement de caractères d&rsquo;un ensemble donné :</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>$rng <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Random\Randomizer</span>();
</span></span><span style="display:flex;"><span>$token <span style="color:#f92672">=</span> $rng<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getBytesFromString</span>(<span style="color:#e6db74">&#39;abcdefghijklmnopqrstuvwxyz0123456789&#39;</span>, <span style="color:#ae81ff">32</span>);
</span></span></code></pre></div><p>L&rsquo;approche précédente : <code>str_split</code>, <code>array_map</code>, sélection aléatoire, <code>implode</code>. Ça marchait, mais c&rsquo;était plus long que ça n&rsquo;avait le droit d&rsquo;être.</p>
<p>8.3 est pour les équipes qui adoptent les versions PHP rapidement et veulent les améliorations incrémentales. Les constantes typées seules valent le coup sur toute base de code avec des constantes d&rsquo;interface.</p>
<h2 id="override-rend-lhéritage-explicite">#[\Override] rend l&rsquo;héritage explicite</h2>
<p>Avant 8.3, rien n&rsquo;empêchait d&rsquo;écrire une méthode qu&rsquo;on croyait surcharger celle d&rsquo;un parent, alors qu&rsquo;on avait un typo dans le nom ou que le parent l&rsquo;avait silencieusement supprimée. Des bugs silencieux, zéro retour du moteur.</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">Cache</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">get</span>(<span style="color:#a6e22e">string</span> $key)<span style="color:#f92672">:</span> <span style="color:#a6e22e">mixed</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Le moteur vérifie que cette méthode existe dans un parent ou une interface
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Si la méthode n&rsquo;existe dans aucune classe parente ou interface implémentée, PHP lève une erreur. Même concept que <code>@Override</code> en Java ou <code>override</code> en C#, enfin en PHP.</p>
<h2 id="final-sur-les-méthodes-de-trait">final sur les méthodes de trait</h2>
<p>Les traits ont toujours eu des aspérités dans le modèle OOP de PHP. Un problème spécifique : une classe utilisant un trait pouvait surcharger n&rsquo;importe laquelle de ses méthodes, sapant les garanties que le trait essayait de fournir. 8.3 laisse le trait lui-même marquer une méthode comme final :</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">trait</span> <span style="color:#a6e22e">Singleton</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">final</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getInstance</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">static</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Maintenant, une classe utilisant le trait ne peut pas surcharger <code>getInstance()</code>. La garantie tient.</p>
<h2 id="les-classes-anonymes-peuvent-être-readonly">Les classes anonymes peuvent être readonly</h2>
<p>PHP 8.1 avait apporté les classes readonly. Les classes anonymes avaient été laissées de côté pour une raison obscure. 8.3 corrige ç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>$point <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">class</span>(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">4</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">float</span> $x,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">float</span> $y,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Pratique quand on a besoin d&rsquo;un value object immuable jetable sans la cérémonie de lui donner un nom.</p>
<h2 id="les-initialiseurs-de-variables-statiques-acceptent-des-expressions">Les initialiseurs de variables statiques acceptent des expressions</h2>
<p>Une restriction petite mais ancienne : les initialiseurs de variables statiques n&rsquo;acceptaient que des expressions constantes, pas d&rsquo;appels de fonctions. 8.3 lève cette contrainte :</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">function</span> <span style="color:#a6e22e">connection</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">PDO</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">static</span> $pdo <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PDO</span>(<span style="color:#a6e22e">getenv</span>(<span style="color:#e6db74">&#39;DATABASE_URL&#39;</span>));
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $pdo;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;initialiseur s&rsquo;exécute une seule fois au premier appel, la variable statique persiste. Faisable avec un test null avant, c&rsquo;est juste plus propre.</p>
<h2 id="mb_str_pad-existe-enfin">mb_str_pad() existe enfin</h2>
<p><code>str_pad()</code> a toujours été conscient des octets, pas des caractères. Pour les chaînes multibyte (arabe, japonais, caractères accentués) il produisait une sortie incorrecte. 8.3 ajoute enfin la variante multibyte :</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>$padded <span style="color:#f92672">=</span> <span style="color:#a6e22e">mb_str_pad</span>(<span style="color:#e6db74">&#39;日本&#39;</span>, <span style="color:#ae81ff">10</span>, <span style="color:#e6db74">&#39;*&#39;</span>, <span style="color:#a6e22e">STR_PAD_BOTH</span>);
</span></span></code></pre></div><p>La fonction respecte les limites de caractères, pas les comptages d&rsquo;octets.</p>
<h2 id="str_increment-et-str_decrement">str_increment() et str_decrement()</h2>
<p>L&rsquo;opérateur <code>++</code> de PHP sur les chaînes a une histoire de bizarreries : il incrémente les séquences de lettres (<code>'a'</code> → <code>'b'</code>, <code>'z'</code> → <code>'aa'</code>), mais <code>--</code> n&rsquo;a jamais fonctionné symétriquement. Le comportement était suffisamment surprenant que 8.3 déprécie <code>++</code>/<code>--</code> sur les chaînes non alphanumériques et introduit des fonctions explicites :</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">echo</span> <span style="color:#a6e22e">str_increment</span>(<span style="color:#e6db74">&#39;a&#39;</span>);  <span style="color:#75715e">// b
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">str_increment</span>(<span style="color:#e6db74">&#39;Az&#39;</span>); <span style="color:#75715e">// Ba
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">str_decrement</span>(<span style="color:#e6db74">&#39;b&#39;</span>);  <span style="color:#75715e">// a
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">str_decrement</span>(<span style="color:#e6db74">&#39;Ba&#39;</span>); <span style="color:#75715e">// Az
</span></span></span></code></pre></div><p>Les fonctions rendent l&rsquo;intention évidente et le comportement prévisible.</p>
<h2 id="randomrandomizer-gagne-le-support-des-flottants">Random\Randomizer gagne le support des flottants</h2>
<p>8.3 comble le côté flottant de l&rsquo;API Randomizer :</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>$rng <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Random\Randomizer</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Un flottant dans [0.0, 1.0)
</span></span></span><span style="display:flex;"><span>$f <span style="color:#f92672">=</span> $rng<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">nextFloat</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Un flottant dans une plage spécifique avec contrôle de l&#39;inclusion des bornes
</span></span></span><span style="display:flex;"><span>$f <span style="color:#f92672">=</span> $rng<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getFloat</span>(<span style="color:#ae81ff">1.5</span>, <span style="color:#ae81ff">3.5</span>, <span style="color:#a6e22e">Random\IntervalBoundary</span><span style="color:#f92672">::</span><span style="color:#a6e22e">ClosedOpen</span>);
</span></span></code></pre></div><p><code>IntervalBoundary</code> est un nouvel enum avec quatre valeurs : <code>ClosedOpen</code>, <code>ClosedClosed</code>, <code>OpenClosed</code>, <code>OpenOpen</code>. C&rsquo;est important pour la justesse statistique : l&rsquo;approche naïve avec <code>rand() / getrandmax()</code> ne produit pas une distribution uniforme sur les flottants.</p>
<h2 id="la-hiérarchie-dexceptions-de-date">La hiérarchie d&rsquo;exceptions de Date</h2>
<p>Les erreurs de date/heure en PHP levaient des exceptions génériques sans moyen de distinguer &ldquo;chaîne malformée&rdquo; de &ldquo;timezone invalide&rdquo; sans parser le message. 8.3 ajoute une hiérarchie propre :</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>    <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">DateTimeImmutable</span>(<span style="color:#e6db74">&#39;not a date&#39;</span>);
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">DateMalformedStringException</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// spécifiquement un échec de parsing
</span></span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">DateException</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// autres erreurs liées aux dates
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;arbre complet : <code>DateError</code> (niveau moteur), <code>DateException</code> (base), avec des sous-classes spécifiques pour timezone invalide, chaîne d&rsquo;intervalle malformée, chaîne de période malformée, et chaîne de date malformée.</p>
<h2 id="gc_status-en-dit-plus">gc_status() en dit plus</h2>
<p><code>gc_status()</code> retourne maintenant huit champs supplémentaires : <code>running</code>, <code>protected</code>, <code>full</code>, <code>buffer_size</code>, et des décompositions temporelles (<code>application_time</code>, <code>collector_time</code>, <code>destructor_time</code>, <code>free_time</code>). Si vous profilez la pression mémoire ou les pauses GC, ces données étaient auparavant inaccessibles sans passer par une extension.</p>
<h2 id="strrchr-gagne-un-argument-de-direction">strrchr() gagne un argument de direction</h2>
<p><code>strrchr()</code> (trouver la dernière occurrence d&rsquo;un caractère, retourner de là jusqu&rsquo;à la fin) accepte maintenant un booléen <code>$before_needle</code>, alignant son API sur celle de <code>strstr()</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>$path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/var/www/html/index.php&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">strrchr</span>($path, <span style="color:#e6db74">&#39;/&#39;</span>, <span style="color:#a6e22e">before_needle</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>);  <span style="color:#75715e">// /var/www/html
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">strrchr</span>($path, <span style="color:#e6db74">&#39;/&#39;</span>);                        <span style="color:#75715e">// /index.php
</span></span></span></code></pre></div><p>Une fonction présente dans PHP depuis 1994, enfin cohérente avec sa voisine.</p>
<h2 id="dépréciations-à-noter">Dépréciations à noter</h2>
<p><code>get_class()</code> et <code>get_parent_class()</code> sans arguments émettent maintenant des avertissements de dépréciation. Les formes sans argument reposaient sur un contexte <code>$this</code> implicite, facile à mal lire. Passez l&rsquo;objet explicitement.</p>
<p><code>assert_options()</code> et les constantes <code>ASSERT_*</code> sont dépréciées au profit de la directive INI <code>zend.assertions</code>, qui est le bon outil pour contrôler le comportement des assertions selon les environnements.</p>
<p>Les opérateurs <code>++</code>/<code>--</code> sur les chaînes vides et les chaînes non numériques non alphanumériques émettent maintenant des avertissements de dépréciation. Le comportement était un territoire indéfini. 8.3 amorce la migration vers un comportement défini en 9.0.</p>
<h2 id="protection-contre-les-débordements-de-pile">Protection contre les débordements de pile</h2>
<p>Deux nouvelles directives INI : <code>zend.max_allowed_stack_size</code> fixe une limite dure sur la profondeur de pile de PHP, et <code>zend.reserved_stack_size</code> réserve un buffer pour le nettoyage après qu&rsquo;une limite soit atteinte. Avant 8.3, un code récursif profond pouvait tout simplement crasher au niveau OS. Maintenant PHP l&rsquo;intercepte et lève une <code>Error</code> avec un message utile.</p>
]]></content:encoded></item><item><title>API Platform 3.2 : les erreurs comme ressources et le retour des sous-ressources</title><link>https://guillaumedelre.github.io/fr/2023/10/12/api-platform-3.2-les-erreurs-comme-ressources-et-le-retour-des-sous-ressources/</link><pubDate>Thu, 12 Oct 2023 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2023/10/12/api-platform-3.2-les-erreurs-comme-ressources-et-le-retour-des-sous-ressources/</guid><description>Part 3 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.2 fait des erreurs des ressources Problem Detail de première classe, rétablit les sous-ressources proprement, et rend les event listeners optionnels.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.2 est arrivé en octobre 2023 avec trois changements qui ont fait avancer le modèle d&rsquo;état : les erreurs sont devenues des ressources, les sous-ressources sont revenues sous une forme qui s&rsquo;intègre vraiment dans l&rsquo;architecture, et le dernier point d&rsquo;extension hérité — les event listeners — a été formellement remplacé.</p>
<h2 id="les-erreurs-comme-ressources">Les erreurs comme ressources</h2>
<p>Avant la 3.2, la gestion des erreurs était en dehors du modèle de ressources. Les exceptions étaient interceptées par un event listener Symfony et converties en réponse, avec un contrôle limité sur la forme de la sortie.</p>
<p>La 3.2 fait des erreurs des classes <code>ApiResource</code> de première classe conformes à la <a href="https://www.rfc-editor.org/rfc/rfc9457" target="_blank" rel="noopener noreferrer">RFC 9457</a>
 (Problem Details for HTTP APIs). La classe d&rsquo;erreur intégrée est <code>ApiPlatform\ApiResource\Error</code>, et on peut créer la sienne :</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ErrorResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Exception\ProblemExceptionInterface</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[ErrorResource]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookNotFoundError</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">\RuntimeException</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ProblemExceptionInterface</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">readonly</span> <span style="color:#a6e22e">string</span> $bookId)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">__construct</span>(<span style="color:#e6db74">&#34;Book </span><span style="color:#e6db74">$bookId</span><span style="color:#e6db74"> not found&#34;</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">getType</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;/errors/book-not-found&#39;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Quand cette exception est levée n&rsquo;importe où dans la couche d&rsquo;état, API Platform l&rsquo;intercepte, la sérialise comme une réponse Problem Detail, et génère un schéma OpenAPI approprié pour elle. Le type d&rsquo;erreur, le titre, le détail et le statut font tous partie du contrat de la ressource — pas des chaînes codées en dur dans un listener.</p>
<h2 id="les-sous-ressources-sans-les-contournements">Les sous-ressources sans les contournements</h2>
<p>Les sous-ressources existaient en 2.x mais ont été supprimées en 3.0 parce qu&rsquo;elles étaient étroitement couplées à l&rsquo;ancien modèle de data provider et ne pouvaient pas être proprement mappées à la nouvelle architecture orientée opération. La 3.2 les réintroduit d&rsquo;une façon qui s&rsquo;intègre.</p>
<p>Une sous-ressource est une ressource accessible via l&rsquo;URI d&rsquo;une ressource parente. En 3.2, elle est déclarée directement sur la ressource enfant en utilisant <code>uriTemplate</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/books/{bookId}/reviews&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriVariables</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;bookId&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Link</span>(<span style="color:#a6e22e">fromClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Book</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><span style="display:flex;"><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">Review</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></span></code></pre></div><p>Le descripteur <code>Link</code> rend la relation explicite. Le provider reçoit <code>bookId</code> dans <code>$uriVariables</code> et peut l&rsquo;utiliser pour délimiter la requête. Pas d&rsquo;inférence magique, pas de jointures implicites — la structure d&rsquo;URI et l&rsquo;accès aux données sont tous les deux déclarés.</p>
<h2 id="canonical_uri_template-pour-plusieurs-chemins-daccès"><code>canonical_uri_template</code> pour plusieurs chemins d&rsquo;accès</h2>
<p>Quand une ressource est accessible via plusieurs URI (un endpoint direct et un endpoint de sous-ressource), OpenAPI doit savoir quelle URI est canonique pour les liens <code>$ref</code>. La 3.2 utilise le <code>uriTemplate</code> de niveau supérieur sur <code>ApiResource</code> comme URI canonique par défaut. Pour plus de contrôle, l&rsquo;option <code>canonical_uri_template</code> peut être passée via <code>extraProperties</code> sur n&rsquo;importe quelle opération pour la définir 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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/reviews/{id}&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Get</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/books/{bookId}/reviews&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriVariables</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;bookId&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Link</span>(<span style="color:#a6e22e">fromClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Book</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><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Review</span> {}
</span></span></code></pre></div><p>La spec OpenAPI générée utilise l&rsquo;URI canonique pour les références de schéma, gardant le document cohérent quand une ressource apparaît sous plusieurs chemins.</p>
<h2 id="types-union-et-intersection">Types union et intersection</h2>
<p>La 3.2 ajoute le support des types union et intersection PHP dans la couche de métadonnées. Une propriété déclarée comme <code>Book|Magazine</code> génère un schéma <code>oneOf</code> approprié dans OpenAPI. C&rsquo;était auparavant non supporté — on devait tomber sur un <code>mixed</code> non typé ou annoter la propriété manuellement.</p>
<h2 id="les-event-listeners-rendus-optionnels">Les event listeners rendus optionnels</h2>
<p>Le dernier shim de compatibilité venu de la 2.x était la possibilité d&rsquo;utiliser les event listeners Symfony sur les événements <code>kernel.request</code> et <code>kernel.view</code> pour intercepter le flux de données d&rsquo;API Platform. La 3.2 ne les supprime pas, mais introduit un moyen de s&rsquo;en passer : passer <code>event_listeners_backward_compatibility_layer: false</code> dans la configuration d&rsquo;API Platform désactive entièrement les hooks basés sur les événements. Le remplacement est un provider ou processor décoré par un autre provider ou processor. Le hook basé sur les événements était avec état, dépendant de l&rsquo;ordre, et court-circuitait entièrement le contexte d&rsquo;opération. Les providers décorés reçoivent l&rsquo;objet opération et peuvent appeler le provider interne quand ils sont prêts.</p>
<h2 id="le-modèle-détat-est-maintenant-complet">Le modèle d&rsquo;état est maintenant complet</h2>
<p>La 3.0 a introduit l&rsquo;architecture. La 3.1 a ajouté la séparation ressource/entité. La 3.2 ferme les lacunes restantes : les erreurs ont un contrat de ressource, les sous-ressources ont un modèle de déclaration propre, et la couche d&rsquo;état couvre désormais tous les points d&rsquo;extension que les event listeners géraient autrefois. Les shims 2.x existent encore, mais s&rsquo;en passer n&rsquo;est plus qu&rsquo;une ligne de configuration.</p>
]]></content:encoded></item><item><title>API Platform 3.1 : votre ressource n'a pas à être votre entité</title><link>https://guillaumedelre.github.io/fr/2023/01/23/api-platform-3.1-votre-ressource-na-pas-%C3%A0-%C3%AAtre-votre-entit%C3%A9/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2023/01/23/api-platform-3.1-votre-ressource-na-pas-%C3%A0-%C3%AAtre-votre-entit%C3%A9/</guid><description>Part 2 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.1 découple les ressources API des entités Doctrine, introduit un PUT conforme à la spec, et collecte les erreurs de dénormalisation en liste.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>Quatre mois après la 3.0, API Platform 3.1 est arrivé avec le premier lot de fonctionnalités construites sur le nouveau modèle d&rsquo;état. Tous les changements ne sont pas spectaculaires, mais l&rsquo;un d&rsquo;eux résout un problème qui a engendré beaucoup de contournements alambiqués en 2.x : votre ressource API n&rsquo;a plus besoin d&rsquo;être votre entité Doctrine.</p>
<h2 id="la-séparation-ressourceentité">La séparation ressource/entité</h2>
<p>En 2.x, API Platform fonctionnait mieux quand votre ressource API et votre modèle de persistance étaient la même classe. Utiliser un DTO comme surface API était possible via le système Input/Output DTO, mais ce système a été supprimé en 3.0 — il compliquait le modèle d&rsquo;état sans apporter suffisamment de bénéfices.</p>
<p>La 3.1 le remplace par quelque chose de plus propre. Le paramètre <code>stateOptions</code> d&rsquo;une opération accepte un objet <code>DoctrineOrmOptions</code> qui pointe vers une entité différente :</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Doctrine\Orm\State\Options</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">stateOptions</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Options</span>(<span style="color:#a6e22e">entityClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookEntity</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><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $title;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $author;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le provider reçoit les objets <code>BookEntity</code> de Doctrine et la couche de sérialisation les mappe sur <code>BookDto</code>. Les filtres Doctrine, la pagination et le tri fonctionnent tous sur <code>BookEntity</code>. La surface API expose <code>BookDto</code>. Les deux peuvent évoluer indépendamment.</p>
<p>Ça compte plus qu&rsquo;il n&rsquo;y paraît. Votre modèle de persistance accumule des champs internes, des relations et des colonnes qui n&rsquo;ont aucune raison d&rsquo;apparaître dans votre API. Avant la 3.1, on les exposait quand même ou on construisait un normaliseur élaboré pour les cacher. Maintenant on déclare ce que l&rsquo;API expose comme classe distincte et on laisse le framework gérer la correspondance.</p>
<h2 id="un-put-conforme-à-la-spec">Un PUT conforme à la spec</h2>
<p>Depuis la version 1.0, le handler PUT d&rsquo;API Platform mettait à jour des ressources existantes. Créer une ressource via PUT — ce que la spec HTTP autorise explicitement — n&rsquo;était pas supporté. La 3.1 ajoute la création basée sur l&rsquo;<code>uriTemplate</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:#75715e">#[Put(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/books/{id}&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">allowCreate</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>)]
</span></span></code></pre></div><p>Avec <code>allowCreate: true</code>, un PUT vers une URI qui n&rsquo;existe pas crée la ressource au lieu de retourner une 404. L&rsquo;identifiant vient de l&rsquo;URI, pas du corps de la requête. C&rsquo;est ce que la RFC 9110 décrit pour PUT : &ldquo;Si la ressource cible n&rsquo;a pas de représentation courante et que le PUT en crée une avec succès, le serveur d&rsquo;origine DOIT informer le user agent en envoyant une réponse 201 (Created).&rdquo;</p>
<p>C&rsquo;est un petit paramètre, mais il ouvre API Platform à des cas d&rsquo;usage — création idempotente, identifiants assignés par le client — qui nécessitaient auparavant un contrôleur personnalisé.</p>
<h2 id="les-erreurs-de-dénormalisation-collectées-pas-jetées">Les erreurs de dénormalisation collectées, pas jetées</h2>
<p>Avant la 3.1, les erreurs de désérialisation s&rsquo;arrêtaient au premier problème. Envoyer un corps de requête avec cinq champs invalides renvoyait une erreur sur le premier. On le corrigeait, on renvoyait, on trouvait le deuxième. À répéter cinq fois.</p>
<p>La 3.1 ajoute une option <code>collect_denormalization_errors</code> sur l&rsquo;opération qui change ce comportement :</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">#[Post(collectDenormalizationErrors: true)]
</span></span></span></code></pre></div><p>Avec cette option activée, API Platform intercepte toutes les erreurs de type et les violations de contraintes pendant la désérialisation et les retourne sous forme de liste structurée dans la réponse, formatée de la même façon que les erreurs de validation. Un seul aller-retour, une vue complète.</p>
<h2 id="apiresourceopenapi-remplace-openapicontext"><code>ApiResource::openapi</code> remplace <code>openapiContext</code></h2>
<p>L&rsquo;ancien paramètre <code>openapiContext</code> acceptait un tableau brut fusionné dans le schéma OpenAPI généré — pratique mais non typé. La 3.1 introduit un paramètre <code>openapi</code> de premier ordre qui accepte un objet <code>OpenApiOperation</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">ApiPlatform\OpenApi\Model\Operation</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\RequestBody</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Post(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">openapi</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Operation</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">requestBody</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">RequestBody</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">description</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Créer un livre&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">required</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">summary</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Créer une nouvelle entrée livre&#39;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>)]
</span></span></code></pre></div><p>L&rsquo;ancien tableau <code>openapiContext</code> fonctionne encore mais est déprécié. La nouvelle approche est typée, compatible avec l&rsquo;autocomplétion IDE, et se valide à la construction plutôt qu&rsquo;à la génération du schéma. Les backed enums PHP 8.1 obtiennent également une génération de schéma OpenAPI correcte en 3.1 — un champ typé comme backed enum produit un schéma avec des valeurs <code>enum</code> et le type correct, sans annotation supplémentaire.</p>
<h2 id="le-pattern-est-clair">Le pattern est clair</h2>
<p>La 3.0 a établi l&rsquo;architecture. La 3.1 montre ce que cette architecture permet : une séparation propre ressource/entité sans système DTO parallèle, une sémantique HTTP correcte selon la RFC, un meilleur rapport d&rsquo;erreurs. Aucune de ces fonctionnalités n&rsquo;aurait été aussi propre à implémenter sur le modèle data provider de la 2.x. Les features de la 3.1 sont la première preuve que la réécriture était la bonne décision.</p>
]]></content:encoded></item><item><title>PHP 8.2 : les classes readonly et la dépréciation qui compte vraiment</title><link>https://guillaumedelre.github.io/fr/2023/01/22/php-8.2-les-classes-readonly-et-la-d%C3%A9pr%C3%A9ciation-qui-compte-vraiment/</link><pubDate>Sun, 22 Jan 2023 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2023/01/22/php-8.2-les-classes-readonly-et-la-d%C3%A9pr%C3%A9ciation-qui-compte-vraiment/</guid><description>Part 8 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 8.2 introduit les classes readonly, déprécie les propriétés dynamiques, et ajoute les types en forme normale disjonctive.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 8.2 est sorti le 8 décembre. Les classes readonly font les gros titres. La dépréciation des propriétés dynamiques, elle, demande votre attention concrète.</p>
<h2 id="les-propriétés-dynamiques-dépréciées">Les propriétés dynamiques dépréciées</h2>
<p>PHP a toujours permis d&rsquo;ajouter des propriétés à des objets sans les déclarer dans la classe :</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">User</span> {}
</span></span><span style="display:flex;"><span>$user <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">User</span>();
</span></span><span style="display:flex;"><span>$user<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Alice&#39;</span>; <span style="color:#75715e">// aucune déclaration, aucune erreur... jusqu&#39;ici
</span></span></span></code></pre></div><p>En 8.2, ça déclenche un avertissement de dépréciation. En PHP 9.0, ce sera une erreur fatale. Le délai de grâce existe, mais le compteur tourne.</p>
<p>La logique est solide : les propriétés dynamiques sont une source classique de typos qui passent silencieusement (écrivez <code>$user-&gt;nmae</code> et PHP crée simplement une nouvelle propriété au lieu de se plaindre). Des déclarations explicites rendent le contrat de la classe lisible et donnent aux outils matière à travailler.</p>
<p>La migration est essentiellement mécanique : déclarez les propriétés, ou posez <code>#[AllowDynamicProperties]</code> sur les classes legacy que vous ne pouvez pas encore toucher.</p>
<h2 id="les-classes-readonly">Les classes readonly</h2>
<p>8.1 avait ajouté <code>readonly</code> sur les propriétés individuelles. 8.2 l&rsquo;ajoute sur la déclaration de classe elle-même :</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">Point</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">float</span> $x,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">float</span> $y,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">float</span> $z,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Toutes les propriétés promues et déclarées explicitement deviennent readonly automatiquement. Les value objects (coordonnées, montants monétaires, identifiants) sont la cible évidente. La syntaxe est propre et l&rsquo;intention se lit clairement.</p>
<p>Une contrainte : les classes readonly ne peuvent pas avoir de propriétés non typées, ce qui était déjà une mauvaise idée avec readonly de toute façon.</p>
<h2 id="les-types-dnf">Les types DNF</h2>
<p>Les types en Forme Normale Disjonctive permettent de combiner types union et types intersection :</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">function</span> <span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">Countable</span><span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Iterator</span><span style="color:#f92672">|</span><span style="color:#66d9ef">null</span> $collection)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> { <span style="color:#f92672">...</span> }
</span></span></code></pre></div><p><code>(Countable&amp;Iterator)|null</code> : un objet qui implémente les deux interfaces, ou null. Cela couvre des expressions de type que les unions de 8.0 et les intersections de 8.1 approchaient chacune sans pouvoir les représenter ensemble.</p>
<h2 id="lextension-random">L&rsquo;extension Random</h2>
<p>Une extension <code>Random</code> dédiée remplace les fonctions éparpillées <code>rand()</code>, <code>mt_rand()</code>, <code>random_int()</code> par une API orientée objet :</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>$rng <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Random\Randomizer</span>();
</span></span><span style="display:flex;"><span>$rng<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getInt</span>(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">100</span>);
</span></span><span style="display:flex;"><span>$rng<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">shuffleArray</span>($items);
</span></span></code></pre></div><p>Les moteurs sont interchangeables : <code>Mersenne Twister</code>, <code>PCG64</code>, <code>Xoshiro256StarStar</code>, ou <code>CryptoSafeEngine</code> pour les contextes sensibles à la sécurité. Même code, moteur déterministe avec seed dans les tests, moteur cryptographique en production.</p>
<p>8.2 est une version de consolidation. La dépréciation des propriétés dynamiques est la seule décision à prendre maintenant.</p>
<h2 id="null-false-et-true-comme-types-autonomes"><code>null</code>, <code>false</code>, et <code>true</code> comme types autonomes</h2>
<p>PHP avait les types nullable depuis 7.1 et les types union depuis 8.0, mais <code>null</code> comme déclaration de type autonome n&rsquo;était pas valide. 8.2 corrige ç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">function</span> <span style="color:#a6e22e">alwaysNull</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">disabled</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</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">function</span> <span style="color:#a6e22e">enabled</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>false</code> et <code>true</code> comme types autonomes sont utiles quand on veut être précis sur ce qu&rsquo;une fonction peut réellement retourner. C&rsquo;est étroit mais juste : une fonction qui retourne <code>false</code> en cas d&rsquo;échec et une chaîne en cas de succès devrait déclarer <code>string|false</code>, et maintenant les deux côtés de cette union sont de vrais types.</p>
<h2 id="les-constantes-dans-les-traits">Les constantes dans les traits</h2>
<p>Les traits pouvaient contenir des propriétés et des méthodes. Les constantes étaient le trou dans la raquette. 8.2 le comble :</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">trait</span> <span style="color:#a6e22e">Timestamps</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">DATE_FORMAT</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Y-m-d H:i:s&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">formatCreatedAt</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</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">createdAt</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">format</span>(<span style="color:#a6e22e">self</span><span style="color:#f92672">::</span><span style="color:#a6e22e">DATE_FORMAT</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Article</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Timestamps</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">echo</span> <span style="color:#a6e22e">Article</span><span style="color:#f92672">::</span><span style="color:#a6e22e">DATE_FORMAT</span>; <span style="color:#75715e">// &#39;Y-m-d H:i:s&#39;
</span></span></span></code></pre></div><p>La constante appartient à la classe qui utilise le trait, pas au trait lui-même, donc on ne peut pas accéder à <code>Timestamps::DATE_FORMAT</code> directement. Comportement de scope attendu, cohérent avec le fonctionnement des méthodes de trait.</p>
<h2 id="sensitiveparameter"><code>#[SensitiveParameter]</code></h2>
<p>Les stack traces ont toujours été un risque : les arguments de fonction sont loggés verbatim, ce qui veut dire que les mots de passe et les tokens se retrouvent dans les logs d&rsquo;erreur et les dashboards de monitoring. 8.2 ajoute un attribut pour stopper ç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">function</span> <span style="color:#a6e22e">authenticate</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">string</span> $user,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\SensitiveParameter] string $password,
</span></span></span><span style="display:flex;"><span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">bool</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// si ça lève une exception, la stack trace affiche :
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">// authenticate(&#39;alice&#39;, Object(SensitiveParameterValue))
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">hash</span>(<span style="color:#e6db74">&#39;sha256&#39;</span>, $password) <span style="color:#f92672">===</span> <span style="color:#a6e22e">getStoredHash</span>($user);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La valeur du paramètre dans la trace est remplacée par un objet <code>SensitiveParameterValue</code>. Un attribut, zéro excuse pour ne pas l&rsquo;ajouter sur chaque fonction qui touche à des credentials.</p>
<h2 id="les-syntaxes-dinterpolation-de-chaînes-dépréciées">Les syntaxes d&rsquo;interpolation de chaînes dépréciées</h2>
<p>Deux façons d&rsquo;interpoler des expressions dans des chaînes sont dépréciées en 8.2 :</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>$name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;world&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Celles-ci sont dépréciées :
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#e6db74">&#34;Hello </span><span style="color:#e6db74">${</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>;       <span style="color:#75715e">// utiliser &#34;$name&#34; ou &#34;{$name}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#e6db74">&#34;Hello </span><span style="color:#e6db74">${</span>getName()<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>;  <span style="color:#75715e">// utiliser &#34;{$this-&gt;getName()}&#34;
</span></span></span></code></pre></div><p>Les formes <code>${...}</code> créaient une ambiguïté entre les variables variables et les expressions. La syntaxe plus propre <code>{$...}</code> a toujours été là et fait la même chose. C&rsquo;est essentiellement un travail de recherche-remplacement sur les bases de code qui ont adopté les formes dépréciées par habitude.</p>
<h2 id="utf8_encode-et-utf8_decode-dépréciées"><code>utf8_encode()</code> et <code>utf8_decode()</code> dépréciées</h2>
<p>Ces deux fonctions sont dépréciées en 8.2 et disparaissent en 9.0. Leur comportement a toujours été plus étroit que ce que les noms suggéraient : <code>utf8_encode()</code> convertit ISO-8859-1 en UTF-8, pas &ldquo;n&rsquo;importe quel encodage en UTF-8&rdquo;.</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">// Déprécié en 8.2 :
</span></span></span><span style="display:flex;"><span>$utf8 <span style="color:#f92672">=</span> <span style="color:#a6e22e">utf8_encode</span>($latin1String);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Utiliser à la place :
</span></span></span><span style="display:flex;"><span>$utf8 <span style="color:#f92672">=</span> <span style="color:#a6e22e">mb_convert_encoding</span>($latin1String, <span style="color:#e6db74">&#39;UTF-8&#39;</span>, <span style="color:#e6db74">&#39;ISO-8859-1&#39;</span>);
</span></span></code></pre></div><p><code>mb_convert_encoding()</code> ou <code>iconv()</code> gèrent le cas général. Si vous traitez vraiment des entrées en Latin-1, le remplacement est direct.</p>
<h2 id="les-fonctions-de-chaîne-indépendantes-de-la-locale">Les fonctions de chaîne indépendantes de la locale</h2>
<p>Plusieurs fonctions de chaîne variaient silencieusement leur comportement selon la locale système, produisant des résultats différents en production par rapport à un container de dev. En 8.2, elles sont indépendantes de la locale et ne gèrent que l&rsquo;ASCII :</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">// strtolower, strtoupper, stristr, stripos, strripos,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// lcfirst, ucfirst, ucwords, str_ireplace font maintenant une conversion
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// de casse ASCII uniquement.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Pour un comportement sensible à la locale, utiliser les équivalents mb_* :
</span></span></span><span style="display:flex;"><span>$lowered <span style="color:#f92672">=</span> <span style="color:#a6e22e">mb_strtolower</span>($text, <span style="color:#e6db74">&#39;UTF-8&#39;</span>);
</span></span></code></pre></div><p>C&rsquo;est un correctif de cohérence. Si votre code reposait sur le comportement sensible à la locale de ces fonctions, il était déjà cassé sur des systèmes avec des configurations de locale différentes. 8.2 rend le comportement déterministe partout, ce qui est ce qu&rsquo;on voulait vraiment.</p>
<h2 id="str_split-sur-une-chaîne-vide"><code>str_split()</code> sur une chaîne vide</h2>
<p>Un changement de comportement discret à noter :</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">// PHP 8.1 : str_split(&#39;&#39;) === [&#39;&#39;]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// PHP 8.2 : str_split(&#39;&#39;) === []
</span></span></span></code></pre></div><p>Le nouveau comportement a plus de sens : découper rien ne produit rien. Si vous vérifiez <code>count(str_split($input))</code>, une entrée vide ne produit plus un count de 1.</p>
]]></content:encoded></item><item><title>API Platform 3.0 : un nouveau modèle d'état et la fin des DataProviders</title><link>https://guillaumedelre.github.io/fr/2022/11/18/api-platform-3.0-un-nouveau-mod%C3%A8le-d%C3%A9tat-et-la-fin-des-dataproviders/</link><pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/11/18/api-platform-3.0-un-nouveau-mod%C3%A8le-d%C3%A9tat-et-la-fin-des-dataproviders/</guid><description>Part 1 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.0 a remplacé les DataProviders et DataPersisters par un modèle d&amp;#39;état qui rend les opérations HTTP explicites — et a exigé PHP 8.1 et Symfony 6 pour y arriver.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.0 est arrivé en septembre 2022 avec Symfony 6.1 comme prérequis strict et une architecture de base qui ne ressemblait en rien à la 2.x. Le guide de migration est long. La raison pour laquelle il est long est intéressante.</p>
<p>L&rsquo;ancien modèle avait une fuite conceptuelle. <code>DataProviderInterface</code> et <code>DataPersisterInterface</code> étaient appelés pour chaque requête HTTP, mais le provider recevait le contexte de l&rsquo;opération comme un indice — pas comme un contrat. Un provider de collection et un provider d&rsquo;item étaient des interfaces distinctes, mais les deux vivaient dans le même seau mental : &ldquo;choses qui retournent des données.&rdquo; La couche HTTP savait ce qui était demandé ; le provider devait reconstruire cette connaissance à partir d&rsquo;indices passés dans le tableau <code>$context</code>.</p>
<p>La 3.0 inverse le modèle. Les opérations sont déclarées en premier. L&rsquo;accès aux données est câblé aux opérations.</p>
<h2 id="les-state-providers-remplacent-les-data-providers">Les state providers remplacent les data providers</h2>
<p>L&rsquo;ancien <code>DataProviderInterface</code> a disparu. Le remplaçant est <code>ProviderInterface</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">ApiPlatform\State\ProviderInterface</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Operation</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">BookProvider</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ProviderInterface</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">provide</span>(<span style="color:#a6e22e">Operation</span> $operation, <span style="color:#66d9ef">array</span> $uriVariables <span style="color:#f92672">=</span> [], <span style="color:#66d9ef">array</span> $context <span style="color:#f92672">=</span> [])<span style="color:#f92672">:</span> <span style="color:#a6e22e">object</span><span style="color:#f92672">|</span><span style="color:#66d9ef">array</span><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 style="color:#66d9ef">if</span> ($operation <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">CollectionOperationInterface</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">repository</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">findAll</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">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">repository</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">find</span>($uriVariables[<span style="color:#e6db74">&#39;id&#39;</span>]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La différence n&rsquo;est pas syntaxique. En 2.x, on enregistrait un provider et API Platform l&rsquo;appelait pour toute ressource correspondante. En 3.0, on lie un provider à une opération spécifique. Le provider n&rsquo;a plus à deviner ce qui l&rsquo;a déclenché — l&rsquo;objet opération qu&rsquo;il reçoit est le contrat.</p>
<h2 id="les-state-processors-remplacent-les-data-persisters">Les state processors remplacent les data persisters</h2>
<p><code>DataPersisterInterface</code> avait le même problème côté écriture : une seule classe gérant la création, la mise à jour et la suppression, les distinguant en inspectant la méthode HTTP ou l&rsquo;état de l&rsquo;objet. <code>ProcessorInterface</code> reçoit l&rsquo;opération comme argument typé :</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">ApiPlatform\State\ProcessorInterface</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Operation</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">BookProcessor</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ProcessorInterface</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">process</span>(<span style="color:#a6e22e">mixed</span> $data, <span style="color:#a6e22e">Operation</span> $operation, <span style="color:#66d9ef">array</span> $uriVariables <span style="color:#f92672">=</span> [], <span style="color:#66d9ef">array</span> $context <span style="color:#f92672">=</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">entityManager</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">persist</span>($data);
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">entityManager</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 style="color:#66d9ef">return</span> $data;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Plus utile encore : on peut lier un processor différent par opération. L&rsquo;opération de suppression en reçoit un qui supprime. L&rsquo;opération de création en reçoit un qui valide et stocke. Pas de switch, pas d&rsquo;inspection de méthode, pas de classe partagée qui essaie d&rsquo;être trois choses à la fois.</p>
<h2 id="les-opérations-déclarées-explicitement-en-attributs-php-81">Les opérations déclarées explicitement en attributs PHP 8.1</h2>
<p>L&rsquo;autre moitié de la 3.0 est la couche de métadonnées. Les annotations Doctrine sont remplacées par des attributs natifs PHP 8.1, et chaque opération est déclarée explicitement sur la classe ressource :</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">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Post</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(<span style="color:#a6e22e">provider</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookProvider</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">provider</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookProvider</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Post</span>(<span style="color:#a6e22e">processor</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookProcessor</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><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</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></span></code></pre></div><p>C&rsquo;est plus verbeux que <code>@ApiResource</code> avec ses valeurs par défaut magiques. C&rsquo;est aussi explicite. On sait exactement quelles opérations HTTP existent pour cette ressource, ce qui récupère les données, ce qui les écrit, et où vit la logique. Les valeurs par défaut de la 2.x étaient pratiques jusqu&rsquo;au jour où il fallait en surcharger une sans réussir à déterminer quel service décorer sans lire le code source.</p>
<h2 id="php-81-nétait-pas-un-hasard">PHP 8.1 n&rsquo;était pas un hasard</h2>
<p>L&rsquo;exigence stricte pour PHP 8.1 est structurante. Les callables de première classe rendent l&rsquo;enregistrement des filtres plus propre. L&rsquo;immuabilité des métadonnées d&rsquo;opération est assurée par un pattern de clonage (méthodes <code>withX()</code>) qui s&rsquo;appuie sur les arguments nommés et les propriétés de constructeur promues — des fondations PHP 8.0 sur lesquelles l&rsquo;architecture s&rsquo;appuie massivement.</p>
<p>Plus concrètement : l&rsquo;expression complète de l&rsquo;architecture 3.0 — opérations typées, providers scopés à l&rsquo;opération, métadonnées explicites — avait besoin de la 8.1 pour ne pas ressembler à des contournements. Abandonner PHP 7.x et 8.0 n&rsquo;était pas une décision de nettoyage.</p>
<h2 id="la-migration-est-un-vrai-travail">La migration est un vrai travail</h2>
<p>Le passage de 2.x à 3.0 n&rsquo;est pas un simple bump de version. Chaque <code>DataProvider</code> devient un <code>ProviderInterface</code>. Chaque <code>DataPersister</code> devient un <code>ProcessorInterface</code>. Les annotations deviennent des attributs. Les normaliseurs et filtres personnalisés peuvent nécessiter une restructuration. Le guide de mise à jour documente tout cela, mais &ldquo;documenté&rdquo; ne veut pas dire &ldquo;rapide.&rdquo;</p>
<p>Ce qu&rsquo;on obtient de l&rsquo;autre côté est une architecture qui passe à l&rsquo;échelle sans la complexité ambiante de la 2.x : plus besoin de deviner quelle interface implémenter, plus de chaînes <code>$this-&gt;supports()</code>, plus de valeurs par défaut invisibles qui surchargent silencieusement la config explicite.</p>
<p>La 3.0 est l&rsquo;API Platform qu&rsquo;on concevrait de zéro en sachant ce qu&rsquo;on sait après des années de 2.x. Le prix est la migration. Le numéro de version est honnête là-dessus.</p>
]]></content:encoded></item><item><title>Swarrot vs Symfony Messenger : une comparaison en conditions réelles</title><link>https://guillaumedelre.github.io/fr/2022/01/26/swarrot-vs-symfony-messenger-une-comparaison-en-conditions-r%C3%A9elles/</link><pubDate>Wed, 26 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/01/26/swarrot-vs-symfony-messenger-une-comparaison-en-conditions-r%C3%A9elles/</guid><description>Swarrot et Symfony Messenger gèrent tous deux RabbitMQ en PHP. Voici pourquoi on a gardé Swarrot après avoir sérieusement évalué une migration.</description><content:encoded><![CDATA[<p>On a migré une plateforme de microservices médias vers Symfony 6 début 2022. Douze services, la plupart consommant des messages depuis RabbitMQ via <a href="https://github.com/swarrot/swarrot" target="_blank" rel="noopener noreferrer">Swarrot</a>. Symfony 6 a rendu <a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Messenger</a> plus central que jamais, et pendant la planification de la migration un développeur a posé la question évidente : pourquoi ne pas migrer en même temps ?</p>
<p>Ça vient avec le framework. Ça a de la logique de retry, du support AMQP natif, de la documentation first-party. Notre setup ressemblait à de l&rsquo;artisanat par comparaison.</p>
<p>Question légitime. On l&rsquo;a prise au sérieux. Voilà ce qu&rsquo;on a trouvé.</p>
<h2 id="câbler-la-topologie-à-la-main">Câbler la topologie à la main</h2>
<p>Swarrot est une bibliothèque consumer qui enveloppe l&rsquo;extension PECL AMQP. Elle lit des octets depuis une queue, les fait passer à travers une chaîne de processors (leur terme pour middleware), et laisse votre code décider quoi faire avec le payload. C&rsquo;est vraiment tout.</p>
<p>La chaîne de middleware est la partie intéressante. Les processors sont des décorateurs imbriqués, chacun enveloppant le suivant. Les couches extérieures gèrent les préoccupations d&rsquo;infrastructure avant même que le message n&rsquo;atteigne la logique métier :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">middleware_stack</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.signal_handler&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.max_execution_time&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.exception_catcher&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.doctrine_object_manager&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.ack&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;app.processor.retry&#39;</span>
</span></span></code></pre></div><p><code>signal_handler</code> est en tête parce qu&rsquo;il doit intercepter <code>SIGTERM</code> avant que tout autre processor ne le voie. <code>ack</code> est près du bas parce qu&rsquo;on n&rsquo;acquitte le message qu&rsquo;après que le traitement réussit. L&rsquo;ordre n&rsquo;est pas arbitraire, et il est entièrement visible dans la configuration.</p>
<p>La topologie est tout aussi explicite. On déclare tout soi-même : exchanges, routing keys, queues de retry, queues de lettres mortes :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">messages_types</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_retry</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.retry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_dead</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.dead</span>
</span></span></code></pre></div><p>Trois entrées par type de message logique : queue principale, queue de retry, queue de lettres mortes. Tout ce qui existe sur le broker est nommé ici. La config est verbeuse mais honnête : pas d&rsquo;inférence, pas de convention plutôt que configuration. Si une queue existe dans RabbitMQ, on peut la tracer jusqu&rsquo;à une seule ligne de YAML.</p>
<h2 id="quand-le-nom-de-classe-devient-la-route">Quand le nom de classe devient la route</h2>
<p><a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Symfony Messenger</a> opère un niveau plus haut. On définit une classe de message, un handler, et un transport. La bibliothèque gère la sérialisation, le routing, le retry et les queues d&rsquo;échec automatiquement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">IngestContent</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $contentId,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $source,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">dsn</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">retry_strategy</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">max_retries</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#39;App\Message\IngestContent&#39;</span>: <span style="color:#ae81ff">async</span>
</span></span></code></pre></div><p>Messenger sérialise l&rsquo;objet, le met sur le transport, et le désérialise de l&rsquo;autre côté dans la classe correcte. Pas de topologie manuelle, pas de noms d&rsquo;exchange explicites. Le nom de classe est la primitive de routing.</p>
<p>Cette dernière phrase est exactement là où les choses se sont compliquées pour nous.</p>
<h2 id="quand-le-typage-devient-du-couplage">Quand le typage devient du couplage</h2>
<p>Messenger suppose que le producteur et le consumer partagent une définition de classe PHP. C&rsquo;est bien pour une seule application, ou pour des services qui partagent un package de contrats dédié. Dans un monorepo d&rsquo;applications Symfony indépendantes, ça crée un couplage qui n&rsquo;existe tout simplement pas aujourd&rsquo;hui.</p>
<p>Prenez un message d&rsquo;ingestion de contenu que douze services consomment. Avec Swarrot, chaque service lit le payload JSON brut et prend les champs qui l&rsquo;intéressent. Ajouter un nouveau champ signifie mettre à jour le producteur. Les consumers qui n&rsquo;ont pas besoin du champ continuent de fonctionner sans modification.</p>
<p>Avec Messenger, <code>IngestContent</code> doit être définie quelque part que les douze services peuvent référencer. Ça signifie soit :</p>
<ul>
<li>Un package PHP partagé, versionné, déployé et maintenu à travers les services. Chaque changement de schéma devient un exercice de coordination inter-services.</li>
<li>Des classes dupliquées dans chaque service, qui divergent silencieusement sous la pression.</li>
</ul>
<p>Ni l&rsquo;une ni l&rsquo;autre n&rsquo;est gratuite. L&rsquo;approche package partagé inverse le modèle de propriété : le schéma de message devient une dépendance plutôt qu&rsquo;un contrat défini à la frontière. L&rsquo;approche duplication est juste le problème original différé.</p>
<p>La différence fondamentale est ce que représente un message. Messenger est conçu pour des <strong>commandes typées</strong> : un objet qui porte du sens et se distribue à un handler spécifique. Swarrot traite les messages comme des <strong>données opaques</strong> : des octets qui coulent à travers une topologie, traités par le consumer qui écoute. Si vos messages sont des données, l&rsquo;abstraction supplémentaire qu&rsquo;ajoute Messenger ne vous aide pas. Elle crée de la friction.</p>
<h2 id="le-bloquant">Le bloquant</h2>
<p>Le problème de sérialisation était le décisif. Dans un monorepo où les services sont autonomes, partager des classes PHP entre eux n&rsquo;est pas architecturalement neutre : c&rsquo;est une décision de couplage qui rend les changements futurs plus difficiles. On aurait échangé une bibliothèque nominalement &ldquo;legacy&rdquo; pour une plus moderne tout en introduisant exactement le genre de couplage fort qu&rsquo;on avait passé des années à éviter.</p>
<p>Il y avait des préoccupations secondaires aussi. L&rsquo;extension PECL AMQP donne un accès direct aux fonctionnalités du broker (priorités de messages, TTL par queue, routing par headers exchange) que Messenger abstrait. Et migrer quinze consumers sans jour J signifie faire tourner les deux bibliothèques en parallèle, ce qui est une vraie contrainte opérationnelle.</p>
<p>Mais le problème de sérialisation seul aurait suffi.</p>
<h2 id="données-ou-commandes--voilà-la-question">Données ou commandes : voilà la question</h2>
<p>Le choix ne concerne pas la qualité des bibliothèques. Messenger est bien maintenu, bien documenté, et s&rsquo;intègre proprement dans l&rsquo;écosystème Symfony.</p>
<p>La question à se poser en premier est : que sont vos messages ?</p>
<p>Si ce sont des commandes typées avec un schéma connu et un seul consumer faisant autorité, Messenger est un choix naturel. On écrit une classe, un handler, on configure un transport, et l&rsquo;infrastructure gère le reste.</p>
<p>Si ce sont des payloads de données consommés par plusieurs services indépendants, chacun possédant sa propre désérialisation, l&rsquo;abstraction qu&rsquo;ajoute Messenger joue contre vous. La topologie explicite de Swarrot et son modèle de payload brut donnent plus de contrôle là où on en a vraiment besoin.</p>
<p>Une vraie limitation à garder à l&rsquo;esprit : Swarrot est lié à l&rsquo;extension PECL AMQP, qui n&rsquo;implémente qu&rsquo;AMQP 0-9-1. Ce qui signifie que RabbitMQ (ou un broker compatible) est une dépendance dure. Si l&rsquo;infrastructure migre un jour vers un broker AMQP 1.0 (Azure Service Bus, ActiveMQ Artemis), Swarrot ne peut pas suivre. La couche transport de Messenger abstrait ça proprement : changer de broker signifie changer un DSN, pas réécrire les consumers.</p>
<p>Si la portabilité de broker est une exigence, ou susceptible de le devenir, ça change significativement le calcul.</p>
<p>Swarrot n&rsquo;est pas du legacy à migrer. Pour l&rsquo;instant, c&rsquo;est le bon choix : le routing AMQP comme primitive, les messages comme données, RabbitMQ comme choix d&rsquo;infrastructure long terme.</p>
<p>Ça pourrait changer. Un package de contrats partagé, une nouvelle exigence de broker, un service greenfield qui ne porte pas le poids de la topologie existante : n&rsquo;importe lequel de ces éléments pourrait faire pencher la balance vers Messenger. La bibliothèque n&rsquo;est pas inadaptée à cette plateforme. Elle est peut-être juste la bonne réponse pour une version future de celle-ci.</p>
]]></content:encoded></item><item><title>Symfony 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>PHP 8.1 : enums, fibers, et un système de types qui grandit</title><link>https://guillaumedelre.github.io/fr/2022/01/09/php-8.1-enums-fibers-et-un-syst%C3%A8me-de-types-qui-grandit/</link><pubDate>Sun, 09 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/01/09/php-8.1-enums-fibers-et-un-syst%C3%A8me-de-types-qui-grandit/</guid><description>Part 7 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 8.1 apporte les enums natifs, les fibers pour la concurrence coopérative, les propriétés readonly, et les types d&amp;#39;intersection.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 8.1 est sorti le 25 novembre. Il fait suite à la refonte massive de 8.0 avec quelque chose de différent : moins de fonctionnalités, mais chacune vraiment réfléchie plutôt que greffée à la va-vite.</p>
<h2 id="les-enums">Les enums</h2>
<p>C&rsquo;est la nouveauté qui change les bases de code dès la mise à jour. Avant 8.1, les énumérations en PHP se résumaient à des constantes de classe, des chaînes ou des entiers sans rien pour les faire respecter :</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 : rien n&#39;empêche de passer Status::INVALID
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">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">const</span> <span style="color:#66d9ef">INACTIVE</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;inactive&#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><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><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">activate</span>(<span style="color:#a6e22e">Status</span> $status)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> { <span style="color:#f92672">...</span> }
</span></span></code></pre></div><p>Les enums PHP sont des objets, pas des scalaires. Ils supportent les méthodes, les interfaces et les constantes. Les backed enums (avec une valeur string ou int) se sérialisent proprement et se mappent naturellement aux colonnes de base de données. Les pure enums (sans type de backing) expriment des concepts métier sans se soucier de la sérialisation.</p>
<p>L&rsquo;effet immédiat : chaque champ de statut, chaque ensemble fini d&rsquo;états dans toutes les bases de code que je maintiens est devenu un candidat à l&rsquo;enum. Le système de types a enfin un moyen natif d&rsquo;exprimer ce que chaque projet PHP simulait depuis des années.</p>
<h2 id="les-fibers">Les fibers</h2>
<p>Les fibers sont une primitive de concurrence coopérative : vous pouvez suspendre et reprendre l&rsquo;exécution d&rsquo;une fonction, en cédant le contrôle sans recourir aux threads.</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>$fiber <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Fiber</span>(<span style="color:#66d9ef">function</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>    $value <span style="color:#f92672">=</span> <span style="color:#a6e22e">Fiber</span><span style="color:#f92672">::</span><span style="color:#a6e22e">suspend</span>(<span style="color:#e6db74">&#39;first&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">echo</span> <span style="color:#e6db74">&#34;Repris avec : </span><span style="color:#e6db74">{</span>$value<span style="color:#e6db74">}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>;
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$result <span style="color:#f92672">=</span> $fiber<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">start</span>();    <span style="color:#75715e">// &#39;first&#39;
</span></span></span><span style="display:flex;"><span>$fiber<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">resume</span>(<span style="color:#e6db74">&#39;hello&#39;</span>);      <span style="color:#75715e">// &#34;Repris avec : hello&#34;
</span></span></span></code></pre></div><p>Les fibers sont la fondation dont les bibliothèques async comme ReactPHP et Amp avaient besoin depuis un moment du côté du runtime. Pour la plupart des développeurs d&rsquo;applications, l&rsquo;API directe compte moins que les bibliothèques construites par-dessus, mais comprendre les fibers explique ce que font ces bibliothèques en coulisses.</p>
<h2 id="pencil2-les-propriétés-readonly">:pencil2: Les propriétés readonly</h2>
<p>8.0 avait apporté la promotion des paramètres du constructeur. 8.1 ajoute <code>readonly</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">class</span> <span style="color:#a6e22e">User</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">int</span> $id,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $name,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Une propriété <code>readonly</code> ne peut être écrite qu&rsquo;une seule fois, lors de l&rsquo;initialisation. Après ça, toute écriture lève une <code>Error</code>. Combiné avec la promotion des paramètres, les value objects et les DTOs deviennent concis et signifient réellement ce qu&rsquo;ils annoncent.</p>
<h2 id="la-syntaxe-callable-de-première-classe">La syntaxe callable de première classe</h2>
<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>$fn <span style="color:#f92672">=</span> <span style="color:#a6e22e">strlen</span>(<span style="color:#f92672">...</span>);
</span></span><span style="display:flex;"><span>$fn <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">process</span>(<span style="color:#f92672">...</span>);
</span></span><span style="display:flex;"><span>$fn <span style="color:#f92672">=</span> <span style="color:#a6e22e">MyClass</span><span style="color:#f92672">::</span><span style="color:#a6e22e">create</span>(<span style="color:#f92672">...</span>);
</span></span></code></pre></div><p><code>...</code> après un callable crée une <code>Closure</code> sans le boilerplate de <code>Closure::fromCallable()</code>. Utile quand on passe des méthodes comme callbacks.</p>
<p>8.1 est précis. Les enums justifient à eux seuls la mise à jour.</p>
<h2 id="les-types-dintersection">Les types d&rsquo;intersection</h2>
<p>Les types union ont débarqué en 8.0. Les types d&rsquo;intersection suivent en 8.1. Là où un union dit « l&rsquo;un ou l&rsquo;autre », une intersection dit « tous à la fois » :</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">function</span> <span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">Countable</span><span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Iterator</span> $collection)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> ($collection <span style="color:#66d9ef">as</span> $item) { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">count</span>($collection);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Une contrainte : les types d&rsquo;intersection ne peuvent pas être mélangés avec les types union dans la même déclaration (ça arrivera en 8.2 avec les DNF types). Mais ça débloque déjà une vérification de types précise pour les objets qui doivent satisfaire plusieurs interfaces à la fois, un pattern que les frameworks utilisent constamment et qui devait rester sans typage jusqu&rsquo;ici.</p>
<h2 id="le-type-de-retour-never">Le type de retour <code>never</code></h2>
<p>Une fonction qui ne retourne jamais (elle lève toujours une exception ou sort) a maintenant un type pour le dire :</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">function</span> <span style="color:#a6e22e">redirect</span>(<span style="color:#a6e22e">string</span> $url)<span style="color:#f92672">:</span> <span style="color:#a6e22e">never</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">header</span>(<span style="color:#e6db74">&#34;Location: </span><span style="color:#e6db74">{</span>$url<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">exit</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">function</span> <span style="color:#a6e22e">fail</span>(<span style="color:#a6e22e">string</span> $message)<span style="color:#f92672">:</span> <span style="color:#a6e22e">never</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\RuntimeException</span>($message);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;avantage concret : les analyseurs statiques peuvent prouver que le code après une fonction <code>never</code> est inatteignable, et les appelants savent qu&rsquo;il n&rsquo;y a pas de valeur de retour à gérer. Avant ça, ça vivait dans des docblocks sans enforcement.</p>
<h2 id="les-constantes-de-classe-finales">Les constantes de classe finales</h2>
<p>Avant 8.1, n&rsquo;importe quelle sous-classe pouvait silencieusement surcharger la constante de classe d&rsquo;un parent. Maintenant vous pouvez y mettre un terme :</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">Base</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">final</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">VERSION</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;1.0&#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">class</span> <span style="color:#a6e22e">Child</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Base</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Fatal error: Cannot override final constant Base::VERSION
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">VERSION</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;2.0&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Parallèlement, les constantes d&rsquo;interface sont désormais surchargeables par les classes implémentant l&rsquo;interface par défaut. Un correctif de comportement qui était incohérent depuis l&rsquo;introduction des interfaces.</p>
<h2 id="new-dans-les-initialiseurs"><code>new</code> dans les initialiseurs</h2>
<p>Les valeurs par défaut des paramètres étaient autrefois limitées aux scalaires et aux tableaux. 8.1 lève cette restriction :</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">Logger</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">Handler</span> $handler <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">NullHandler</span>(),
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">createUser</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Validator</span> $validator <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">DefaultValidator</span>(),
</span></span><span style="display:flex;"><span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">User</span> { <span style="color:#75715e">/* ... */</span> }
</span></span></code></pre></div><p>Idem pour les arguments d&rsquo;attributs et les initialiseurs de variables statiques. Ce qui signifie que l&rsquo;injection de dépendances avec des valeurs par défaut sensées ne nécessite plus une vérification de null et une instanciation paresseuse dans le corps de la méthode.</p>
<h2 id="le-déballage-de-tableaux-avec-des-clés-string">Le déballage de tableaux avec des clés string</h2>
<p>Le déballage de tableau via l&rsquo;opérateur spread ne fonctionnait qu&rsquo;avec des tableaux à clés entières avant 8.1. Les clés string fonctionnent aussi maintenant :</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>$defaults <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;color&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;red&#39;</span>, <span style="color:#e6db74">&#39;size&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;M&#39;</span>];
</span></span><span style="display:flex;"><span>$custom <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;size&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;L&#39;</span>, <span style="color:#e6db74">&#39;weight&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;200g&#39;</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$merged <span style="color:#f92672">=</span> [<span style="color:#f92672">...</span>$defaults, <span style="color:#f92672">...</span>$custom];
</span></span><span style="display:flex;"><span><span style="color:#75715e">// [&#39;color&#39; =&gt; &#39;red&#39;, &#39;size&#39; =&gt; &#39;L&#39;, &#39;weight&#39; =&gt; &#39;200g&#39;]
</span></span></span></code></pre></div><p>Les clés ultérieures écrasent les précédentes. Même comportement que <code>array_merge()</code>, mais exprimé inline. La différence de performance est marginale ; la différence de lisibilité, elle, ne l&rsquo;est pas.</p>
<h2 id="fsync-et-fdatasync"><code>fsync</code> et <code>fdatasync</code></h2>
<p>Deux fonctions qui n&rsquo;avaient aucune bonne raison d&rsquo;être absentes d&rsquo;un langage orienté système de fichiers :</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>$fp <span style="color:#f92672">=</span> <span style="color:#a6e22e">fopen</span>(<span style="color:#e6db74">&#39;/tmp/important.dat&#39;</span>, <span style="color:#e6db74">&#39;w&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fwrite</span>($fp, $data);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fsync</span>($fp);   <span style="color:#75715e">// vide les buffers OS vers le stockage physique
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">fclose</span>($fp);
</span></span></code></pre></div><p><code>fdatasync()</code> fait la même chose mais saute la synchronisation des métadonnées quand on ne se soucie que de la durabilité des données. Les deux retournent <code>false</code> en cas d&rsquo;échec. Si vous écrivez quoi que ce soit qui nécessite une sécurité en cas de crash, vous aviez besoin de ça.</p>
<h2 id="passer-null-aux-paramètres-non-nullables-des-fonctions-internes">Passer <code>null</code> aux paramètres non-nullables des fonctions internes</h2>
<p>Un changement plus discret mais aux conséquences réelles : les fonctions internes qui acceptent des chaînes, des entiers, etc. ont toujours avalé silencieusement <code>null</code> et l&rsquo;ont coercé. En 8.1, ça commence à émettre un avertissement de dépréciation.</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">str_contains</span>(<span style="color:#e6db74">&#34;foobar&#34;</span>, <span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Deprecated: Passing null to parameter #2 ($needle) of type string is deprecated
</span></span></span></code></pre></div><p>Ça aligne les fonctions internes sur les fonctions définies par l&rsquo;utilisateur, qui refusaient déjà les arguments nullable pour des paramètres non-nullables. PHP 9.0 transforme ça en erreur fatale. Si vous passez <code>null</code> dans des fonctions de chaînes, c&rsquo;est maintenant un meilleur moment pour le corriger que pendant un incident de production.</p>
<h2 id="mysqli-lève-des-exceptions-par-défaut">MySQLi lève des exceptions par défaut</h2>
<p>Avant 8.1, MySQLi échouait silencieusement sauf si vous appeliez explicitement <code>mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)</code>. C&rsquo;est maintenant la valeur par défaut :</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">// Ceci lève \mysqli_sql_exception en cas d&#39;échec de connexion en 8.1
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Auparavant retournait false et définissait une erreur que vous deviez vérifier manuellement
</span></span></span><span style="display:flex;"><span>$connection <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">mysqli</span>(<span style="color:#e6db74">&#39;localhost&#39;</span>, <span style="color:#e6db74">&#39;user&#39;</span>, <span style="color:#e6db74">&#39;wrong_password&#39;</span>, <span style="color:#e6db74">&#39;db&#39;</span>);
</span></span></code></pre></div><p>Toute base de code qui attrape les erreurs MySQLi en vérifiant les valeurs de retour doit être revue. Les échecs silencieux qui causaient des bugs difficiles à diagnostiquer lèvent maintenant des exceptions, ce qui est le bon comportement, même si ça peut surprendre si vous l&rsquo;attrapez en pleine mise à jour.</p>
]]></content:encoded></item><item><title>PHP 8.0 : match, arguments nommés, attributs et JIT</title><link>https://guillaumedelre.github.io/fr/2021/01/10/php-8.0-match-arguments-nomm%C3%A9s-attributs-et-jit/</link><pubDate>Sun, 10 Jan 2021 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2021/01/10/php-8.0-match-arguments-nomm%C3%A9s-attributs-et-jit/</guid><description>Part 6 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 8.0 remodèle le langage : compilateur JIT, arguments nommés, expressions match, types union et opérateur nullsafe.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 8.0 est sorti le 26 novembre. Je le fais tourner depuis six semaines sur un projet perso et un nouveau service au boulot. C&rsquo;est la version PHP la plus significative depuis 7.0, et à certains égards plus impactante, parce que les changements se renforcent mutuellement de façon utile.</p>
<h2 id="jit">JIT</h2>
<p>Le compilateur Just-In-Time était l&rsquo;annonce principale. La réalité en production est plus nuancée : pour les applications web typiques (requêtes en base, appels HTTP, rendu de templates) les gains sont modestes, parce que ces workloads sont limités par les I/O, pas par le calcul. Là où le JIT brille vraiment, c&rsquo;est le code intensif en CPU : manipulation d&rsquo;images, transformation de données, calcul mathématique.</p>
<p>Pour la plupart des applications web, l&rsquo;amélioration de performance vient du travail sur le moteur en général dans 8.0, pas du JIT spécifiquement. Vaut quand même la peine de l&rsquo;activer : ça ne coûte rien sur les workloads I/O-bound.</p>
<h2 id="les-expressions-match">Les expressions match</h2>
<p><code>switch</code> a trois problèmes : il utilise la comparaison souple, il tombe en cascade par défaut, et il ne peut pas être utilisé comme expression. <code>match</code> règle les trois :</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>$result <span style="color:#f92672">=</span> <span style="color:#a6e22e">match</span>($status) {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;active&#39;</span>, <span style="color:#e6db74">&#39;pending&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;processing&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;done&#39;</span>             <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;finished&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">default</span>            <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\UnexpectedValueException</span>($status),
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Comparaison stricte. Pas de cascade. Expression qui retourne une valeur. Un match non exhaustif lève une exception. Après une semaine avec <code>match</code>, j&rsquo;ai arrêté d&rsquo;écrire des <code>switch</code>.</p>
<h2 id="les-arguments-nommés">Les arguments nommés</h2>
<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">array_slice</span>(<span style="color:#66d9ef">array</span><span style="color:#f92672">:</span> $users, <span style="color:#a6e22e">offset</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">length</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">10</span>, <span style="color:#a6e22e">preserve_keys</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>);
</span></span></code></pre></div><p>Les arguments nommés permettent de passer les arguments dans n&rsquo;importe quel ordre et d&rsquo;en sauter des optionnels. Le gain évident : la lisibilité sur les fonctions avec plusieurs flags booléens. Le gain moins évident : les arguments nommés survivent aux mises à jour de PHP même quand l&rsquo;ordre des paramètres change, parce qu&rsquo;on nomme ce qu&rsquo;on veut dire.</p>
<h2 id="les-attributs">Les attributs</h2>
<p>Place aux docblock annotations (le style <code>@Route</code>, <code>@ORM\Column</code> sur lequel les frameworks se sont appuyés pendant des années), bienvenue à la syntaxe PHP de première classe :</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">#[Route(&#39;/users&#39;, methods: [&#39;GET&#39;])]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[IsGranted(&#39;ROLE_ADMIN&#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">list</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span> { <span style="color:#f92672">...</span> }
</span></span></code></pre></div><p>Les attributs sont validés par le moteur, pas parsés depuis des strings. Le support IDE fonctionne directement, sans magie de plugin. Pour les utilisateurs de Symfony et Doctrine, c&rsquo;est le vrai gain quotidien de PHP 8.0.</p>
<h2 id="la-promotion-de-constructeur">La promotion de constructeur</h2>
<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">User</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">int</span> $id,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $name,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span> $email <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><p>Propriétés déclarées et assignées en une ligne dans la signature du constructeur. Le gain de refactoring le plus immédiat dans 8.0 : chaque classe de données que j&rsquo;ai touchée depuis la mise à jour fait moitié moins de lignes qu&rsquo;avant.</p>
<h2 id="lopérateur-nullsafe">L&rsquo;opérateur nullsafe</h2>
<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>$city <span style="color:#f92672">=</span> $user<span style="color:#f92672">?-&gt;</span><span style="color:#a6e22e">getAddress</span>()<span style="color:#f92672">?-&gt;</span><span style="color:#a6e22e">getCity</span>()<span style="color:#f92672">?-&gt;</span><span style="color:#a6e22e">getName</span>();
</span></span></code></pre></div><p><code>null</code> à n&rsquo;importe quel point de la chaîne court-circuite le reste et retourne <code>null</code>. L&rsquo;alternative était des null checks imbriqués ou une chaîne de retours anticipés. Ça se compose naturellement.</p>
<h2 id="les-types-union">Les types union</h2>
<p>Les arguments nommés rendent les signatures de fonctions plus explicites au site d&rsquo;appel. Les types union les rendent plus honnêtes au site de déclaration :</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">function</span> <span style="color:#a6e22e">processInput</span>(<span style="color:#a6e22e">int</span><span style="color:#f92672">|</span><span style="color:#a6e22e">float</span><span style="color:#f92672">|</span><span style="color:#a6e22e">string</span> $value)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span><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:#66d9ef">if</span> (<span style="color:#a6e22e">is_string</span>($value)) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">strlen</span>($value);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (<span style="color:#a6e22e">int</span>) <span style="color:#a6e22e">round</span>($value);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;union <code>int|float|string</code> est un OU littéral. Le moteur l&rsquo;impose à l&rsquo;entrée et à la sortie. Avant 8.0, &ldquo;ce paramètre accepte int ou float&rdquo; vivait dans un docblock que rien n&rsquo;imposait. Il y a aussi <code>null</code> comme composant de type : <code>?string</code> est juste du sucre syntaxique pour <code>string|null</code>, les deux sont valides.</p>
<p>Un cas spécial : <code>false</code>. PHP a un tas de fonctions natives qui retournent une valeur typée en cas de succès et <code>false</code> en cas d&rsquo;échec. Le système de types de 8.0 accommode ça : <code>array|false</code>, <code>string|false</code>. C&rsquo;est une reconnaissance honnête que la codebase ne peut pas être réécrite du jour au lendemain.</p>
<h2 id="le-type-de-retour-static">Le type de retour static</h2>
<p><code>static</code> comme type de retour était possible de manière informelle via les docblocks, mais 8.0 le rend officiel. La distinction entre <code>self</code> et <code>static</code> compte dans l&rsquo;héritage :</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">Builder</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">array</span> $config <span style="color:#f92672">=</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">set</span>(<span style="color:#a6e22e">string</span> $key, <span style="color:#a6e22e">mixed</span> $value)<span style="color:#f92672">:</span> <span style="color:#66d9ef">static</span> {
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">config</span>[$key] <span style="color:#f92672">=</span> $value;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $this;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SpecialBuilder</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Builder</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$result <span style="color:#f92672">=</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">SpecialBuilder</span>())<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">set</span>(<span style="color:#e6db74">&#39;foo&#39;</span>, <span style="color:#e6db74">&#39;bar&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// $result est SpecialBuilder, pas Builder
</span></span></span></code></pre></div><p>Avec <code>self</code> comme type de retour, cette chaîne retournerait <code>Builder</code>, cassant les interfaces fluides dans les sous-classes. <code>static</code> fait fonctionner correctement les APIs fluides à travers les hiérarchies d&rsquo;héritage sans surcharges manuelles.</p>
<h2 id="le-type-mixed">Le type mixed</h2>
<p><code>mixed</code> était une convention de docblock pendant des années. 8.0 en fait un vrai type qui apparaît dans les signatures :</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">function</span> <span style="color:#a6e22e">debug</span>(<span style="color:#a6e22e">mixed</span> $value)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">var_dump</span>($value);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Il accepte tout : <code>null</code>, objets, ressources, scalaires, tableaux. Sémantiquement c&rsquo;est la même chose que n&rsquo;avoir aucune déclaration de type, mais c&rsquo;est explicite plutôt qu&rsquo;absent. La différence entre &ldquo;ce paramètre est non typé&rdquo; et &ldquo;ce paramètre accepte intentionnellement n&rsquo;importe quoi.&rdquo; Vaut la peine de l&rsquo;utiliser quand on écrit un utilitaire généraliste qui serait malhonnête avec un type plus étroit.</p>
<h2 id="throw-comme-expression">throw comme expression</h2>
<p>Avant 8.0, <code>throw</code> était une instruction. Ça semble une distinction pédante jusqu&rsquo;à ce qu&rsquo;on tombe sur les endroits où on veut vraiment une expression :</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">// Dans un ternaire :
</span></span></span><span style="display:flex;"><span>$value <span style="color:#f92672">=</span> $input <span style="color:#f92672">??</span> <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\InvalidArgumentException</span>(<span style="color:#e6db74">&#39;input required&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Dans une arrow function :
</span></span></span><span style="display:flex;"><span>$getId <span style="color:#f92672">=</span> <span style="color:#a6e22e">fn</span>(<span style="color:#a6e22e">User</span> $u) <span style="color:#f92672">=&gt;</span> $u<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">id</span> <span style="color:#f92672">??</span> <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\RuntimeException</span>(<span style="color:#e6db74">&#39;no id&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Dans un bras match (qui est déjà une expression) :
</span></span></span><span style="display:flex;"><span>$status <span style="color:#f92672">=</span> <span style="color:#a6e22e">match</span>($code) {
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">200</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;ok&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">404</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;not found&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">default</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\UnexpectedValueException</span>(<span style="color:#e6db74">&#34;unknown code: </span><span style="color:#e6db74">$code</span><span style="color:#e6db74">&#34;</span>),
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Le dernier est particulièrement utile : un match sans default lancera <code>UnhandledMatchError</code> automatiquement, mais parfois on veut contrôler le type d&rsquo;exception et le message.</p>
<h2 id="catch-sans-variable">catch sans variable</h2>
<p>Petite amélioration de qualité de vie. Quand on attrape une exception mais qu&rsquo;on n&rsquo;utilise pas réellement l&rsquo;objet, 8.0 permet d&rsquo;omettre la variable :</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>    $result <span style="color:#f92672">=</span> $cache<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>($key);
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">CacheMissException</span>) {
</span></span><span style="display:flex;"><span>    $result <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">compute</span>($key);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Avant 8.0, il fallait écrire <code>catch (CacheMissException $e)</code> et ensuite soit utiliser <code>$e</code> soit vivre avec l&rsquo;avertissement IDE sur la variable inutilisée. Aucune des deux options n&rsquo;était satisfaisante.</p>
<h2 id="les-fonctions-string-qui-auraient-dû-exister-depuis-des-années">Les fonctions string qui auraient dû exister depuis des années</h2>
<p>Trois fonctions que chaque développeur PHP a écrites manuellement au moins une fois :</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">str_contains</span>(<span style="color:#e6db74">&#39;hello world&#39;</span>, <span style="color:#e6db74">&#39;world&#39;</span>);  <span style="color:#75715e">// true
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">str_starts_with</span>(<span style="color:#e6db74">&#39;hello world&#39;</span>, <span style="color:#e6db74">&#39;hell&#39;</span>); <span style="color:#75715e">// true
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">str_ends_with</span>(<span style="color:#e6db74">&#39;hello world&#39;</span>, <span style="color:#e6db74">&#39;world&#39;</span>); <span style="color:#75715e">// true
</span></span></span></code></pre></div><p>Avant 8.0, les approches habituelles étaient <code>strpos() !== false</code>, <code>strncmp()</code>, ou <code>substr() ===</code>, qui nécessitent toutes de s&rsquo;arrêter pour se souvenir de la sémantique. Ces nouvelles fonctions sont juste directes et lisibles. Pas de regex, pas d&rsquo;arithmétique d&rsquo;offset.</p>
<h2 id="un-tri-stable">Un tri stable</h2>
<p>Les fonctions de tri de PHP n&rsquo;étaient pas stables avant 8.0. &ldquo;Pas stable&rdquo; signifie que les éléments qui se comparent comme égaux pouvaient se retrouver dans n&rsquo;importe quel ordre les uns par rapport aux autres. En pratique, ça causait des bugs subtils dans le code UI qui avait besoin d&rsquo;un ordre cohérent, une pagination qui changeait entre les chargements, et des tests qui ne passaient que par chance.</p>
<p>8.0 garantit la stabilité à travers toutes les fonctions de tri : <code>sort()</code>, <code>usort()</code>, <code>array_multisort()</code>, et le reste. Les éléments égaux conservent leur position relative originale. C&rsquo;est le comportement que la plupart des gens supposaient déjà être là.</p>
<h2 id="weakmap">WeakMap</h2>
<p>7.4 apportait <code>WeakReference</code> pour les objets simples. 8.0 apporte <code>WeakMap</code> : une map où les clés (des objets) et leurs données associées peuvent être ramassées par le GC quand aucune autre référence à l&rsquo;objet-clé n&rsquo;existe :</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">RequestCache</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">WeakMap</span> $cache;
</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>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">cache</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">WeakMap</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">get</span>(<span style="color:#a6e22e">Request</span> $request)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</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">cache</span>[$request] <span style="color:#f92672">??=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">compute</span>($request);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Dès que <code>$request</code> n&rsquo;est plus référencé ailleurs, l&rsquo;entrée disparaît de la map. Pas de nettoyage manuel nécessaire. C&rsquo;est le bon pattern pour la mémoïsation et les caches de propriétés calculées où on ne veut pas être la seule raison qu&rsquo;un objet reste vivant.</p>
<h2 id="les-nouveaux-types-dexception">Les nouveaux types d&rsquo;exception</h2>
<p><code>ValueError</code> est levée quand une fonction reçoit le bon type mais une valeur invalide, par opposition à <code>TypeError</code> qui se déclenche sur les mauvais 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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">array_chunk</span>([], <span style="color:#f92672">-</span><span style="color:#ae81ff">5</span>); <span style="color:#75715e">// ValueError: array_chunk(): Argument #2 ($length) must be greater than 0
</span></span></span></code></pre></div><p>Avant 8.0, beaucoup de ces cas étaient des warnings qui retournaient <code>false</code> ou <code>null</code>. Maintenant ils lèvent des exceptions. Le moteur est plus strict, ce qui signifie qu&rsquo;on attrape les problèmes plus tôt plutôt que d&rsquo;obtenir des résultats bizarres quelque part en aval.</p>
<h2 id="get_debug_type-et-fdiv">get_debug_type() et fdiv()</h2>
<p>Deux fonctions utilitaires à connaître.</p>
<p><code>get_debug_type()</code> retourne une représentation string normalisée de n&rsquo;importe quelle valeur, pratique pour les messages d&rsquo;erreur :</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">get_debug_type</span>(<span style="color:#ae81ff">1</span>);          <span style="color:#75715e">// &#34;int&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">get_debug_type</span>(<span style="color:#ae81ff">1.0</span>);        <span style="color:#75715e">// &#34;float&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">get_debug_type</span>(<span style="color:#66d9ef">null</span>);       <span style="color:#75715e">// &#34;null&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">get_debug_type</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Foo</span>());  <span style="color:#75715e">// &#34;Foo&#34; (pas &#34;object&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">get_debug_type</span>([]);         <span style="color:#75715e">// &#34;array&#34;
</span></span></span></code></pre></div><p>La différence avec <code>gettype()</code> : elle retourne les noms de classes pour les objets et utilise des noms normalisés (<code>&quot;int&quot;</code> pas <code>&quot;integer&quot;</code>). Exactement ce qu&rsquo;on veut pour construire un message d&rsquo;exception qui dit ce qu&rsquo;on a reçu versus ce qu&rsquo;on attendait.</p>
<p><code>fdiv()</code> effectue une division en virgule flottante suivant IEEE 754, ce qui signifie que la division par zéro retourne <code>INF</code>, <code>-INF</code>, ou <code>NAN</code> au lieu d&rsquo;un warning :</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">fdiv</span>(<span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">0</span>);   <span style="color:#75715e">// INF
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">fdiv</span>(<span style="color:#f92672">-</span><span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">0</span>);  <span style="color:#75715e">// -INF
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">fdiv</span>(<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>);    <span style="color:#75715e">// NAN
</span></span></span></code></pre></div><h2 id="les-changements-qui-cassent-des-choses">Les changements qui cassent des choses</h2>
<p>8.0 inclut aussi quelques changements qui ne sont pas des fonctionnalités, ce sont des corrections.</p>
<p>Le plus important : <code>0 == &quot;foo&quot;</code> est maintenant <code>false</code>. En PHP 7, comparer un entier à une string non numérique castait la string en 0, donc <code>0 == &quot;n'importe-quoi-non-numérique&quot;</code> s&rsquo;évaluait à <code>true</code>. C&rsquo;était une source persistante de bugs et de maux de tête de sécurité. PHP 8 l&rsquo;inverse : l&rsquo;entier est casté en string à la place :</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">var_dump</span>(<span style="color:#ae81ff">0</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;foo&#34;</span>);  <span style="color:#75715e">// bool(false) en 8.0, bool(true) en 7.x
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">var_dump</span>(<span style="color:#ae81ff">0</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span>);     <span style="color:#75715e">// bool(false) en 8.0, bool(true) en 7.x
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">var_dump</span>(<span style="color:#ae81ff">0</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;0&#34;</span>);    <span style="color:#75715e">// bool(true) dans les deux (&#34;0&#34; est numérique)
</span></span></span></code></pre></div><p>Si on s&rsquo;appuyait sur ça intentionnellement, on savait déjà que c&rsquo;était douteux. Si on ne savait pas qu&rsquo;on s&rsquo;en appuyait, 8.0 va trouver ces chemins de code.</p>
<p>Plusieurs fonctions qui retournaient des ressources retournent maintenant des objets propres : <code>curl_init()</code> retourne un <code>CurlHandle</code>, <code>imagecreate()</code> retourne un <code>GdImage</code>, <code>xml_parser_create()</code> retourne un <code>XMLParser</code>. Le code qui vérifie <code>is_resource($curl)</code> va casser, parce que <code>is_resource()</code> retourne <code>false</code> pour ces objets. La correction consiste à vérifier contre <code>false</code> (la valeur de retour en cas d&rsquo;échec) plutôt que de vérifier le type du cas de succès.</p>
<p>PHP 8.0 est le genre de version où les fonctionnalités se renforcent mutuellement. Les attributs se marient bien avec la promotion de constructeur. Match s&rsquo;associe naturellement avec les types union. Les fonctions string réduisent le bruit qui cachait l&rsquo;intention. Les corrections sont parfois cassantes, mais elles poussent le langage vers une cohérence qu&rsquo;il aurait dû avoir depuis des années.</p>
]]></content:encoded></item><item><title>Élagage des révisions avec des window functions et des logarithmes, quand DQL ne suffisait plus</title><link>https://guillaumedelre.github.io/fr/2020/09/27/%C3%A9lagage-des-r%C3%A9visions-avec-des-window-functions-et-des-logarithmes-quand-dql-ne-suffisait-plus/</link><pubDate>Sun, 27 Sep 2020 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2020/09/27/%C3%A9lagage-des-r%C3%A9visions-avec-des-window-functions-et-des-logarithmes-quand-dql-ne-suffisait-plus/</guid><description>Comment un score logarithmique et ROW_NUMBER() OVER PARTITION BY ont résolu la croissance incontrôlable d&amp;#39;une table de révisions après que DQL a atteint ses limites.</description><content:encoded><![CDATA[<p>Chaque mise à jour de contenu sur la plateforme crée une révision. C&rsquo;est délibéré : les éditeurs ont besoin d&rsquo;un historique sur lequel ils peuvent revenir, et la plateforme a besoin d&rsquo;une piste d&rsquo;audit. Ce que personne n&rsquo;avait anticipé, c&rsquo;était le rythme. Certains articles passent par quarante sauvegardes en un seul après-midi. Une pièce à fort trafic accumule des centaines de révisions sur sa durée de vie. Après quelques mois, la table de révisions avait plusieurs millions de lignes.</p>
<p>Les supprimer naïvement n&rsquo;était pas une option. &ldquo;Garder les 50 dernières&rdquo; perd tout contexte historique pour les articles qui n&rsquo;ont pas été touchés depuis un an. &ldquo;Garder une par jour&rdquo; perd tous les détails pour le contenu qui est activement édité. Ce dont on avait besoin, c&rsquo;était une distribution qui correspondait à la façon dont les révisions sont réellement utilisées : couverture dense pour l&rsquo;historique récent, couverture clairsemée pour l&rsquo;ancien.</p>
<p>C&rsquo;est une distribution logarithmique. Et la construire nécessitait du SQL brut.</p>
<h2 id="pourquoi-les-stratégies-simples-échouent">Pourquoi les stratégies simples échouent</h2>
<p>L&rsquo;attrait d&rsquo;une fenêtre fixe est évident : garder les N révisions les plus récentes et supprimer le reste. C&rsquo;est une ligne de SQL et zéro maths. Le problème, c&rsquo;est qu&rsquo;elle traite une révision d&rsquo;hier et une révision d&rsquo;il y a trois ans comme également précieuses, ce qu&rsquo;elles ne sont pas. Un éditeur qui ouvre un article de 2017 n&rsquo;a pas besoin de ses 50 dernières versions ; il pourrait avoir besoin d&rsquo;une par trimestre. Un article qui a été publié ce matin pourrait avoir besoin de chaque sauvegarde de la dernière heure.</p>
<p>Une stratégie temporelle (une révision par jour calendaire) a le problème inverse : elle est trop agressive pour le contenu actif. Si un article reçoit 30 sauvegardes entre 09h00 et 10h00, toutes sauf une disparaissent. Ce n&rsquo;est pas de l&rsquo;histoire, c&rsquo;est de l&rsquo;effacement.</p>
<p>Ni l&rsquo;une ni l&rsquo;autre ne peut exprimer &ldquo;garder plus de détails pour le contenu récent, moins pour le vieux&rdquo;. Cette relation est logarithmique.</p>
<h2 id="lidée-de-score">L&rsquo;idée de score</h2>
<p>L&rsquo;algorithme assigne à chaque révision un score basé sur son âge, puis garde seulement une révision par bucket de score. La formule de score produit des valeurs hautes et bien espacées pour les révisions récentes, et des valeurs petites et regroupées pour les anciennes.</p>
<p>L&rsquo;expression centrale, simplifiée, ressemble à ç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-sql" data-lang="sql"><span style="display:flex;"><span>(
</span></span><span style="display:flex;"><span>  ln( <span style="color:#66d9ef">EXTRACT</span>(epoch <span style="color:#66d9ef">FROM</span> (now() <span style="color:#f92672">-</span> created_at)) )
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">/</span>
</span></span><span style="display:flex;"><span>  ( <span style="color:#66d9ef">EXTRACT</span>(epoch <span style="color:#66d9ef">FROM</span> (now() <span style="color:#f92672">-</span> created_at)) <span style="color:#f92672">/</span> <span style="color:#ae81ff">6000</span> )
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">*</span> ( <span style="color:#ae81ff">1</span> <span style="color:#f92672">/</span> (<span style="color:#66d9ef">EXTRACT</span>(epoch <span style="color:#66d9ef">FROM</span> (now() <span style="color:#f92672">-</span> created_at)) <span style="color:#f92672">/</span> <span style="color:#ae81ff">60</span> <span style="color:#f92672">/</span> <span style="color:#ae81ff">1440</span>) )
</span></span><span style="display:flex;"><span><span style="color:#f92672">*</span> <span style="color:#ae81ff">1000</span>
</span></span></code></pre></div><p>Soit <code>s</code> l&rsquo;âge en secondes. La formule est grossièrement <code>ln(s) / s * C</code>, où le logarithme au numérateur et <code>s</code> au dénominateur font diminuer le résultat rapidement à mesure que <code>s</code> augmente.</p>
<p>Converti en entier, l&rsquo;effet est le suivant : une révision sauvegardée il y a 10 minutes pourrait scorer 8432, une sauvegardée il y a 11 minutes score 8431. Elles sont dans des buckets différents. Une révision d&rsquo;il y a six mois score 2, une d&rsquo;il y a huit mois score aussi 2. Même bucket. La window function choisit ensuite la révision la plus récente de chaque bucket et supprime le reste.</p>
<p>Le résultat est automatique : les sauvegardes récentes sont toutes gardées parce que chacune a un score distinct ; les anciennes sont élagées parce que beaucoup partagent le même score.</p>
<h2 id="la-tentative-dql-qui-na-pas-abouti">La tentative DQL qui n&rsquo;a pas abouti</h2>
<p>Les window functions ne font pas partie de DQL. Le langage de requête de Doctrine n&rsquo;a pas de syntaxe pour <code>OVER</code>, <code>PARTITION BY</code> ou <code>ROW_NUMBER()</code>. Avant de passer au SQL brut, l&rsquo;équipe a essayé de les ajouter.</p>
<p>L&rsquo;approche <code>FunctionNode</code> fonctionne pour les fonctions SQL simples, comme on l&rsquo;avait déjà vu avec la FTS. Un nœud <code>RowNumber</code> émettant <code>ROW_NUMBER()</code> est trivial :</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">RowNumber</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">FunctionNode</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">getSql</span>(<span style="color:#a6e22e">SqlWalker</span> $sqlWalker)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;ROW_NUMBER()&#39;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La partie plus difficile est <code>OVER(PARTITION BY ... ORDER BY ...)</code>. Un nœud de fonction <code>Over</code> a été ébauché, avec un nœud AST <code>PartitionByClause</code> personnalisé pour gérer la clause <code>PARTITION BY</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">class</span> <span style="color:#a6e22e">Over</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">FunctionNode</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">PartitionByClause</span> $partitionByClause <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">OrderByClause</span> $orderByClause <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 style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getSql</span>(<span style="color:#a6e22e">SqlWalker</span> $sqlWalker)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;OVER(&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">.</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">partitionByClause</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">?</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">partitionByClause</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($sqlWalker)
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">:</span> ($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">orderByClause</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">?</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">orderByClause</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($sqlWalker)
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;&#39;</span>))
</span></span><span style="display:flex;"><span>            <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></span></code></pre></div><p>Ça n&rsquo;a jamais été terminé. Les classes ont été livrées marquées <code>@deprecated</code> et &ldquo;NOT TESTED YET&rdquo;. Le problème est la composabilité : <code>FunctionNode</code> de DQL fonctionne bien pour les fonctions qui apparaissent dans les clauses WHERE ou les expressions SELECT. Une window function comme <code>ROW_NUMBER() OVER (PARTITION BY ...)</code> est une structure différente : elle apparaît dans une position SELECT, modifie la sémantique de la requête englobante, et exige que le parseur gère <code>PARTITION BY</code> comme une extension de la grammaire DQL. Rendre ça suffisamment robuste pour être fiable en production est un investissement significatif. Passer à DBAL et écrire le SQL directement a pris un après-midi.</p>
<h2 id="la-requête-couche-par-couche">La requête, couche par couche</h2>
<p>L&rsquo;implémentation finale est trois requêtes imbriquées :</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">FROM</span> revision
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> iri <span style="color:#f92672">=</span> <span style="color:#f92672">?</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span> id <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">IN</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">SELECT</span> id <span style="color:#66d9ef">FROM</span> (
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>            row_number() OVER (
</span></span><span style="display:flex;"><span>                PARTITION <span style="color:#66d9ef">BY</span> num, iri
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> num <span style="color:#66d9ef">DESC</span>, created_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span>            ) <span style="color:#66d9ef">AS</span> lines,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">*</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">FROM</span> (
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>                (
</span></span><span style="display:flex;"><span>                    ( ln( <span style="color:#66d9ef">EXTRACT</span>(epoch <span style="color:#66d9ef">FROM</span> (now() <span style="color:#f92672">-</span> created_at)) )
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">/</span> ( <span style="color:#66d9ef">EXTRACT</span>(epoch <span style="color:#66d9ef">FROM</span> (now() <span style="color:#f92672">-</span> created_at)) <span style="color:#f92672">/</span> <span style="color:#ae81ff">6000</span> ) )
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">*</span> ( <span style="color:#ae81ff">1</span> <span style="color:#f92672">/</span> (<span style="color:#66d9ef">EXTRACT</span>(epoch <span style="color:#66d9ef">FROM</span> (now() <span style="color:#f92672">-</span> created_at)) <span style="color:#f92672">/</span> <span style="color:#ae81ff">60</span> <span style="color:#f92672">/</span> <span style="color:#ae81ff">1440</span>) )
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">*</span> <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>                )::numeric::integer <span style="color:#66d9ef">AS</span> num,
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">*</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">FROM</span> revision
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">WHERE</span> iri <span style="color:#f92672">=</span> <span style="color:#f92672">?</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> created_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span>        ) <span style="color:#66d9ef">AS</span> lst
</span></span><span style="display:flex;"><span>    ) <span style="color:#66d9ef">AS</span> rst
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">WHERE</span> lines <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p><strong>Requête intérieure :</strong> calcule <code>num</code>, le score entier, pour chaque révision de l&rsquo;IRI donnée. Les lignes sont triées par <code>created_at DESC</code> à ce stade.</p>
<p><strong>Requête intermédiaire :</strong> exécute <code>ROW_NUMBER() OVER (PARTITION BY num, iri ORDER BY num DESC, created_at DESC)</code>. Dans chaque bucket de score (<code>num</code>), les révisions sont numérotées à partir de 1 dans l&rsquo;ordre décroissant d&rsquo;âge. La révision la plus récente de chaque bucket obtient <code>lines = 1</code>.</p>
<p><strong>Filtre extérieur :</strong> ne garde que les lignes <code>lines = 1</code>, une révision par bucket de score.</p>
<p><strong>DELETE :</strong> supprime chaque révision pour cet IRI qui n&rsquo;est pas dans l&rsquo;ensemble gardé.</p>
<p>Le <code>PARTITION BY num, iri</code> est redondant sur l&rsquo;IRI (toute la requête est déjà filtrée sur un IRI), mais rend l&rsquo;intention explicite et garde la logique correcte si la requête est un jour réutilisée dans un contexte plus large.</p>
<p>La méthode est appelée depuis une requête complémentaire qui identifie quels IRIs ont accumulé plus qu&rsquo;un seuil de révisions :</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">getIrisWithMoreRevisionThan</span>(<span style="color:#a6e22e">int</span> $maxRevisionsCount, <span style="color:#a6e22e">int</span> $limit <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>, <span style="color:#f92672">?</span><span style="color:#a6e22e">int</span> $retencyDay <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</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>    $queryBuilder <span style="color:#f92672">=</span> $this
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">createQueryBuilder</span>(<span style="color:#e6db74">&#39;revision&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">select</span>(<span style="color:#e6db74">&#39;revision.iri&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">groupBy</span>(<span style="color:#e6db74">&#39;revision.iri&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">having</span>(<span style="color:#e6db74">&#39;COUNT(1) &gt; :maxRevisions&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">orderBy</span>(<span style="color:#e6db74">&#39;COUNT(1)&#39;</span>, <span style="color:#a6e22e">Order</span><span style="color:#f92672">::</span><span style="color:#a6e22e">Descending</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">value</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setParameter</span>(<span style="color:#e6db74">&#39;maxRevisions&#39;</span>, $maxRevisionsCount);
</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></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">array_column</span>($queryBuilder<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getQuery</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getResult</span>(), <span style="color:#e6db74">&#39;iri&#39;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Les deux méthodes tournent ensemble dans un nettoyage planifié : trouver les IRIs au-dessus du seuil, élaguer chacun.</p>
<h2 id="le-câbler-à-une-commande-planifiée">Le câbler à une commande planifiée</h2>
<p>La requête d&rsquo;élagage ne s&rsquo;exécute pas dans une requête HTTP. Elle tourne derrière une commande Symfony, appelée sur un planning.</p>
<p>La commande prend quelques options pour contrôler son agressivité :</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(&#39;app:purge:revision&#39;, &#39;Remove useless revisions&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PurgeRevisionCommand</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">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">configure</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></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addOption</span>(<span style="color:#e6db74">&#39;max-revisions&#39;</span>, <span style="color:#e6db74">&#39;m&#39;</span>, <span style="color:#a6e22e">InputOption</span><span style="color:#f92672">::</span><span style="color:#a6e22e">VALUE_REQUIRED</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;Seuil de révisions au-dessus duquel un IRI est élaguée&#39;</span>, <span style="color:#ae81ff">30</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addOption</span>(<span style="color:#e6db74">&#39;limit&#39;</span>, <span style="color:#e6db74">&#39;l&#39;</span>, <span style="color:#a6e22e">InputOption</span><span style="color:#f92672">::</span><span style="color:#a6e22e">VALUE_REQUIRED</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;Nombre max d\&#39;IRIs à traiter par exécution&#39;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addOption</span>(<span style="color:#e6db74">&#39;delay&#39;</span>, <span style="color:#e6db74">&#39;w&#39;</span>, <span style="color:#a6e22e">InputOption</span><span style="color:#f92672">::</span><span style="color:#a6e22e">VALUE_REQUIRED</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;Délai en secondes entre chaque IRI&#39;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addOption</span>(<span style="color:#e6db74">&#39;retencyDay&#39;</span>, <span style="color:#e6db74">&#39;r&#39;</span>, <span style="color:#a6e22e">InputOption</span><span style="color:#f92672">::</span><span style="color:#a6e22e">VALUE_OPTIONAL</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;Ne traiter que les IRIs dont la dernière révision est plus vieille que N jours&#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">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>        $iris <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">revisionRepository</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getIrisWithMoreRevisionThan</span>(
</span></span><span style="display:flex;"><span>            (<span style="color:#a6e22e">int</span>) $input<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getOption</span>(<span style="color:#e6db74">&#39;max-revisions&#39;</span>),
</span></span><span style="display:flex;"><span>            (<span style="color:#a6e22e">int</span>) $input<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getOption</span>(<span style="color:#e6db74">&#39;limit&#39;</span>),
</span></span><span style="display:flex;"><span>            (<span style="color:#a6e22e">int</span>) $input<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getOption</span>(<span style="color:#e6db74">&#39;retencyDay&#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">foreach</span> ($iris <span style="color:#66d9ef">as</span> $iri) {
</span></span><span style="display:flex;"><span>            $totalDeleted <span style="color:#f92672">+=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">revisionRepository</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">deleteOldRevisionForIri</span>($iri);
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">usleep</span>((<span style="color:#a6e22e">int</span>) $input<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getOption</span>(<span style="color:#e6db74">&#39;delay&#39;</span>) <span style="color:#f92672">*</span> <span style="color:#ae81ff">1_000_000</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">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>L&rsquo;option <code>--delay</code> mérite attention : sur une base de données chargée, marteler une centaine d&rsquo;instructions <code>DELETE</code> dos à dos peut provoquer de la contention de verrous. Un petit sleep entre les itérations empêche l&rsquo;élagage d&rsquo;entrer en concurrence avec le trafic de production.</p>
<p>La commande tourne derrière deux entrées crontab avec des seuils différents :</p>
<pre tabindex="0"><code># Horaire : garder 30 révisions par IRI, traiter 100 IRIs par exécution
0 * * * * php bin/console app:purge:revision --max-revisions 30 --limit 100

# Nocturne : pour le contenu non touché depuis un an, garder seulement 3
0 0 * * * php bin/console app:purge:revision --max-revisions 3 --limit 100 --retencyDay 365
</code></pre><p>La stratégie à deux niveaux est importante. Le job horaire garde 30 révisions par IRI, ce qui est un plafond raisonnable pour le contenu activement édité. Le job nocturne cible seulement les IRIs non mis à jour depuis plus d&rsquo;un an et n&rsquo;en garde que 3. Un article qui n&rsquo;a pas bougé depuis douze mois n&rsquo;a pas besoin de trente versions dans son historique.</p>
<h2 id="ce-que-ça-donne-en-pratique">Ce que ça donne en pratique</h2>
<p>Un article sauvegardé 200 fois gardera typiquement 20 à 30 révisions après élagage : la plupart des sauvegardes récentes, quelques-unes du mois dernier, une ou deux de chaque trimestre de l&rsquo;année précédente. Le décompte exact dépend de la distribution d&rsquo;âge des sauvegardes, pas d&rsquo;un plafond arbitraire.</p>
<p>Un article mis à jour pour la dernière fois il y a deux ans pourrait se retrouver avec 5 ou 6 révisions. Les modifications récentes sont toutes là ; l&rsquo;ancien historique est compressé mais pas effacé.</p>
<p>Ce n&rsquo;est pas un historique parfait. C&rsquo;est un historique utile.</p>
<h2 id="la-frontière-entre-dql-et-sql-brut">La frontière entre DQL et SQL brut</h2>
<p>La tentative window function n&rsquo;est pas un échec à cacher. C&rsquo;est une donnée utile : <code>FunctionNode</code> fonctionne bien pour les fonctions scalaires dans les positions WHERE et SELECT, mais composer une expression complète <code>ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)</code> en DQL est plus difficile qu&rsquo;il n&rsquo;y paraît. L&rsquo;extension de grammaire, les nœuds AST, l&rsquo;intégration du SQL walker : c&rsquo;est une quantité non triviale de code pour quelque chose que le SQL natif gère en trois lignes.</p>
<p>La frontière pratique est grossièrement celle-ci : si une fonctionnalité PostgreSQL correspond à un appel de fonction d&rsquo;arité fixe, le DQL personnalisé convient. Si elle nécessite une nouvelle syntaxe de clause (frames de fenêtre, CTEs, lateral joins), le DBAL natif est généralement le meilleur compromis.</p>
]]></content:encoded></item><item><title>PHP 7.4 : les propriétés typées et les arrow functions qu'on attendait</title><link>https://guillaumedelre.github.io/fr/2020/01/12/php-7.4-les-propri%C3%A9t%C3%A9s-typ%C3%A9es-et-les-arrow-functions-quon-attendait/</link><pubDate>Sun, 12 Jan 2020 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2020/01/12/php-7.4-les-propri%C3%A9t%C3%A9s-typ%C3%A9es-et-les-arrow-functions-quon-attendait/</guid><description>Part 5 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 7.4 apporte les propriétés typées et les arrow functions concises — dernière version 7.x et prévisualisation la plus claire de PHP 8.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 7.4 est sorti le 28 novembre. C&rsquo;est la dernière version 7.x avant PHP 8.0, et ça se sent. Les fonctionnalités sont suffisamment substantielles pour tenir debout seules, mais elles ressemblent aussi à des fondations pour ce qui arrive.</p>
<h2 id="les-propriétés-typées">Les propriétés typées</h2>
<p>C&rsquo;est la grande nouveauté. Depuis PHP 7.0, on pouvait typer les paramètres de fonctions et les valeurs de retour. Mais les propriétés de classe ? Toujours non typées :</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">User</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">int</span> $id;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $name;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">DateTimeInterface</span> $deletedAt;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>7.4 change ça. Les propriétés typées font respecter les types à l&rsquo;affectation, pas seulement au niveau des sites d&rsquo;appel. Les classes deviennent auto-documentées d&rsquo;une façon que les docblocks n&rsquo;ont jamais vraiment réussi à faire, et le moteur attrape les erreurs de type avant qu&rsquo;elles ne se propagent dans la moitié de la stack.</p>
<p>Une subtilité : les propriétés typées sont <code>uninitialized</code> par défaut (pas <code>null</code>). Accéder à une propriété non initialisée lève une <code>Error</code>. C&rsquo;est un piège classique : <code>?string</code> n&rsquo;implique pas un défaut de <code>null</code>. Il faut encore un <code>= null</code> explicite pour ça.</p>
<h2 id="les-arrow-functions">Les arrow functions</h2>
<p>Les closures en PHP ont toujours nécessité d&rsquo;importer explicitement les variables de la portée extérieure avec <code>use</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>$multiplier <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
</span></span><span style="display:flex;"><span>$fn <span style="color:#f92672">=</span> <span style="color:#a6e22e">fn</span>($x) <span style="color:#f92672">=&gt;</span> $x <span style="color:#f92672">*</span> $multiplier; <span style="color:#75715e">// pas besoin de use()
</span></span></span></code></pre></div><p>Les arrow functions capturent automatiquement la portée englobante. Expression unique, retour implicite, pas de boilerplate. Elles ne remplacent pas les closures complètes pour la logique complexe, mais pour les callbacks courts, elles éliminent une catégorie de bruit qui s&rsquo;accumulait depuis des années.</p>
<h2 id="le-préchargement-opcache">Le préchargement opcache</h2>
<p>Pour les setups PHP-FPM à longue durée de vie, le préchargement permet à un script de charger et compiler des fichiers PHP en mémoire opcache au démarrage du serveur. Ces fichiers sont disponibles pour toutes les requêtes sans overhead de compilation.</p>
<p>Le gain varie selon l&rsquo;application. Sur les grands frameworks où les mêmes fichiers sont chargés à chaque requête, c&rsquo;est réel. Sur les petites applications, négligeable. Vaut la peine de benchmarker avant d&rsquo;ajouter la complexité de configuration.</p>
<h2 id="les-petites-choses-qui-saccumulent">Les petites choses qui s&rsquo;accumulent</h2>
<p>Les fonctionnalités mentionnées en passant méritent plus qu&rsquo;une ligne. L&rsquo;opérateur d&rsquo;affectation null-coalescente <code>??=</code> résout un pattern suffisamment agaçant à écrire à chaque fois, mais jamais assez pour se donner la peine de l&rsquo;abstraire :</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>$config[<span style="color:#e6db74">&#39;timeout&#39;</span>] <span style="color:#f92672">??=</span> <span style="color:#ae81ff">30</span>;
</span></span><span style="display:flex;"><span><span style="color:#75715e">// équivalent à : $config[&#39;timeout&#39;] = $config[&#39;timeout&#39;] ?? 30;
</span></span></span></code></pre></div><p>L&rsquo;opérateur spread dans les littéraux de tableau fait ce qu&rsquo;on attend de la version pour les appels de fonctions — dépacker un itérable dans un littéral de tableau :</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>$defaults <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;color&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;blue&#39;</span>, <span style="color:#e6db74">&#39;size&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;M&#39;</span>];
</span></span><span style="display:flex;"><span>$options <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;size&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;L&#39;</span>, <span style="color:#f92672">...</span>$defaults, <span style="color:#e6db74">&#39;weight&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1.2</span>];
</span></span><span style="display:flex;"><span><span style="color:#75715e">// [&#39;size&#39; =&gt; &#39;M&#39;, &#39;color&#39; =&gt; &#39;blue&#39;, &#39;weight&#39; =&gt; 1.2]
</span></span></span></code></pre></div><p>Note : les clés string n&rsquo;étaient pas supportées dans 7.4 pour le dépaquet de tableau. Ça viendra plus tard.</p>
<p>Les types de retour covariants et les types de paramètres contravariants comblent un vide qui rendait certains patterns d&rsquo;héritage inutilement maladroits. Une classe enfant peut maintenant affiner son type de retour vers un sous-type de celui du parent, sans erreur fatale :</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">Producer</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">get</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Iterator</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">ChildProducer</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Producer</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">get</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">ArrayIterator</span> {} <span style="color:#75715e">// ArrayIterator implémente Iterator
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="lire-des-nombres-à-3h-du-matin">Lire des nombres à 3h du matin</h2>
<p>Le séparateur de littéraux numériques est une de ces fonctionnalités dont on ne sait pas qu&rsquo;on la voulait jusqu&rsquo;à la première fois qu&rsquo;on écrit une grande constante et qu&rsquo;on perd immédiatement le sens de l&rsquo;ordre de grandeur :</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>$earthMass    <span style="color:#f92672">=</span> <span style="color:#ae81ff">5_972_168_000_000_000_000_000_000</span>; <span style="color:#75715e">// kg
</span></span></span><span style="display:flex;"><span>$lightSpeed   <span style="color:#f92672">=</span> <span style="color:#ae81ff">299_792_458</span>;                        <span style="color:#75715e">// m/s
</span></span></span><span style="display:flex;"><span>$planck       <span style="color:#f92672">=</span> <span style="color:#ae81ff">6.626</span><span style="color:#a6e22e">_070_15e</span><span style="color:#f92672">-</span><span style="color:#ae81ff">34</span>;                  <span style="color:#75715e">// J·s
</span></span></span><span style="display:flex;"><span>$hexMask      <span style="color:#f92672">=</span> <span style="color:#ae81ff">0xFF_EC_D5_08</span>;
</span></span><span style="display:flex;"><span>$binaryFlags  <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span><span style="color:#a6e22e">b0001_1111_0010_0000</span>;
</span></span></code></pre></div><p>L&rsquo;underscore est purement syntaxique. Le moteur le supprime avant de parser la valeur. On peut le mettre n&rsquo;importe où entre les chiffres, bien que la convention suive le groupement naturel du système numérique utilisé.</p>
<h2 id="référencer-sans-posséder">Référencer sans posséder</h2>
<p><code>WeakReference</code> permet de tenir une référence à un objet sans empêcher le ramasse-miettes de le détruire. Le cas d&rsquo;usage : les caches et registres — on veut savoir qu&rsquo;un objet est vivant, mais on ne veut pas être la raison qu&rsquo;il reste vivant :</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>$object <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">HeavyObject</span>();
</span></span><span style="display:flex;"><span>$ref <span style="color:#f92672">=</span> <span style="color:#a6e22e">WeakReference</span><span style="color:#f92672">::</span><span style="color:#a6e22e">create</span>($object);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">var_dump</span>($ref<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>()); <span style="color:#75715e">// object(HeavyObject)
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">unset</span>($object);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">var_dump</span>($ref<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>()); <span style="color:#75715e">// NULL — le GC l&#39;a collecté
</span></span></span></code></pre></div><p>Avant 7.4, il y avait <code>WeakRef</code> via une extension, et certains frameworks faisaient des tours de passe-passe avec <code>SplObjectStorage</code> qui ne se comportaient pas tout à fait pareil. La classe native est juste directe.</p>
<h2 id="la-sérialisation-sans-surprise">La sérialisation sans surprise</h2>
<p>La sérialisation personnalisée d&rsquo;objets avant 7.4 passait par l&rsquo;interface <code>Serializable</code> : implémenter <code>serialize()</code> et <code>unserialize()</code>, retourner une string, reconstruire depuis elle. Le problème est que <code>serialize()</code> déclenchait <code>__sleep()</code>, <code>unserialize()</code> déclenchait <code>__wakeup()</code>, et l&rsquo;interaction entre ces hooks était fragile, surtout dans les hiérarchies d&rsquo;héritage.</p>
<p>7.4 introduit <code>__serialize()</code> et <code>__unserialize()</code>, qui travaillent avec des tableaux plutôt que des strings et n&rsquo;interagissent pas avec les anciens hooks :</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">Session</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span> $token;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">\DateTime</span> $createdAt;
</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">__serialize</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">array</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> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">token</span>, <span style="color:#e6db74">&#39;created&#39;</span> <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">createdAt</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getTimestamp</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">__unserialize</span>(<span style="color:#66d9ef">array</span> $data)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> $data[<span style="color:#e6db74">&#39;token&#39;</span>];
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">createdAt</span> <span style="color:#f92672">=</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\DateTime</span>())<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setTimestamp</span>($data[<span style="color:#e6db74">&#39;created&#39;</span>]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Quand les nouvelles et anciennes méthodes coexistent sur la même classe, <code>__serialize()</code> gagne. L&rsquo;ancienne interface <code>Serializable</code> est dépréciée dans 8.1.</p>
<h2 id="ce-que-la-bibliothèque-standard-a-discrètement-reçu">Ce que la bibliothèque standard a discrètement reçu</h2>
<p><code>mb_str_split()</code> fait ce que <code>str_split()</code> fait mais correctement pour les strings multibyte. Le manque était franchement embarrassant pour un langage utilisé dans autant de locales que 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:#a6e22e">mb_str_split</span>(<span style="color:#e6db74">&#39;héllo&#39;</span>, <span style="color:#ae81ff">1</span>); <span style="color:#75715e">// [&#39;h&#39;, &#39;é&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">str_split</span>(<span style="color:#e6db74">&#39;héllo&#39;</span>, <span style="color:#ae81ff">1</span>);    <span style="color:#75715e">// [&#39;h&#39;, &#39;Ã&#39;, &#39;©&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;] — cassé
</span></span></span></code></pre></div><p><code>strip_tags()</code> accepte maintenant un tableau de tags autorisés, ce qui est plus propre que le format string qu&rsquo;il fallait passer auparavant :</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">strip_tags</span>($html, [<span style="color:#e6db74">&#39;p&#39;</span>, <span style="color:#e6db74">&#39;br&#39;</span>, <span style="color:#e6db74">&#39;strong&#39;</span>]); <span style="color:#75715e">// était : &#39;&lt;p&gt;&lt;br&gt;&lt;strong&gt;&#39;
</span></span></span></code></pre></div><p><code>proc_open()</code> accepte maintenant un tableau de commandes, contournant complètement l&rsquo;interprétation par le shell. Même idée que <code>subprocess</code> de Python avec <code>shell=False</code>. À retenir quand on passe des arguments fournis par l&rsquo;utilisateur à un processus externe.</p>
<h2 id="le-chapitre-ffi">Le chapitre FFI</h2>
<p>L&rsquo;extension Foreign Function Interface a atterri dans 7.4 après avoir passé du temps dans une branche feature. Elle permet à PHP d&rsquo;appeler des fonctions C natives en chargeant une bibliothèque partagée et en déclarant les signatures :</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>$ffi <span style="color:#f92672">=</span> <span style="color:#a6e22e">FFI</span><span style="color:#f92672">::</span><span style="color:#a6e22e">cdef</span>(<span style="color:#e6db74">&#34;int strlen(const char *s);&#34;</span>, <span style="color:#e6db74">&#34;libc.so.6&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">var_dump</span>($ffi<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">strlen</span>(<span style="color:#e6db74">&#34;hello&#34;</span>)); <span style="color:#75715e">// int(5)
</span></span></span></code></pre></div><p>Les applications pratiques sont étroites mais réelles : appeler des API de plateforme sans binding PHP, wrapper du code C critique pour les performances sans écrire une extension complète, ou juste jouer avec des bibliothèques natives directement. Ce n&rsquo;est pas un remplacement des extensions propres en production, mais ça supprime la barrière &ldquo;écrire une extension C&rdquo; pour l&rsquo;exploration.</p>
<h2 id="ce-qui-a-été-déprécié">Ce qui a été déprécié</h2>
<p>Quelques choses qui auraient dû être nettoyées depuis longtemps ont finalement reçu le traitement dépréciation dans 7.4.</p>
<p>Les ternaires imbriqués sans parenthèses ont toujours été ambigus. PHP les évaluait de gauche à droite alors que pratiquement tous les autres langages avec un ternaire évaluent de droite à gauche :</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">// Était ambigu, maintenant déprécié :
</span></span></span><span style="display:flex;"><span>$a <span style="color:#f92672">?</span> $b <span style="color:#f92672">:</span> $c <span style="color:#f92672">?</span> $d <span style="color:#f92672">:</span> $e;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Rendre explicite :
</span></span></span><span style="display:flex;"><span>($a <span style="color:#f92672">?</span> $b <span style="color:#f92672">:</span> $c) <span style="color:#f92672">?</span> $d <span style="color:#f92672">:</span> $e;
</span></span></code></pre></div><p>L&rsquo;accès par offset avec accolades pour les strings et tableaux — <code>$str{0}</code> au lieu de <code>$str[0]</code> — est déprécié et supprimé dans 8.0. C&rsquo;était toujours un alias, jamais une fonctionnalité distincte.</p>
<p><code>implode()</code> avec l&rsquo;ordre d&rsquo;arguments inversé (tableau en premier, colle en second) est déprécié. La fonction a accepté les deux ordres depuis le début, ce qui était une erreur. L&rsquo;ordre correct est <code>implode(string $separator, array $array)</code>.</p>
<h2 id="ce-qui-arrive-ensuite">Ce qui arrive ensuite</h2>
<p>7.4 est la dernière version 7.x. Les dépréciations sont principalement du déblayage pour les suppressions dans 8.0. Le backlog de RFCs pour 8.0 est substantiel : JIT, attributs, arguments nommés, expressions match. 7.4 est un bon endroit où atterrir en attendant que tout ça arrive.</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>PHP 7.3 : des petites victoires qui s'accumulent</title><link>https://guillaumedelre.github.io/fr/2019/01/20/php-7.3-des-petites-victoires-qui-saccumulent/</link><pubDate>Sun, 20 Jan 2019 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2019/01/20/php-7.3-des-petites-victoires-qui-saccumulent/</guid><description>Part 4 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 7.3 apporte une syntaxe heredoc flexible, des virgules finales dans les appels de fonctions, et des améliorations du quotidien qui s&amp;#39;accumulent vite.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 7.3 est sorti le 6 décembre. Pas de fonctionnalité phare. C&rsquo;est une collection d&rsquo;améliorations du quotidien qui, individuellement, semblent mineures, mais qui ensemble rendent le travail de tous les jours nettement moins agaçant.</p>
<h2 id="heredoc-et-nowdoc-flexibles">Heredoc et nowdoc flexibles</h2>
<p>Jusqu&rsquo;à 7.3, le marqueur de fermeture d&rsquo;un heredoc devait être en colonne zéro. Ce qui forçait une désindentation maladroite dans du code par ailleurs bien formaté :</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>$html <span style="color:#f92672">=</span> <span style="color:#e6db74">&lt;&lt;&lt;</span><span style="color:#e6db74">HTML</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &lt;div&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;p&gt;Hello&lt;/p&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &lt;/div&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">HTML; // devait être en colonne 0, moche
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">// après
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">$html = &lt;&lt;&lt;HTML
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &lt;div&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;p&gt;Hello&lt;/p&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &lt;/div&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    </span><span style="color:#e6db74">HTML</span>;
</span></span></code></pre></div><p>Le marqueur de fermeture peut désormais être indenté pour correspondre au code environnant, et cette indentation est retirée du contenu. Ça paraît cosmétique. Ce n&rsquo;est pas le cas. Les heredocs dans des contextes imbriqués (méthodes de classe, conditions) étaient visuellement dissonants avant. Maintenant ils s&rsquo;intègrent.</p>
<h2 id="array_key_first-et-array_key_last"><code>array_key_first()</code> et <code>array_key_last()</code></h2>
<p>Ce contournement existait depuis toujours :</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>$first <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_keys</span>($array)[<span style="color:#ae81ff">0</span>];
</span></span></code></pre></div><p>7.3 ajoute les helpers évidents :</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>$first <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_key_first</span>($array);
</span></span><span style="display:flex;"><span>$last  <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_key_last</span>($array);
</span></span></code></pre></div><p>Et <code>is_countable()</code> pour vérifier proprement avant d&rsquo;appeler <code>count()</code> sur quelque chose qui n&rsquo;implémente peut-être pas <code>Countable</code>. Des fonctions qui auraient dû exister depuis des années.</p>
<h2 id="pcre2">PCRE2</h2>
<p>Le moteur d&rsquo;expressions régulières a migré de PCRE vers PCRE2. Largement invisible pour les patterns existants, mais PCRE2 est activement maintenu et gère mieux les cas limites. L&rsquo;impact pratique principal : certains patterns qui produisaient auparavant un comportement indéfini lancent maintenant des erreurs. C&rsquo;est le bon comportement, même si ça surprend lors du premier upgrade.</p>
<h2 id="virgules-finales-dans-les-appels-de-fonctions">Virgules finales dans les appels de fonctions</h2>
<p>7.2 autorisait les virgules finales dans les imports de namespaces groupés. 7.3 étend ça aux appels de fonctions et de méthodes :</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>$result <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_merge</span>(
</span></span><span style="display:flex;"><span>    $defaults,
</span></span><span style="display:flex;"><span>    $overrides,
</span></span><span style="display:flex;"><span>    $extras, <span style="color:#75715e">// plus besoin de retirer cette virgule avant la parenthèse fermante
</span></span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>Ça compte surtout pour les appels multiligne. Ajouter ou retirer un argument ne nécessite plus de toucher à la ligne adjacente. Les diffs restent honnêtes, les rebases deviennent un peu moins douloureux.</p>
<h2 id="assignments-par-référence-dans-la-déstructuration-de-tableaux">Assignments par référence dans la déstructuration de tableaux</h2>
<p>La déstructuration de tableaux a gagné la capacité de capturer des références plutôt que des copies :</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>$data <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;Alice&#39;</span>, <span style="color:#ae81ff">42</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#f92672">&amp;</span>$name, $age] <span style="color:#f92672">=</span> $data;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Bob&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">var_dump</span>($data[<span style="color:#ae81ff">0</span>]); <span style="color:#75715e">// string(3) &#34;Bob&#34;
</span></span></span></code></pre></div><p>Les références imbriquées fonctionnent aussi :</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>[$a, [<span style="color:#f92672">&amp;</span>$b]] <span style="color:#f92672">=</span> [<span style="color:#ae81ff">1</span>, [<span style="color:#ae81ff">2</span>]];
</span></span></code></pre></div><p>Plus de niche que les virgules finales, mais le bon outil quand on a besoin d&rsquo;aliaser profondément dans une structure sans un tas d&rsquo;assignations intermédiaires.</p>
<h2 id="instanceof-avec-des-littéraux-est-maintenant-légal"><code>instanceof</code> avec des littéraux est maintenant légal</h2>
<p>Avant, utiliser <code>instanceof</code> avec un littéral à gauche était une erreur de parsing. 7.3 le rend valide :</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">var_dump</span>(<span style="color:#66d9ef">null</span> <span style="color:#a6e22e">instanceof</span> <span style="color:#66d9ef">stdClass</span>); <span style="color:#75715e">// bool(false)
</span></span></span></code></pre></div><p>Ça retourne toujours <code>false</code>, ce qui est exactement correct. L&rsquo;avantage, c&rsquo;est que du code qui construit conditionnellement une valeur puis vérifie son type n&rsquo;a plus besoin d&rsquo;extraire la valeur dans une variable au préalable. Utile dans le code généré et les helpers de test.</p>
<h2 id="json_decode-et-json_encode-peuvent-maintenant-lever-des-exceptions"><code>json_decode()</code> et <code>json_encode()</code> peuvent maintenant lever des exceptions</h2>
<p>Avant 7.3, les erreurs JSON étaient silencieuses à moins de penser à vérifier <code>json_last_error()</code>. Facile à oublier, facile à rater :</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>$data <span style="color:#f92672">=</span> <span style="color:#a6e22e">json_decode</span>($response);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">json_last_error</span>() <span style="color:#f92672">!==</span> <span style="color:#a6e22e">JSON_ERROR_NONE</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// la plupart des gens oubliaient cette partie
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>7.3 ajoute <code>JSON_THROW_ON_ERROR</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>$data <span style="color:#f92672">=</span> <span style="color:#a6e22e">json_decode</span>($response, <span style="color:#66d9ef">true</span>, <span style="color:#ae81ff">512</span>, <span style="color:#a6e22e">JSON_THROW_ON_ERROR</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// lève JsonException sur une entrée malformée
</span></span></span></code></pre></div><p><code>JsonException</code> étend <code>RuntimeException</code>. Attrapez-la spécifiquement ou laissez-la se propager. Ça aurait dû fonctionner comme ça dès le début.</p>
<h2 id="setcookie-avec-un-tableau-doptions"><code>setcookie()</code> avec un tableau d&rsquo;options</h2>
<p>L&rsquo;ancienne signature de <code>setcookie()</code> est un vestige : sept arguments positionnels, dont la plupart qu&rsquo;on laisse à leurs valeurs par défaut juste pour atteindre celui qu&rsquo;on veut vraiment. 7.3 ajoute une forme alternative qui prend un tableau associatif :</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">setcookie</span>(<span style="color:#e6db74">&#39;session&#39;</span>, $token, [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;expires&#39;</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">time</span>() <span style="color:#f92672">+</span> <span style="color:#ae81ff">3600</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;path&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;secure&#39;</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;httponly&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;samesite&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Lax&#39;</span>,
</span></span><span style="display:flex;"><span>]);
</span></span></code></pre></div><p>L&rsquo;option <code>samesite</code> est la vraie raison pour laquelle ça a été ajouté — l&rsquo;ancienne signature positionnelle n&rsquo;avait pas de slot pour elle. <code>session_set_cookie_params()</code> a reçu le même traitement, et une nouvelle directive ini <code>session.cookie_samesite</code> couvre la valeur par défaut.</p>
<h2 id="hrtime-pour-un-benchmarking-qui-mesure-vraiment-le-temps"><code>hrtime()</code> pour un benchmarking qui mesure vraiment le temps</h2>
<p><code>microtime()</code> lit l&rsquo;horloge murale. Très bien pour la plupart des cas. Mais elle est affectée par les ajustements NTP, et sa résolution dépend de l&rsquo;implémentation. <code>hrtime()</code> lit l&rsquo;horloge monotone haute résolution :</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>$start <span style="color:#f92672">=</span> <span style="color:#a6e22e">hrtime</span>(<span style="color:#66d9ef">true</span>);  <span style="color:#75715e">// nanosecondes sous forme d&#39;entier
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">doWork</span>();
</span></span><span style="display:flex;"><span>$elapsed <span style="color:#f92672">=</span> <span style="color:#a6e22e">hrtime</span>(<span style="color:#66d9ef">true</span>) <span style="color:#f92672">-</span> $start;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $elapsed <span style="color:#f92672">/</span> <span style="color:#ae81ff">1e6</span> <span style="color:#f92672">.</span> <span style="color:#e6db74">&#34; ms</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>;
</span></span></code></pre></div><p>Sans l&rsquo;argument <code>true</code>, elle retourne <code>[secondes, nanosecondes]</code> sous forme d&rsquo;un tableau à deux éléments. Utilisez ça pour les microbenchmarks, ou partout où la dérive d&rsquo;horloge corromprait silencieusement vos mesures.</p>
<h2 id="gc_status--regarder-à-lintérieur-du-ramasse-miettes"><code>gc_status()</code> — regarder à l&rsquo;intérieur du ramasse-miettes</h2>
<p>Le ramasse-miettes cyclique de PHP se déclenche quand un buffer de cycles potentiels se remplit. Jusqu&rsquo;à 7.3 il n&rsquo;y avait pas de moyen simple de voir ce qu&rsquo;il faisait réellement. <code>gc_status()</code> expose l&rsquo;état interne :</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>$status <span style="color:#f92672">=</span> <span style="color:#a6e22e">gc_status</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;runs&#39;       =&gt; 3,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//   &#39;collected&#39;  =&gt; 127,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//   &#39;threshold&#39;  =&gt; 10001,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//   &#39;roots&#39;      =&gt; 42,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// ]
</span></span></span></code></pre></div><p>Pas quelque chose que la plupart du code applicatif a besoin. Utile quand on essaie de comprendre pourquoi la mémoire continue de grimper sous des charges de travail spécifiques.</p>
<h2 id="compileerror-rejoint-la-hiérarchie-des-exceptions"><code>CompileError</code> rejoint la hiérarchie des exceptions</h2>
<p>Les erreurs de parsing sont catchables en tant que <code>ParseError</code> depuis PHP 7.0. 7.3 introduit <code>CompileError</code> comme classe parente pour les échecs à la compilation, avec <code>ParseError</code> qui devient une sous-classe :</p>
<pre tabindex="0"><code>Error
└── CompileError
    └── ParseError
</code></pre><p>En pratique, le code qui catch <code>ParseError</code> continue de fonctionner. La nouvelle classe donne juste aux futures erreurs de compilation (qui ne sont pas des erreurs de parsing) une place correcte dans la hiérarchie.</p>
<h2 id="bcscale-comme-getter"><code>bcscale()</code> comme getter</h2>
<p>L&rsquo;échelle BC Math était toujours settable via <code>bcscale($n)</code>. Obtenir l&rsquo;échelle actuelle nécessitait de la suivre soi-même. 7.3 fait fonctionner <code>bcscale()</code> sans 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:#a6e22e">bcscale</span>(<span style="color:#ae81ff">4</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#a6e22e">bcscale</span>(); <span style="color:#75715e">// 4
</span></span></span></code></pre></div><p>Mineur. Utile à savoir si vous écrivez du code de bibliothèque qui doit respecter ou restaurer le paramètre d&rsquo;échelle de l&rsquo;appelant.</p>
<h2 id="lavertissement-pour-continue-dans-un-switch">L&rsquo;avertissement pour <code>continue</code> dans un <code>switch</code></h2>
<p>Celui-là est un correctif d&rsquo;exactitude qui ressemble à une dépréciation. En PHP, <code>continue</code> dans un <code>switch</code> s&rsquo;est toujours comporté comme <code>break</code> — il sort du switch, pas de la boucle englobante. Les développeurs venant d&rsquo;autres langages écrivent souvent ça en espérant passer à l&rsquo;itération suivante de la boucle :</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">foreach</span> ($items <span style="color:#66d9ef">as</span> $item) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> ($item<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">type</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;skip&#39;</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>; <span style="color:#75715e">// FAUX : sort du switch, pas du foreach
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>7.3 ajoute un warning pour ce pattern. Le correctif, c&rsquo;est <code>continue 2</code> pour cibler explicitement la boucle englobante. Le comportement n&rsquo;a pas changé. Le silence, si.</p>
<h2 id="dépréciations">Dépréciations</h2>
<p>Les constantes insensibles à la casse déclarées via <code>define()</code> sont dépréciées :</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">define</span>(<span style="color:#e6db74">&#39;MY_CONST&#39;</span>, <span style="color:#ae81ff">42</span>, <span style="color:#66d9ef">true</span>); <span style="color:#75715e">// troisième argument déprécié
</span></span></span></code></pre></div><p>Passer une needle non-chaîne à <code>strpos()</code>, <code>strstr()</code>, et fonctions similaires est déprécié. En PHP 8, ces fonctions interpréteront la needle comme une chaîne, pas comme un codepoint ASCII. Si vous passez intentionnellement des entiers à ces fonctions, <code>chr($n)</code> est la forme explicite.</p>
<p><code>fgetss()</code> est déprécié — c&rsquo;était <code>fgets()</code> avec les balises HTML/PHP retirées. Utilisez <code>fgets()</code> et retirez les balises explicitement si nécessaire. Le filtre de stream <code>string.strip_tags</code> disparaît avec lui.</p>
<p>7.3 est le genre de version qu&rsquo;on apprécie avec du recul. Rien d&rsquo;individuellement dramatique, mais après six mois avec elle, la correction des heredocs seule a amorti le coût de la migration en lisibilité. Parfois le banal est exactement ce qu&rsquo;il faut.</p>
]]></content:encoded></item><item><title>PHP 7.2 : adieu mcrypt, bonjour sodium</title><link>https://guillaumedelre.github.io/fr/2018/01/14/php-7.2-adieu-mcrypt-bonjour-sodium/</link><pubDate>Sun, 14 Jan 2018 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2018/01/14/php-7.2-adieu-mcrypt-bonjour-sodium/</guid><description>Part 3 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 7.2 supprime l&amp;#39;extension mcrypt, abandonnée depuis des années, et intègre libsodium — la cryptographie PHP entre enfin dans le XXIe siècle.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 7.2 est sorti le 30 novembre. La grande nouvelle n&rsquo;est pas une nouvelle fonctionnalité, c&rsquo;est une suppression. <code>mcrypt</code> disparaît.</p>
<p>C&rsquo;est une bonne chose, même si ça ne le semble pas quand c&rsquo;est vous qui devez faire la migration.</p>
<h2 id="le-problème-mcrypt">Le problème mcrypt</h2>
<p><code>mcrypt</code> n&rsquo;est plus maintenu depuis 2007. Plus d&rsquo;une décennie de stagnation dans une bibliothèque cryptographique. Dépréciée en 7.1, elle est retirée définitivement en 7.2. Son remplaçant : <code>sodium</code>, désormais intégré comme extension core.</p>
<p>Sodium est le binding PHP pour <a href="https://libsodium.org" target="_blank" rel="noopener noreferrer">libsodium</a>, une bibliothèque cryptographique moderne conçue autour de valeurs par défaut sûres. Là où mcrypt vous demandait de choisir le bon algorithme, le bon mode et le bon padding (et vous laissait vous planter silencieusement), l&rsquo;API de sodium rend les choix dangereux structurellement difficiles. <code>sodium_crypto_secretbox()</code> pour le chiffrement symétrique, <code>sodium_crypto_box()</code> pour l&rsquo;asymétrique, <code>sodium_crypto_sign()</code> pour les signatures. Les noms disent ce qu&rsquo;on fait.</p>
<p>Si vous avez du code mcrypt en production, la migration est inévitable. Et ça vaut le coup de le faire soigneusement : du code cryptographique qui &ldquo;fonctionne encore&rdquo; peut être silencieusement cassé d&rsquo;une façon que vous ne remarquerez qu&rsquo;il sera trop tard.</p>
<h2 id="le-type-hint-object">Le type hint <code>object</code></h2>
<p>7.2 ajoute <code>object</code> comme type de paramètre et de retour :</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">function</span> <span style="color:#a6e22e">serialize</span>(<span style="color:#a6e22e">object</span> $data)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// accepte n&#39;importe quel objet
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>C&rsquo;est large — n&rsquo;importe quel objet le satisfait — mais c&rsquo;est mieux qu&rsquo;aucun type du tout quand vous ne vous souciez vraiment pas de la classe spécifique. Complète les types existants <code>array</code>, <code>callable</code> et les hints par classe concrète.</p>
<h2 id="argon2-dans-password_hash">Argon2 dans password_hash</h2>
<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>$hash <span style="color:#f92672">=</span> <span style="color:#a6e22e">password_hash</span>($password, <span style="color:#a6e22e">PASSWORD_ARGON2I</span>);
</span></span></code></pre></div><p><code>PASSWORD_BCRYPT</code> était la valeur par défaut et reste raisonnable, mais Argon2 a remporté le Password Hashing Competition pour une bonne raison : il est memory-hard, ce qui rend le craquage par GPU significativement plus coûteux. Vaut la peine de basculer pour les nouvelles apps.</p>
<p>7.2 est davantage une version sécurité qu&rsquo;une version langage. Supprimez mcrypt, ajoutez sodium, et vous placez la plateforme dans un état où on peut lui faire confiance avec des données sensibles. Les fonctionnalités du langage sont incrémentales. Le changement d&rsquo;infrastructure, lui, ne l&rsquo;est pas.</p>
<h2 id="des-types-de-paramètres-quon-peut-désormais-omettre-intentionnellement">Des types de paramètres qu&rsquo;on peut désormais omettre intentionnellement</h2>
<p>7.2 formalise quelque chose qui était jusqu&rsquo;ici juste une odeur de code : quand vous implémentez une interface ou surchargez une méthode, vous pouvez maintenant omettre complètement le type du paramètre. C&rsquo;est de la contravariance valide au sens du principe de substitution de Liskov.</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">interface</span> <span style="color:#a6e22e">Serializable</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">serialize</span>(<span style="color:#66d9ef">array</span> $data)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">JsonSerializer</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">Serializable</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">serialize</span>($data)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> { <span style="color:#75715e">// pas de type — accepte plus, toujours valide
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">json_encode</span>($data);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Ça paraît bizarre au premier abord. Mais c&rsquo;est logiquement correct : une méthode qui accepte tout est strictement plus permissive qu&rsquo;une qui n&rsquo;accepte que des tableaux. Le système de types est d&rsquo;accord, même si votre relecteur de code lève un sourcil.</p>
<h2 id="des-méthodes-abstraites-qui-évoluent">Des méthodes abstraites qui évoluent</h2>
<p>Quand une classe abstraite étend une autre classe abstraite, elle peut désormais surcharger la méthode abstraite avec une signature différente. La contrainte est directionnelle : les paramètres peuvent être élargis (contravariants), les types de retour peuvent être resserrés (covariants).</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">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BaseProcessor</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">string</span> $input);
</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">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">TypedProcessor</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">BaseProcessor</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">process</span>($input)<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span>; <span style="color:#75715e">// paramètre élargi, type de retour ajouté
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>C&rsquo;était refusé avant 7.2. Ça débloque des abstractions intermédiaires sans forcer chaque classe feuille à répéter la même signature.</p>
<h2 id="virgule-finale-dans-les-imports-groupés">Virgule finale dans les imports groupés</h2>
<p>Petit, mais je remarque son absence quand elle manque. Les imports de namespaces groupés peuvent désormais avoir une virgule finale après le dernier élé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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">App\Services\</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">UserService</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">OrderService</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">NotificationService</span>, <span style="color:#75715e">// virgule ici — enfin
</span></span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Ça signifie qu&rsquo;on peut réordonner ou ajouter des lignes sans toucher à la ligne précédente. Les diffs git deviennent plus propres, les conflits de merge plus rares.</p>
<h2 id="count-a-développé-une-conscience"><code>count()</code> a développé une conscience</h2>
<p>Avant 7.2, <code>count(null)</code> retournait 0. Silencieusement. Sans warning. C&rsquo;est exactement le genre de chose qui enterre un bug pendant des mois. Maintenant, ça émet un <code>E_WARNING</code> quand vous passez quelque chose qui n&rsquo;est ni un tableau ni un objet <code>Countable</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">count</span>(<span style="color:#66d9ef">null</span>);  <span style="color:#75715e">// Warning: count(): Parameter must be an array or an object that implements Countable
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">count</span>(<span style="color:#ae81ff">42</span>);    <span style="color:#75715e">// pareil
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">count</span>(<span style="color:#e6db74">&#34;hi&#34;</span>);  <span style="color:#75715e">// pareil
</span></span></span></code></pre></div><p>Le comportement n&rsquo;a pas changé pour les entrées valides. Seul le silence a été brisé. C&rsquo;est la bonne direction.</p>
<h2 id="spl_object_id--ce-que-vous-émuliez-avec-splobjectstorage"><code>spl_object_id()</code> — ce que vous émuliez avec SplObjectStorage</h2>
<p>Si vous avez déjà construit une map indexée par identité d&rsquo;objet, vous avez écrit quelque chose comme ç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>$storage <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">SplObjectStorage</span>();
</span></span><span style="display:flex;"><span>$storage[$obj] <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span></code></pre></div><p>7.2 ajoute <code>spl_object_id()</code>, qui retourne un entier unique pour la durée de vie d&rsquo;un objet. C&rsquo;est le même handle interne qu&rsquo;utilise <code>SplObjectStorage</code>, rendu directement accessible :</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>$id <span style="color:#f92672">=</span> <span style="color:#a6e22e">spl_object_id</span>($obj); <span style="color:#75715e">// ex. 42
</span></span></span><span style="display:flex;"><span>$map[$id] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;something&#39;</span>;
</span></span></code></pre></div><p>L&rsquo;entier est réutilisé après la destruction de l&rsquo;objet, donc ne le conservez pas au-delà de la durée de vie de l&rsquo;objet. Dans un contexte bien délimité cependant, c&rsquo;est une clé d&rsquo;identité peu coûteuse.</p>
<h2 id="pdo--les-chaînes-de-caractères-nationales">PDO : les chaînes de caractères nationales</h2>
<p>Quand on travaille avec des bases de données qui distinguent les types de chaînes régulières et nationales (Oracle, SQL Server), 7.2 ajoute les flags nécessaires :</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>$stmt <span style="color:#f92672">=</span> $pdo<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">prepare</span>(<span style="color:#e6db74">&#34;SELECT * FROM users WHERE name = ?&#34;</span>);
</span></span><span style="display:flex;"><span>$stmt<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">bindValue</span>(<span style="color:#ae81ff">1</span>, <span style="color:#e6db74">&#39;Ångström&#39;</span>, <span style="color:#a6e22e">PDO</span><span style="color:#f92672">::</span><span style="color:#a6e22e">PARAM_STR</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">PDO</span><span style="color:#f92672">::</span><span style="color:#a6e22e">PARAM_STR_NATL</span>);
</span></span></code></pre></div><p>Ou définir une valeur par défaut au niveau de la connexion :</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>$pdo<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setAttribute</span>(<span style="color:#a6e22e">PDO</span><span style="color:#f92672">::</span><span style="color:#a6e22e">ATTR_DEFAULT_STR_PARAM</span>, <span style="color:#a6e22e">PDO</span><span style="color:#f92672">::</span><span style="color:#a6e22e">PARAM_STR_NATL</span>);
</span></span></code></pre></div><p><code>PDO::PARAM_STR_NATL</code> indique que la chaîne est un type caractère national (NCHAR/NVARCHAR). Obscur, certes. Indispensable si vous avez déjà vu vos données Unicode ressortir déformées parce que le driver ne faisait pas la différence.</p>
<h2 id="gd-supporte-les-bmp-et-les-rectangles-de-clipping">GD supporte les BMP et les rectangles de clipping</h2>
<p>Deux choses à connaître. D&rsquo;abord, les fichiers BMP sont désormais des citoyens de première classe dans l&rsquo;extension GD :</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>$image <span style="color:#f92672">=</span> <span style="color:#a6e22e">imagecreatefrombmp</span>(<span style="color:#e6db74">&#39;photo.bmp&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">imagebmp</span>($image, <span style="color:#e6db74">&#39;output.bmp&#39;</span>);
</span></span></code></pre></div><p>Ensuite, on peut maintenant définir un rectangle de clipping pour que les opérations de dessin n&rsquo;affectent qu&rsquo;une portion de l&rsquo;image :</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">imagesetclip</span>($image, <span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">200</span>, <span style="color:#ae81ff">150</span>); <span style="color:#75715e">// x1, y1, x2, y2
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// tout ce qui est dessiné en dehors de ce rectangle est silencieusement ignoré
</span></span></span></code></pre></div><p>Aucune de ces fonctionnalités ne transforme la façon dont la plupart des apps fonctionnent, mais les deux remplacent &ldquo;installer une bibliothèque supplémentaire&rdquo; par &ldquo;c&rsquo;est juste dans le core maintenant.&rdquo;</p>
<h2 id="mb_chr-et-mb_ord--le-chr-et-ord-dunicode"><code>mb_chr()</code> et <code>mb_ord()</code> — le <code>chr()</code> et <code>ord()</code> d&rsquo;Unicode</h2>
<p>PHP a <code>chr()</code> et <code>ord()</code> depuis toujours. Ils travaillent sur des octets. Pour les points de code Unicode, vous étiez livré à vous-même. 7.2 ajoute les équivalents multibyte :</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>$char <span style="color:#f92672">=</span> <span style="color:#a6e22e">mb_chr</span>(<span style="color:#ae81ff">0x1F600</span>); <span style="color:#75715e">// retourne l&#39;emoji 😀
</span></span></span><span style="display:flex;"><span>$code <span style="color:#f92672">=</span> <span style="color:#a6e22e">mb_ord</span>(<span style="color:#e6db74">&#39;é&#39;</span>);     <span style="color:#75715e">// retourne 233
</span></span></span></code></pre></div><p>Et <code>mb_scrub()</code>, qui supprime les séquences d&rsquo;octets invalides d&rsquo;une chaîne plutôt que d&rsquo;échouer silencieusement ou de lancer une exception :</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>$clean <span style="color:#f92672">=</span> <span style="color:#a6e22e">mb_scrub</span>($untrustedInput, <span style="color:#e6db74">&#39;UTF-8&#39;</span>);
</span></span></code></pre></div><p>Pratique à toute frontière externe : réponses API, uploads de fichiers, lectures en base depuis des systèmes legacy.</p>
<h2 id="dépréciations-à-connaître-avant-larrivée-de-74">Dépréciations à connaître avant l&rsquo;arrivée de 7.4</h2>
<p>Plusieurs choses ont été mollement dépréciées en 7.2 et deviendront des erreurs dans les versions ultérieures. Celles qui risquent le plus de piquer :</p>
<p><code>__autoload()</code> est déprécié. Si vous enregistrez encore une fonction d&rsquo;autoload globale au lieu d&rsquo;utiliser <code>spl_autoload_register()</code>, corrigez ça avant que ça devienne fatal.</p>
<p><code>create_function()</code> est déprécié. C&rsquo;est un wrapper autour de <code>eval()</code> et ça a toujours été dangereux. Utilisez une closure :</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>$fn <span style="color:#f92672">=</span> <span style="color:#a6e22e">create_function</span>(<span style="color:#e6db74">&#39;$x&#39;</span>, <span style="color:#e6db74">&#39;return $x * 2;&#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>$fn <span style="color:#f92672">=</span> <span style="color:#a6e22e">fn</span>($x) <span style="color:#f92672">=&gt;</span> $x <span style="color:#f92672">*</span> <span style="color:#ae81ff">2</span>;
</span></span></code></pre></div><p><code>each()</code> est déprécié. Le pattern de boucle qu&rsquo;il permettait s&rsquo;écrit mieux avec <code>foreach</code>. Aucune perte ici.</p>
<p><code>parse_str()</code> sans second argument déverse les valeurs parsées dans la table des symboles locale — un problème de sécurité qui n&rsquo;aurait jamais dû être autorisé. Passez toujours la variable de sortie :</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">parse_str</span>($queryString, $params); <span style="color:#75715e">// correct
</span></span></span></code></pre></div><p>Le cast <code>(unset)</code> est déprécié parce qu&rsquo;il retourne toujours <code>null</code>, que vous pouvez simplement écrire <code>null</code>. Une syntaxe complètement inutile qui n&rsquo;aurait jamais dû exister.</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><item><title>Forcer l'UTC dans Doctrine sans toucher aux entités</title><link>https://guillaumedelre.github.io/fr/2017/02/19/forcer-lutc-dans-doctrine-sans-toucher-aux-entit%C3%A9s/</link><pubDate>Sun, 19 Feb 2017 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2017/02/19/forcer-lutc-dans-doctrine-sans-toucher-aux-entit%C3%A9s/</guid><description>Comment surcharger les types natifs de Doctrine pour imposer l&amp;#39;UTC partout, sans modifier une seule entité.</description><content:encoded><![CDATA[<p>Un timestamp qui revient de la base de données avec une heure de décalage. Pas à chaque fois. Uniquement quand le serveur de dev tourne en <code>Europe/Paris</code> et que la CI tourne en UTC. Le genre de bug qui disparaît quand on le cherche et qui revient en production un vendredi soir.</p>
<p>Le problème n&rsquo;est pas dans la logique métier. Il est dans ce que Doctrine fait discrètement avec les dates.</p>
<h2 id="ce-que-doctrine-fait-par-défaut">Ce que Doctrine fait par défaut</h2>
<p>Quand on déclare un champ <code>datetime</code> dans une entité Doctrine, la conversion entre PHP et la base de données passe par <code>DateTimeType</code>. Cette classe appelle <code>format()</code> sur l&rsquo;objet <code>DateTime</code> pour écrire en base, et <code>DateTime::createFromFormat()</code> pour le relire. Aucune mention de timezone nulle part.</p>
<p>Si l&rsquo;objet PHP est en <code>Europe/Paris</code>, Doctrine formate <code>2017-01-15 11:30:00</code> et l&rsquo;écrit tel quel. Si le serveur qui lit ce champ est en UTC, il obtient <code>2017-01-15 11:30:00</code> et l&rsquo;interprète comme UTC. Une heure s&rsquo;est évaporée dans l&rsquo;aller-retour, sans le moindre message d&rsquo;erreur.</p>
<p><a href="https://www.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/working-with-datetime.html" target="_blank" rel="noopener noreferrer">La doc Doctrine couvre ce sujet</a> et suggère des types personnalisés comme solution. Ce qu&rsquo;elle mentionne en passant, c&rsquo;est qu&rsquo;on peut donner à ces types personnalisés le même nom que les types natifs. Ce détail change tout.</p>
<h2 id="remplacer-pas-ajouter">Remplacer, pas ajouter</h2>
<p>La plupart des exemples de types Doctrine personnalisés introduisent un nouveau nom : <code>utc_datetime</code>, <code>app_date</code>, et ainsi de suite. On annote ensuite chaque champ avec <code>type: 'utc_datetime'</code> dans les entités. Ça fonctionne, mais c&rsquo;est fastidieux et ça ne protège pas contre un <code>type: 'datetime'</code> oublié.</p>
<p>L&rsquo;autre option : enregistrer le type personnalisé sous le nom <code>datetime</code>. Doctrine remplace sa propre implémentation par la nôtre, partout, sans exception. Chaque champ <code>datetime</code> de toutes les entités passe par notre logique, sans changer une seule annotation.</p>
<p>C&rsquo;est ce qu&rsquo;on vient de déployer sur notre plateforme de microservices PHP. Voici à quoi ça ressemble.</p>
<h2 id="le-trait-partagé">Le trait partagé</h2>
<p>Les deux types (<code>date</code> et <code>datetime</code>) partagent la même logique de conversion via un trait :</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">trait</span> <span style="color:#a6e22e">UTCDate</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">\DateTimeZone</span> $utc;
</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">convertToPHPValue</span>($value, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">\DateTime</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">null</span> <span style="color:#f92672">===</span> $value <span style="color:#f92672">||</span> $value <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">\DateTime</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> $value;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        $format <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getFormat</span>($platform);
</span></span><span style="display:flex;"><span>        $converted <span style="color:#f92672">=</span> <span style="color:#a6e22e">\DateTime</span><span style="color:#f92672">::</span><span style="color:#a6e22e">createFromFormat</span>($format, $value, $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getUtc</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>$converted) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\RuntimeException</span>(
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">sprintf</span>(<span style="color:#e6db74">&#39;Could not convert database value &#34;%s&#34; to DateTime using format &#34;%s&#34;.&#39;</span>, $value, $format)
</span></span><span style="display:flex;"><span>            );
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">postConvert</span>($converted);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $converted;
</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">abstract</span> <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getFormat</span>(<span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getUtc</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">\DateTimeZone</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">empty</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">utc</span>)) {
</span></span><span style="display:flex;"><span>            $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">utc</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\DateTimeZone</span>(<span style="color:#e6db74">&#39;UTC&#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">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">utc</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le point clé : <code>\DateTime::createFromFormat()</code> reçoit une timezone UTC explicite. La valeur brute issue de la base est interprétée comme UTC, peu importe la timezone configurée sur le serveur PHP.</p>
<h2 id="utcdatetimetype">UTCDateTimeType</h2>
<p>Pour les champs <code>datetime</code>, le chemin d&rsquo;écriture doit aussi imposer l&rsquo;UTC :</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">UTCDateTimeType</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">DateTimeType</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#a6e22e">UTCDate</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">convertToPHPValue</span>($value, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">\DateTime</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">null</span> <span style="color:#f92672">===</span> $value <span style="color:#f92672">||</span> $value <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">\DateTimeInterface</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">convertToPHPValue</span>($value, $platform);
</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">return</span> <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">convertToPHPValue</span>(<span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$value</span><span style="color:#e6db74">+0000&#34;</span>, $platform);
</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">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">convertToDatabaseValue</span>($value, <span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> ($value <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">\DateTime</span>) {
</span></span><span style="display:flex;"><span>            $value<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setTimezone</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getUtc</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">return</span> <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">convertToDatabaseValue</span>($value, $platform);
</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">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getFormat</span>(<span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $platform<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getDateTimeFormatString</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">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">postConvert</span>(<span style="color:#a6e22e">\DateTime</span> $converted)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>En lecture (<code>convertToPHPValue</code>), si la valeur est une chaîne brute, on y ajoute <code>+0000</code> avant de déléguer au parent. Le parent utilise ensuite ce suffixe de timezone pour créer correctement l&rsquo;objet PHP.</p>
<p>En écriture (<code>convertToDatabaseValue</code>), on force le <code>DateTime</code> en UTC avant de le formater. Ce qui va en base est toujours en UTC.</p>
<h2 id="utcdatetype">UTCDateType</h2>
<p>Pour les colonnes <code>date</code> (sans composante horaire), même approche avec une étape supplémentaire :</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">UTCDateType</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">DateType</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#a6e22e">UTCDate</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[\Override]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getFormat</span>(<span style="color:#a6e22e">AbstractPlatform</span> $platform)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $platform<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getDateFormatString</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">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">postConvert</span>(<span style="color:#a6e22e">\DateTime</span> $converted)<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>        $converted<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setTime</span>(<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La méthode <code>postConvert()</code> remet l&rsquo;heure à <code>00:00:00</code> après le parsing. Sans elle, un champ <code>date</code> pourrait revenir avec <code>23:59:59</code> ou <code>00:00:00+02:00</code> selon la timezone du serveur, ce qui casse les comparaisons et le tri.</p>
<h2 id="enregistrement-dans-symfony">Enregistrement dans Symfony</h2>
<p>La partie décisive : déclarer les types sous leurs noms natifs dans <code>config/packages/doctrine.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">doctrine</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dbal</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">types</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">date</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">class</span>: <span style="color:#ae81ff">App\Doctrine\DBAL\Types\UTCDateType</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">datetime</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">class</span>: <span style="color:#ae81ff">App\Doctrine\DBAL\Types\UTCDateTimeType</span>
</span></span></code></pre></div><p>C&rsquo;est tout. Doctrine échange ses propres implémentations contre les nôtres. Les entités existantes ne changent pas, les migrations ne bougent pas, les annotations restent <code>type: Types::DATETIME_MUTABLE</code>. Le comportement change globalement, sans friction.</p>
<h2 id="12-microservices-89-colonnes-un-bloc-de-config">12 microservices, 89 colonnes, un bloc de config</h2>
<p>Ces deux types tournent maintenant sur 12 microservices indépendants, chacun avec sa propre config Doctrine, couvrant 89 colonnes de production. Les serveurs CI tournent en UTC, les machines de dev en <code>Europe/Paris</code>, les données voyagent entre eux sans dériver. Ce n&rsquo;est pas spectaculaire. C&rsquo;est juste fiable.</p>
<p>La vraie leçon n&rsquo;est pas technique : un problème de timezone non résolu est un problème d&rsquo;intégrité des données. Les décalages s&rsquo;accumulent silencieusement, les comparaisons se trompent, les exports deviennent inexacts. Deux lignes de config et trois classes peuvent prévenir ça définitivement.</p>
]]></content:encoded></item><item><title>PHP 7.1 : un système de types plus rigoureux et les petits gains autour</title><link>https://guillaumedelre.github.io/fr/2017/01/15/php-7.1-un-syst%C3%A8me-de-types-plus-rigoureux-et-les-petits-gains-autour/</link><pubDate>Sun, 15 Jan 2017 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2017/01/15/php-7.1-un-syst%C3%A8me-de-types-plus-rigoureux-et-les-petits-gains-autour/</guid><description>Part 2 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 7.1 a comblé les lacunes laissées par la 7.0 : types nullables, retour void, et déstructuration — de petits ajouts qui ont rendu le système de types utilisable.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 7.1 est sorti le 1er décembre. Pas de titre &ldquo;2x plus rapide&rdquo;, pas de réécriture du moteur. Il comble les lacunes que la 7.0 avait laissées dans le système de types, et ces lacunes étaient vraiment agaçantes.</p>
<h2 id="les-types-nullables">Les types nullables</h2>
<p>La 7.0 permettait de déclarer <code>string $name</code> comme type de paramètre. Ce qu&rsquo;elle ne permettait pas, c&rsquo;était de dire &ldquo;ça peut aussi être null&rdquo;. On devait soit abandonner le type hint complètement, soit bricoler autour. La 7.1 ajoute le préfixe <code>?</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">function</span> <span style="color:#a6e22e">findUser</span>(<span style="color:#f92672">?</span><span style="color:#a6e22e">int</span> $id)<span style="color:#f92672">:</span> <span style="color:#f92672">?</span><span style="color:#a6e22e">User</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($id <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span>) <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</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">repository</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">find</span>($id);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Ça semble mineur. Ce n&rsquo;est pas le cas. Les types nullables font la différence entre une signature qui dit ce que fait une fonction et une qui ment par omission. Chaque codebase sur lequel j&rsquo;ai travaillé a des fonctions qui peuvent retourner null. Maintenant on peut vraiment le dire plutôt que de le cacher dans un docblock.</p>
<h2 id="le-type-de-retour-void">Le type de retour void</h2>
<p>Le complément du nullable : une fonction qui ne retourne intentionnellement 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-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">process</span>(<span style="color:#a6e22e">Order</span> $order)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatcher</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">OrderProcessed</span>($order));
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>void</code> rend l&rsquo;intention explicite et empêche de retourner accidentellement une valeur depuis une fonction qui ne devrait pas. Combiné aux types nullables, le système de types de PHP en 7.1 est bien plus expressif qu&rsquo;en 7.0.</p>
<h2 id="la-visibilité-des-constantes-de-classe">La visibilité des constantes de classe</h2>
<p>Un petit correctif mais bienvenu. Les constantes dans les classes étaient toujours publiques avant la 7.1. Maintenant :</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">Config</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">DB_PASSWORD</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;secret&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">VERSION</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;2.0&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">MAX_RETRIES</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Garder les détails d&rsquo;implémentation privés, ça compte. Ça aurait dû exister depuis le début.</p>
<h2 id="attraper-plusieurs-exceptions">Attraper plusieurs exceptions</h2>
<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>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">InvalidArgumentException</span> <span style="color:#f92672">|</span> <span style="color:#a6e22e">RuntimeException</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// gérer les deux
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Évite un bloc catch dupliqué quand deux exceptions nécessitent un traitement identique. Simple, utile.</p>
<h2 id="déstructurer-des-tableaux-sans-list">Déstructurer des tableaux sans list()</h2>
<p><code>list()</code> est dans PHP depuis la 4.0 et a toujours semblé un peu à côté syntaxiquement. La 7.1 ajoute un raccourci avec <code>[]</code> qui se lit bien plus naturellement :</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>[$first, $second] <span style="color:#f92672">=</span> $coordinates;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> ($rows <span style="color:#66d9ef">as</span> [$id, $name, $email]) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Elle gagne aussi le support des clés, ce qui rend la déstructuration de tableaux associatifs enfin utilisable :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>[<span style="color:#e6db74">&#39;id&#39;</span> <span style="color:#f92672">=&gt;</span> $id, <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> $name] <span style="color:#f92672">=</span> $user;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> ($records <span style="color:#66d9ef">as</span> [<span style="color:#e6db74">&#39;id&#39;</span> <span style="color:#f92672">=&gt;</span> $id, <span style="color:#e6db74">&#39;status&#39;</span> <span style="color:#f92672">=&gt;</span> $status]) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Avant ça, extraire des clés nommées d&rsquo;un tableau signifiait soit <code>extract()</code> (qui déverse tout dans le scope et invite les collisions) soit un tas d&rsquo;assignations individuelles. C&rsquo;est juste plus propre.</p>
<h2 id="le-type-iterable">Le type iterable</h2>
<p>Si on écrit une fonction qui accepte soit un tableau soit un générateur, il n&rsquo;y avait pas de type hint propre pour ça en 7.0. On typait soit en <code>array</code> et on excluait silencieusement les générateurs, soit on abandonnait le hint complètement :</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">function</span> <span style="color:#a6e22e">processItems</span>(<span style="color:#a6e22e">iterable</span> $items)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> ($items <span style="color:#66d9ef">as</span> $item) {
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">handle</span>($item);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>iterable</code> accepte tout ce sur quoi on peut faire un <code>foreach</code> : les tableaux et les implémentations de <code>Traversable</code>. Ça fonctionne aussi comme type de retour. Pas dramatique, mais ça comble un vrai manque.</p>
<h2 id="les-offsets-négatifs-sur-les-chaînes">Les offsets négatifs sur les chaînes</h2>
<p>L&rsquo;indexation de chaînes avec <code>[]</code> ou <code>{}</code> accepte maintenant les valeurs négatives, en comptant depuis la fin :</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>$str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;hello&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $str[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]; <span style="color:#75715e">// &#34;o&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $str[<span style="color:#f92672">-</span><span style="color:#ae81ff">2</span>]; <span style="color:#75715e">// &#34;l&#34;
</span></span></span></code></pre></div><p>Plusieurs fonctions de chaînes ont reçu le même traitement : <code>strpos()</code>, <code>substr()</code>, <code>substr_count()</code> et d&rsquo;autres acceptent maintenant un offset négatif. Cohérent avec ce que Python fait depuis toujours. Mieux vaut tard que jamais.</p>
<h2 id="closurefromcallable">Closure::fromCallable()</h2>
<p>Avant ça, convertir un callable (comme <code>[$object, 'method']</code> ou une chaîne nom de fonction) en une vraie <code>Closure</code> nécessitait <code>Closure::bind()</code> ou <code>bindTo()</code> avec une gestion de portée délicate. La 7.1 ajoute une méthode de fabrique statique :</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">Processor</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">transform</span>(<span style="color:#a6e22e">string</span> $value)<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">strtoupper</span>($value);
</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">getTransformer</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Closure</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Closure</span><span style="color:#f92672">::</span><span style="color:#a6e22e">fromCallable</span>([$this, <span style="color:#e6db74">&#39;transform&#39;</span>]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La closure résultante capture le bon <code>$this</code> et la bonne portée. C&rsquo;est particulièrement utile quand on passe des méthodes comme callbacks à des fonctions qui attendent un <code>callable</code>, ou quand on construit des pipelines.</p>
<h2 id="argumentcounterror">ArgumentCountError</h2>
<p>En PHP 7.0, appeler une fonction définie par l&rsquo;utilisateur avec trop peu d&rsquo;arguments générait un warning et l&rsquo;exécution continuait avec des paramètres remplis à <code>null</code>. En 7.1, ça lève une <code>ArgumentCountError</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">function</span> <span style="color:#a6e22e">connect</span>(<span style="color:#a6e22e">string</span> $host, <span style="color:#a6e22e">int</span> $port)<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><span style="display:flex;"><span><span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">connect</span>(<span style="color:#e6db74">&#39;localhost&#39;</span>); <span style="color:#75715e">// Lève ArgumentCountError
</span></span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">\ArgumentCountError</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>ArgumentCountError</code> étend <code>TypeError</code>, qui étend <code>Error</code>. Les call sites qui se dégradaient silencieusement auparavant échouent maintenant bruyamment. C&rsquo;est un risque de migration si on a du code qui comptait sur le comportement permissif, mais honnêtement, c&rsquo;est la bonne décision.</p>
<p>La 7.1 est le genre de version qui fait davantage faire confiance à une plateforme. La core team regardait clairement les frictions, pas seulement les titres à faire valoir.</p>
]]></content:encoded></item><item><title>PHP 7.0 : performances, types, et les fonctionnalités qui ont marqué</title><link>https://guillaumedelre.github.io/fr/2016/01/17/php-7.0-performances-types-et-les-fonctionnalit%C3%A9s-qui-ont-marqu%C3%A9/</link><pubDate>Sun, 17 Jan 2016 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2016/01/17/php-7.0-performances-types-et-les-fonctionnalit%C3%A9s-qui-ont-marqu%C3%A9/</guid><description>Part 1 of 11 in &amp;quot;Sorties PHP&amp;quot;: PHP 7.0 a doublé les performances grâce à une réécriture du Zend Engine et apporté enfin les type hints scalaires au langage.</description><category>php-releases</category><content:encoded><![CDATA[<p>PHP 7.0 est sorti le 3 décembre. Un mois et demi plus tard, j&rsquo;ai migré deux projets et les résultats sont difficiles à ignorer.</p>
<p>Le chiffre phare : 2x plus rapide que PHP 5.6. Ce n&rsquo;est pas un benchmark cherry-pick — c&rsquo;est la médiane sur des applications réelles. Le Zend Engine a été réécrit pour utiliser une nouvelle représentation interne des valeurs, ce qui réduit significativement l&rsquo;utilisation mémoire et diminue les allocations. Sur un projet, le temps de réponse moyen a chuté de 40% sans aucune modification du code. On met à jour, et ça va plus vite.</p>
<p>Mais les performances ne sont pas la partie la plus intéressante.</p>
<h2 id="les-types-enfin">Les types, enfin</h2>
<p>PHP a eu les type hints pour les objets depuis la 5.0, pour les tableaux depuis la 5.1. En 7.0, on peut enfin déclarer des types scalaires pour les paramètres de fonctions et les valeurs de retour :</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">function</span> <span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">int</span> $a, <span style="color:#a6e22e">int</span> $b)<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $a <span style="color:#f92672">+</span> $b;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>En mode strict (<code>declare(strict_types=1)</code>), passer un float à cette fonction lève une <code>TypeError</code>. En mode coercitif par défaut, PHP convertit la valeur. Cette distinction compte : le mode strict est par fichier, on peut donc l&rsquo;adopter progressivement sans tout casser d&rsquo;un coup.</p>
<p>Les déclarations de type de retour constituent l&rsquo;autre moitié. Placer l&rsquo;intention dans la signature plutôt que dans un docblock signifie que c&rsquo;est le moteur qui l&rsquo;applique, pas un code reviewer à moitié endormi.</p>
<h2 id="lopérateur-null-coalescent">L&rsquo;opérateur null coalescent</h2>
<p><code>??</code> est petit mais utilisé en permanence :</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>$username <span style="color:#f92672">=</span> $_GET[<span style="color:#e6db74">&#39;user&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;guest&#39;</span>;
</span></span></code></pre></div><p>Ça remplace <code>isset($_GET['user']) ? $_GET['user'] : 'guest'</code>. Il se chaîne aussi : <code>$a ?? $b ?? $c</code>. Après des années de bruit avec <code>isset()</code>, ça seul valait la mise à jour.</p>
<h2 id="la-partie-qui-casse">La partie qui casse</h2>
<p>La refonte de la gestion des erreurs est le vrai risque lors de la migration. Beaucoup d&rsquo;erreurs fatales sont maintenant des exceptions <code>Error</code>, attrapables mais différentes des <code>Exception</code>. Le code qui comptait sur les erreurs fatales pour stopper l&rsquo;exécution silencieusement a maintenant besoin d&rsquo;une gestion explicite. La suppression d&rsquo;erreurs avec <code>@</code> fonctionne aussi différemment par endroits.</p>
<p>Lire le guide de migration avant de toucher une appli en production. Le gain est réel, mais le fossé entre 5.6 et 7.0 est le plus large que PHP ait jamais eu.</p>
<h2 id="lopérateur-vaisseau-spatial">L&rsquo;opérateur vaisseau spatial</h2>
<p><code>&lt;=&gt;</code> est un opérateur de comparaison combiné qui retourne -1, 0 ou 1. Il est surtout là pour le tri :</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">usort</span>($users, <span style="color:#66d9ef">function</span> ($a, $b) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $a<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">age</span> <span style="color:#f92672">&lt;=&gt;</span> $b<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">age</span>;
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>Avant ça, un comparateur de tri personnalisé était un petit exercice de mémoire arithmétique. <code>$a - $b</code> fonctionne pour les entiers mais plante silencieusement pour les flottants. <code>&lt;=&gt;</code> fait ce qu&rsquo;il faut pour chaque type comparable.</p>
<h2 id="les-classes-anonymes">Les classes anonymes</h2>
<p>On peut maintenant instancier une classe définie en ligne, sur le moment, sans lui donner de 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>$logger <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">class</span>($config) <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">LoggerInterface</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:#66d9ef">array</span> $config) {}
</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">log</span>(<span style="color:#a6e22e">string</span> $message)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">file_put_contents</span>($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">config</span>[<span style="color:#e6db74">&#39;path&#39;</span>], $message <span style="color:#f92672">.</span> <span style="color:#a6e22e">PHP_EOL</span>, <span style="color:#a6e22e">FILE_APPEND</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Le cas d&rsquo;usage canonique, ce sont les doublures de test et les implémentations d&rsquo;interface ponctuelles qui ne méritent pas un fichier. Ça supprime une vraie friction : le fossé entre &ldquo;j&rsquo;ai besoin d&rsquo;un objet&rdquo; et &ldquo;je dois créer un fichier de classe pour un truc de 10 lignes&rdquo;.</p>
<h2 id="aléatoire-cryptographiquement-sûr">Aléatoire cryptographiquement sûr</h2>
<p>Les <code>rand()</code> et <code>mt_rand()</code> de PHP 5 n&rsquo;ont jamais été conçus pour la sécurité. La 7.0 ajoute deux fonctions qui le sont :</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>$token <span style="color:#f92672">=</span> <span style="color:#a6e22e">bin2hex</span>(<span style="color:#a6e22e">random_bytes</span>(<span style="color:#ae81ff">32</span>)); <span style="color:#75715e">// token hexadécimal de 64 caractères
</span></span></span><span style="display:flex;"><span>$pin   <span style="color:#f92672">=</span> <span style="color:#a6e22e">random_int</span>(<span style="color:#ae81ff">100000</span>, <span style="color:#ae81ff">999999</span>);
</span></span></code></pre></div><p><code>random_bytes()</code> puise dans le CSPRNG du système d&rsquo;exploitation. <code>random_int()</code> enveloppe ça pour les entiers. Ces fonctions remplacent tous les schémas de génération de tokens maison qui faisaient ça mal en silence, ce qui représente la majorité d&rsquo;entre eux.</p>
<h2 id="les-déclarations-use-groupées">Les déclarations use groupées</h2>
<p>Avant 7.0, importer cinq éléments depuis le même namespace nécessitait cinq instructions <code>use</code>. Maintenant :</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">App\Model\</span>{<span style="color:#a6e22e">User</span>, <span style="color:#a6e22e">Order</span>, <span style="color:#a6e22e">Product</span>};
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">App\Helpers\</span>{<span style="color:#a6e22e">formatDate</span>, <span style="color:#a6e22e">slugify</span>};
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">App\Config\</span>{<span style="color:#a6e22e">MAX_RETRIES</span>, <span style="color:#a6e22e">TIMEOUT</span>};
</span></span></code></pre></div><p>Petite amélioration ergonomique, mais qui réduit le bruit visuel en haut des fichiers avec des hiérarchies de namespaces profondes.</p>
<h2 id="les-générateurs-ont-grandi">Les générateurs ont grandi</h2>
<p>Les générateurs en 5.5 étaient intéressants mais incomplets. La 7.0 ajoute deux choses. Premièrement, un générateur peut maintenant avoir une valeur de retour, accessible après la fin de l&rsquo;itération :</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">function</span> <span style="color:#a6e22e">process</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Generator</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">yield</span> <span style="color:#e6db74">&#39;step 1&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">yield</span> <span style="color:#e6db74">&#39;step 2&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;done&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$gen <span style="color:#f92672">=</span> <span style="color:#a6e22e">process</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> ($gen <span style="color:#66d9ef">as</span> $step) { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $gen<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getReturn</span>(); <span style="color:#75715e">// &#34;done&#34;
</span></span></span></code></pre></div><p>Deuxièmement, <code>yield from</code> délègue à un autre générateur ou itérable, en transmettant transparemment les valeurs et valeurs de retour :</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">function</span> <span style="color:#a6e22e">inner</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Generator</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">yield</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">yield</span> <span style="color:#ae81ff">2</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;inner done&#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">function</span> <span style="color:#a6e22e">outer</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Generator</span> {
</span></span><span style="display:flex;"><span>    $result <span style="color:#f92672">=</span> <span style="color:#66d9ef">yield</span> <span style="color:#a6e22e">from</span> <span style="color:#a6e22e">inner</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">echo</span> $result; <span style="color:#75715e">// &#34;inner done&#34;
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">yield</span> <span style="color:#ae81ff">3</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Ça rend la composition de générateurs pratique sans avoir à câbler manuellement les valeurs entre eux.</p>
<h2 id="closurecall">Closure::call()</h2>
<p>Une façon plus directe de lier une closure à un objet et de l&rsquo;appeler 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:#66d9ef">class</span> <span style="color:#a6e22e">Counter</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">int</span> $count <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$increment <span style="color:#f92672">=</span> <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">int</span> $by)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span> {
</span></span><span style="display:flex;"><span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">count</span> <span style="color:#f92672">+=</span> $by;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$increment<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">call</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Counter</span>(), <span style="color:#ae81ff">5</span>);
</span></span></code></pre></div><p><code>bindTo()</code> existait avant mais nécessitait deux étapes. <code>call()</code> les fusionne et est plus rapide à l&rsquo;exécution car il évite la création d&rsquo;une closure intermédiaire.</p>
<h2 id="syntaxe-déchappement-unicode-dans-les-chaînes">Syntaxe d&rsquo;échappement Unicode dans les chaînes</h2>
<p>On peut maintenant intégrer des caractères Unicode directement dans les chaînes entre guillemets doubles ou les heredocs via un point de 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">echo</span> <span style="color:#e6db74">&#34;\u{1F418}&#34;</span>; <span style="color:#75715e">// 🐘
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#e6db74">&#34;\u{00E9}&#34;</span>;  <span style="color:#75715e">// é
</span></span></span></code></pre></div><p>C&rsquo;est mieux que de copier-coller des caractères depuis une table Unicode dans les fichiers sources, ce que les gens faisaient vraiment.</p>
<h2 id="un-unserialize-plus-sûr">Un unserialize() plus sûr</h2>
<p><code>unserialize()</code> a une longue histoire d&rsquo;être un vecteur d&rsquo;attaques par injection d&rsquo;objets. La 7.0 ajoute une option <code>allowed_classes</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>$data <span style="color:#f92672">=</span> <span style="color:#a6e22e">unserialize</span>($input, [<span style="color:#e6db74">&#39;allowed_classes&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>]);
</span></span><span style="display:flex;"><span>$data <span style="color:#f92672">=</span> <span style="color:#a6e22e">unserialize</span>($input, [<span style="color:#e6db74">&#39;allowed_classes&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#a6e22e">User</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>, <span style="color:#a6e22e">Order</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>]]);
</span></span></code></pre></div><p>Passer <code>false</code> empêche toute instanciation d&rsquo;objet pendant la désérialisation. C&rsquo;est le comportement par défaut à adopter quand on désérialise des données non fiables.</p>
<h2 id="1234-division-entière">:1234: Division entière</h2>
<p><code>intdiv()</code> est une division entière explicite sans intermédiaire flottant :</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>$pages <span style="color:#f92672">=</span> <span style="color:#a6e22e">intdiv</span>(<span style="color:#a6e22e">count</span>($items), $perPage); <span style="color:#75715e">// int, pas besoin de cast
</span></span></span></code></pre></div><p>Oui, on pourrait caster le résultat d&rsquo;une division. <code>intdiv()</code> rend l&rsquo;intention claire et évite les cas limites de précision flottante que le cast introduit pour les grands nombres.</p>
<h2 id="les-constantes-en-tableaux">Les constantes en tableaux</h2>
<p>Avant 7.0, <code>define()</code> n&rsquo;acceptait que les valeurs scalaires. Les tableaux fonctionnaient avec <code>const</code> au niveau de la classe ou du namespace mais pas avec <code>define()</code>. Maintenant si :</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">define</span>(<span style="color:#e6db74">&#39;HTTP_METHODS&#39;</span>, [<span style="color:#e6db74">&#39;GET&#39;</span>, <span style="color:#e6db74">&#39;POST&#39;</span>, <span style="color:#e6db74">&#39;PUT&#39;</span>, <span style="color:#e6db74">&#39;DELETE&#39;</span>, <span style="color:#e6db74">&#39;PATCH&#39;</span>]);
</span></span></code></pre></div><p>Utile pour la configuration qui doit être une constante mais qui vit en dehors d&rsquo;une classe.</p>
<h2 id="des-assertions-avec-des-dents">Des assertions avec des dents</h2>
<p><code>assert()</code> a reçu une vraie refonte. En PHP 5, les assertions étaient un eval de chaînes à l&rsquo;exécution. Maintenant elles peuvent lever des exceptions et être complètement supprimées en production avec zéro overhead :</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">// Dans php.ini ou au bootstrap :
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// assert.active = 1 (dev), 0 (prod)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// assert.exception = 1
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">assert</span>($user<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">isVerified</span>(), <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">\LogicException</span>(<span style="color:#e6db74">&#39;Unverified user reached checkout&#39;</span>));
</span></span></code></pre></div><p>Quand <code>assert.active = 0</code>, l&rsquo;expression n&rsquo;est jamais évaluée. Quand c&rsquo;est activé, une assertion qui échoue lève directement l&rsquo;exception fournie. C&rsquo;est enfin un outil qu&rsquo;on peut utiliser sans honte.</p>
<h2 id="la-refonte-de-session_start">La refonte de session_start()</h2>
<p><code>session_start()</code> accepte maintenant un tableau d&rsquo;options qui surchargent les directives <code>php.ini</code> pour cet appel :</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">session_start</span>([
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;cookie_lifetime&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">86400</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;cookie_secure&#39;</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;cookie_httponly&#39;</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;cookie_samesite&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Lax&#39;</span>,
</span></span><span style="display:flex;"><span>]);
</span></span></code></pre></div><p>Avant ça, on définissait soit les options globalement dans <code>php.ini</code>, soit on appelait <code>ini_set()</code> avant <code>session_start()</code>. Aucune des deux n&rsquo;était top quand on avait besoin de configurations de session différentes dans différentes parties d&rsquo;une appli.</p>
]]></content:encoded></item></channel></rss>