<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Guillaume Sainthillier</title>
    <description>The latest articles on DEV Community by Guillaume Sainthillier (@guillaume_sainthillier).</description>
    <link>https://dev.to/guillaume_sainthillier</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F292728%2F92cb4c01-def7-47b2-b874-7161f31890fd.jpg</url>
      <title>DEV Community: Guillaume Sainthillier</title>
      <link>https://dev.to/guillaume_sainthillier</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guillaume_sainthillier"/>
    <language>en</language>
    <item>
      <title>UX Icons JS : les icônes de Symfony UX, enfin en JavaScript, React et Vue</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:53:14 +0000</pubDate>
      <link>https://dev.to/guillaume_sainthillier/ux-icons-js-les-icones-de-symfony-ux-enfin-en-javascript-react-et-vue-c0h</link>
      <guid>https://dev.to/guillaume_sainthillier/ux-icons-js-les-icones-de-symfony-ux-enfin-en-javascript-react-et-vue-c0h</guid>
      <description>&lt;p&gt;Si vous avez un projet Symfony récent, il y a de bonnes chances que vos icônes passent par &lt;a href="https://symfony.com/bundles/ux-icons/current/index.html" rel="noopener noreferrer"&gt;Symfony UX Icons&lt;/a&gt;. Et c'est vrai que c'est agréable : une ligne de Twig, n'importe laquelle des plus de 200 000 icônes d'&lt;a href="https://icon-sets.iconify.design/" rel="noopener noreferrer"&gt;Iconify&lt;/a&gt;, un &lt;code&gt;ux:icons:lock&lt;/code&gt; avant de déployer, et c'est réglé.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ux_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'lucide:trash-2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'text-danger'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'aria-label'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Supprimer'&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tout va bien. Jusqu'au jour où cette même poubelle doit apparaître dans un contrôleur Stimulus. Ou dans le composant React de la page d'à côté. Là, Twig ne peut plus rien pour vous, et on finit tous par écrire ce genre de joyeuseté :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// assets/controllers/delete_controller.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TRASH_ICON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" aria-hidden="true"&amp;gt;&amp;lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11v6m4-6v6m5-11v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/&amp;gt;&amp;lt;/svg&amp;gt;`&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buttonTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TRASH_ICON&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un SVG copié-collé depuis le navigateur, des attributs recopiés à la main depuis &lt;code&gt;ux_icons.yaml&lt;/code&gt; (ou pas), et un &lt;code&gt;innerHTML&lt;/code&gt; qui fera tiquer votre Content Security Policy. Côté React, c'est &lt;code&gt;lucide-react&lt;/code&gt; qui s'invite dans le &lt;code&gt;package.json&lt;/code&gt; : une deuxième source d'icônes, avec ses propres versions et ses propres attributs par défaut. Et le jour où vous changez &lt;code&gt;default_icon_attributes&lt;/code&gt;, vos icônes Twig changent. Pas les autres. Le fameux "je synchroniserai plus tard".&lt;/p&gt;

&lt;h2&gt;
  
  
  L'histoire d'une PR refusée
&lt;/h2&gt;

&lt;p&gt;Mon premier réflexe a été de proposer la fonctionnalité directement à Symfony UX : &lt;a href="https://github.com/symfony/ux/pull/4072" rel="noopener noreferrer"&gt;symfony/ux#4072&lt;/a&gt;. L'idée : sur le warmup du cache, UX Icons exporte les icônes locales dans un module JavaScript, avec les attributs que &lt;code&gt;ux_icon()&lt;/code&gt; leur donne. Un petit package npm les affiche ensuite en JavaScript, React et Vue.&lt;/p&gt;

&lt;p&gt;La PR a été fermée. Et pour être honnête, les arguments se tiennent : les bundlers savent déjà importer des &lt;code&gt;.svg&lt;/code&gt; (avec SVGR ou &lt;code&gt;vite-svg-loader&lt;/code&gt;), et l'ajout de React et Vue en dépendances de toutes les applications qui utilisent UX Icons, ce n'était pas un bon deal.&lt;/p&gt;

&lt;p&gt;🤓 « &lt;em&gt;Mais alors, SVGR fait déjà le job, non ?&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;Pas tout à fait. SVGR n'a pas vu de release depuis août 2023, et &lt;code&gt;vite-svg-loader&lt;/code&gt; ne connaît que Vite et Vue. Surtout, aucun des deux ne couvre le JavaScript vanilla d'un contrôleur Stimulus. Et aucun ne lit votre config &lt;code&gt;ux_icons&lt;/code&gt; : vos icônes JS n'auront jamais les mêmes attributs que vos icônes Twig.&lt;/p&gt;

&lt;p&gt;Le code était écrit, testé, et il me servait. Je l'ai donc sorti en bundle à part, qui se branche sur un UX Icons non modifié. Open source, évidemment : je vous présente &lt;a href="https://github.com/silarhi/ux-icons-js-bundle" rel="noopener noreferrer"&gt;&lt;strong&gt;UX Icons JS Bundle&lt;/strong&gt;&lt;/a&gt;. Et React, Preact et Vue y sont des dépendances optionnelles : vous n'installez que ce dont vous vous servez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Une icône, quatre façons de l'afficher
&lt;/h2&gt;

&lt;p&gt;Le bundle dumpe vos icônes dans &lt;code&gt;var/icons/index.js&lt;/code&gt;, un export nommé par icône. Vous importez celle dont vous avez besoin, comme n'importe quel module :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// En JavaScript vanilla, par exemple dans un contrôleur Stimulus&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIconElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@silarhi/ux-icons-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LucideTrash2Icon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../var/icons/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buttonTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createIconElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LucideTrash2Icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aria-label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Supprimer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// En React (ou '@silarhi/ux-icons-js/preact' pour Preact, '@silarhi/ux-icons-js/vue' pour Vue)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UXIcon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@silarhi/ux-icons-js/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LucideTrash2Icon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../var/icons/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UXIcon&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;LucideTrash2Icon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-danger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Supprimer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà ce que vous obtenez : &lt;strong&gt;exactement le HTML de &lt;code&gt;ux_icon()&lt;/code&gt;&lt;/strong&gt;. Pas "à peu près le même". Le même, caractère pour caractère, avec vos &lt;code&gt;default_icon_attributes&lt;/code&gt;, les &lt;code&gt;icon_attributes&lt;/code&gt; de vos icon sets, vos alias et vos suffixes. Le tout sans un seul &lt;code&gt;innerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La magnifique démo de cet article : 👉 &lt;a href="https://labs.silarhi.fr/icons" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/icons&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En résumé, avant / après :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sans UX Icons JS&lt;/th&gt;
&lt;th&gt;Avec UX Icons JS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source des icônes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SVG copiés-collés, ou une lib par framework&lt;/td&gt;
&lt;td&gt;Vos icônes UX Icons, et rien d'autre&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attributs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recopiés à la main depuis &lt;code&gt;ux_icons.yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lus depuis la config, comme &lt;code&gt;ux_icon()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JavaScript vanilla&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;innerHTML&lt;/code&gt; et une chaîne de caractères&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createIconElement()&lt;/code&gt;, sans parser de HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React / Preact / Vue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lucide-react&lt;/code&gt;, &lt;code&gt;@heroicons/vue&lt;/code&gt;…&lt;/td&gt;
&lt;td&gt;Un composant &lt;code&gt;UXIcon&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Taille du bundle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ça dépend de la lib&lt;/td&gt;
&lt;td&gt;Seules les icônes importées&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nom d'icône venant d'une API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tout embarquer, ou un &lt;code&gt;switch&lt;/code&gt; géant&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loadIcon()&lt;/code&gt; : un chunk par icône&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nouvelle icône&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Télécharger, copier, coller&lt;/td&gt;
&lt;td&gt;Écrire l'import, &lt;code&gt;ux:icons:lock&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  C'est parti
&lt;/h2&gt;

&lt;p&gt;Deux packages, publiés depuis le même tag git : le bundle Symfony qui dumpe les icônes, et le package npm qui les affiche.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require silarhi/ux-icons-js-bundle
pnpm add @silarhi/ux-icons-js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si Flex ne l'a pas fait pour vous, enregistrez le bundle &lt;strong&gt;après&lt;/strong&gt; &lt;code&gt;UXIconsBundle&lt;/code&gt;, dont il réutilise la configuration :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/bundles.php&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nc"&gt;Symfony\UX\Icons\UXIconsBundle&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nc"&gt;Silarhi\UxIconsJsBundle\SilarhiUxIconsJsBundle&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et c'est tout pour la configuration. Les icônes sont dumpées au prochain warmup du cache, ou à la main pour voir ce qui se passe :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php bin/console ux-icons-js:dump &lt;span class="nt"&gt;-v&lt;/span&gt;
 &lt;span class="k"&gt;*&lt;/span&gt; lucide:trash-2
 &lt;span class="k"&gt;*&lt;/span&gt; user

 &lt;span class="o"&gt;[&lt;/span&gt;OK] Dumped 2 icon&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le dossier &lt;code&gt;var/icons/&lt;/code&gt; est généré : il n'a rien à faire dans git. Si &lt;code&gt;var/&lt;/code&gt; est déjà dans votre &lt;code&gt;.gitignore&lt;/code&gt; (c'est le cas par défaut), vous n'avez rien à faire. Avec AssetMapper, le bundle mappe le dossier tout seul, il ne reste qu'à lui donner un nom dans &lt;code&gt;importmap.php&lt;/code&gt;. Tout est détaillé dans &lt;a href="https://ux-icons-js-bundle.silarhi.dev/php/asset-mapper/" rel="noopener noreferrer"&gt;la doc&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Une exception : si votre front se build sans PHP (une CI JavaScript, un stage Node dans votre Dockerfile), il ne trouvera pas &lt;code&gt;var/icons/&lt;/code&gt;. Vous pouvez alors dumper les icônes dans &lt;code&gt;assets/&lt;/code&gt; et les committer, comme le fait la démo :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/packages/silarhi_ux_icons_js.yaml&lt;/span&gt;
&lt;span class="na"&gt;silarhi_ux_icons_js&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Dans git : le front se build et se type-check sans PHP (CI, Docker)&lt;/span&gt;
    &lt;span class="na"&gt;dump_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%kernel.project_dir%/assets/js/icons'&lt;/span&gt;

&lt;span class="na"&gt;when@prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;silarhi_ux_icons_js&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# L'image embarque les assets déjà buildés : son warmup ne doit pas écrire dans assets/&lt;/span&gt;
        &lt;span class="na"&gt;dump_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%kernel.project_dir%/var/icons'&lt;/span&gt;
        &lt;span class="na"&gt;dump_typescript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La contrepartie : le dump est refait à chaque reconstruction du cache, à vous de committer les changements après avoir ajouté une icône ou modifié les attributs de &lt;code&gt;ux_icons&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Le plus dur est fait ! &lt;strong&gt;TOP.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Les détails qui m'ont donné du fil à retordre
&lt;/h2&gt;

&lt;p&gt;Comme souvent, le gros du travail n'est pas là où on l'attend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Le même HTML, vraiment
&lt;/h3&gt;

&lt;p&gt;Promettre "le même HTML que &lt;code&gt;ux_icon()&lt;/code&gt;", c'est facile. Le tenir, c'est une autre histoire. Les attributs passés à &lt;code&gt;ux_icon()&lt;/code&gt; suivent les règles de la fonction &lt;a href="https://twig.symfony.com/doc/3.x/functions/html_attr.html" rel="noopener noreferrer"&gt;&lt;code&gt;html_attr()&lt;/code&gt;&lt;/a&gt; de Twig, et elles ont leurs subtilités :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;renderIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LucideHeartIcon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// … aria-hidden="true"&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;renderIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LucideHeartIcon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aria-label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Favori&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// … aria-label="Favori"&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;renderIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LucideHeartIcon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-icon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// … class="btn-icon" si isActive est false&lt;/span&gt;
&lt;span class="nf"&gt;renderIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LucideHeartIcon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// plus de height du tout&lt;/span&gt;
&lt;span class="nf"&gt;renderIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LucideHeartIcon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// … data-config="{&amp;amp;quot;id&amp;amp;quot;:1}"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aria-hidden="true"&lt;/code&gt; est ajouté sauf si l'icône a une alternative textuelle, &lt;code&gt;null&lt;/code&gt; supprime un attribut (même configuré), les tableaux deviennent des listes de classes, les objets des &lt;code&gt;data-*&lt;/code&gt; en JSON… Le côté JavaScript reproduit tout ça, jusqu'à la &lt;strong&gt;position&lt;/strong&gt; des attributs : un attribut redéfini garde sa place dans la balise &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, comme en PHP. C'est d'ailleurs un bug que j'avais laissé passer dans la PR, et que le bundle corrige.&lt;/p&gt;

&lt;p&gt;Pour être sûr de ne pas se raconter d'histoires, les tests comparent la sortie de &lt;code&gt;renderIcon()&lt;/code&gt; à celle de &lt;code&gt;ux_icon()&lt;/code&gt;, octet par octet. La démo le fait aussi, en direct dans votre navigateur.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un export par icône, pour le tree-shaking
&lt;/h3&gt;

&lt;p&gt;Chaque icône est un export nommé : le nom de l'icône en PascalCase, suivi de &lt;code&gt;Icon&lt;/code&gt;. Votre bundler ne garde que celles que vous importez, et les autres restent au placard.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Icône&lt;/th&gt;
&lt;th&gt;Export&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserIcon&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lucide:trash-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LucideTrash2Icon&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fa7-brands:x-twitter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Fa7BrandsXTwitterIcon&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_500Icon&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Petite subtilité : deux icônes peuvent donner le même nom d'export (&lt;code&gt;sub:check&lt;/code&gt; et un alias &lt;code&gt;sub-check&lt;/code&gt; donnent tous les deux &lt;code&gt;SubCheckIcon&lt;/code&gt;). Plutôt que d'en écraser une en silence, le dump échoue et vous donne les deux noms. Une icône qui change toute seule en prod, c'est le genre de bug qu'on met une après-midi à comprendre.&lt;/p&gt;

&lt;p&gt;Pour TypeScript, un &lt;code&gt;index.d.ts&lt;/code&gt; est dumpé à côté, avec le nom littéral de chaque icône.&lt;/p&gt;

&lt;h3&gt;
  
  
  Et quand le nom de l'icône vient d'une API ?
&lt;/h3&gt;

&lt;p&gt;C'est &lt;strong&gt;LE&lt;/strong&gt; cas qui m'a fait sortir une version 0.2.0 dans la foulée. Un import nommé, c'est très bien quand vous connaissez l'icône en écrivant le code. Mais quand votre API renvoie &lt;code&gt;{ "icon": "lucide:server" }&lt;/code&gt;, vous ne la connaissez qu'au runtime. Et embarquer toutes les icônes "au cas où", c'est exactement ce qu'on voulait éviter.&lt;/p&gt;

&lt;p&gt;Le bundle dumpe donc aussi un &lt;code&gt;loaders.js&lt;/code&gt;, qui associe chaque nom d'icône au module de cette icône seule :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIconElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadIcon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@silarhi/ux-icons-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loaders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../var/icons/loaders.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// null pour un nom inconnu&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;badge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createIconElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Votre bundler découpe chaque icône dans son propre chunk : importer &lt;code&gt;loaders&lt;/code&gt; ne télécharge aucune icône, et &lt;code&gt;loadIcon()&lt;/code&gt; ne télécharge que celle demandée. Une seule fois, même si dix composants la demandent en même temps. En React et Vue, c'est le composant &lt;code&gt;UXLazyIcon&lt;/code&gt;, avec un fallback pendant le chargement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UXLazyIcon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@silarhi/ux-icons-js/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loaders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../var/icons/loaders.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UXLazyIcon&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;loaders&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loaders&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;placeholder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et le &lt;code&gt;loaders.d.ts&lt;/code&gt; exporte l'union des noms dumpés, &lt;code&gt;IconName&lt;/code&gt;, pour typer vos réponses d'API ou vos props : une faute de frappe dans un nom d'icône ne passe plus la compilation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ux:icons:lock&lt;/code&gt;, côté JavaScript
&lt;/h3&gt;

&lt;p&gt;Avec UX Icons, le workflow classique est d'écrire &lt;code&gt;ux_icon('lucide:mail')&lt;/code&gt; dans un template, puis de lancer &lt;code&gt;ux:icons:lock&lt;/code&gt; : la commande trouve l'icône dans vos templates et la télécharge depuis Iconify. Je voulais exactement la même chose en JavaScript :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// assets/controllers/mail_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LucideMailIcon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../var/icons/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;php bin/console ux:icons:lock
 ✓ Imported lucide:mail &lt;span class="o"&gt;(&lt;/span&gt;License: ISC&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Render with: &lt;span class="o"&gt;{{&lt;/span&gt; ux_icon&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'lucide:mail'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}}&lt;/span&gt;
 JavaScript: import &lt;span class="o"&gt;{&lt;/span&gt; LucideMailIcon &lt;span class="o"&gt;}&lt;/span&gt;
 // Dumped 3 icon&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;JavaScript.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vous écrivez l'import d'une icône qui n'existe pas encore, vous lancez la commande, et elle existe. Le bundle scanne vos fichiers &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt;, &lt;code&gt;.vue&lt;/code&gt; et &lt;code&gt;.svelte&lt;/code&gt; à la recherche des imports nommés depuis un module dont le chemin contient &lt;code&gt;icons&lt;/code&gt;, et retrouve le nom de l'icône depuis le nom de l'export. Pas toujours une seule : &lt;code&gt;LucideTrash2Icon&lt;/code&gt; peut venir de &lt;code&gt;lucide:trash-2&lt;/code&gt; ou de &lt;code&gt;lucide:trash2&lt;/code&gt;. Tous les candidats sont proposés à &lt;code&gt;ux:icons:lock&lt;/code&gt;, qui ignore ceux qu'Iconify ne connaît pas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Et Preact ?
&lt;/h3&gt;

&lt;p&gt;Dernière arrivée, en 0.3.0 : Preact. Avec &lt;code&gt;preact/compat&lt;/code&gt;, le composant React fonctionnait déjà, mais autant se passer de la couche de compatibilité : &lt;code&gt;@silarhi/ux-icons-js/preact&lt;/code&gt; fournit &lt;code&gt;UXIcon&lt;/code&gt; et &lt;code&gt;UXLazyIcon&lt;/code&gt; pour Preact 10 et 11, dans le navigateur comme avec &lt;code&gt;preact-render-to-string&lt;/code&gt;, où il rend lui aussi le HTML de &lt;code&gt;ux_icon()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UXIcon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@silarhi/ux-icons-js/preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LucideTrash2Icon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../var/icons/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UXIcon&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;LucideTrash2Icon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-danger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;stroke&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Supprimer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seule différence avec React : Preact prend les noms d'attributs HTML. On écrit donc &lt;code&gt;class&lt;/code&gt; et &lt;code&gt;stroke-width&lt;/code&gt;, pas &lt;code&gt;className&lt;/code&gt; et &lt;code&gt;strokeWidth&lt;/code&gt; (&lt;code&gt;className&lt;/code&gt; reste accepté, et remplace la classe dumpée comme le fait &lt;code&gt;class&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP 8.2 et Symfony 6.4
&lt;/h3&gt;

&lt;p&gt;La PR visait la dernière version de UX Icons, donc PHP 8.4. Le bundle, lui, descend jusqu'à PHP 8.2, Symfony 6.4 et &lt;code&gt;symfony/ux-icons&lt;/code&gt; 2.36.1 : parce que dans la vraie vie, les projets ne sont pas tous à jour (les miens non plus 🤫). Il y a juste deux limites sur UX Icons 2.x, &lt;code&gt;ux:icons:lock&lt;/code&gt; n'y scanne pas le JavaScript, et les booléens n'y sont pas rendus pareil. Tout est dans &lt;a href="https://ux-icons-js-bundle.silarhi.dev/reference/compatibility/" rel="noopener noreferrer"&gt;les notes de compatibilité&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ça donne quoi au final ?
&lt;/h2&gt;

&lt;p&gt;La démo de cet article : 👉 &lt;a href="https://labs.silarhi.fr/icons" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/icons&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vous y trouverez la même icône rendue par Twig, par JavaScript et par React (avec la comparaison octet par octet), un bouton "copier" en Stimulus qui change d'icône sans &lt;code&gt;innerHTML&lt;/code&gt;, une notation en étoiles en React, et un tableau de statut dont les icônes viennent d'une API : ouvrez l'onglet réseau, chaque icône arrive dans son propre petit chunk. Le code est sur &lt;a href="https://github.com/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;silarhi/symfony-docker-ci&lt;/a&gt;, le projet qui me sert depuis des années pour les démos de ce blog.&lt;/p&gt;

&lt;p&gt;Pour les curieux, sous le capot :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.2+, Symfony 6.4, 7.x et 8.x, &lt;code&gt;symfony/ux-icons&lt;/code&gt; 2.36.1+ ou 3.5+&lt;/li&gt;
&lt;li&gt;React 18 / 19, Preact 10 / 11 et Vue 3 en dépendances optionnelles&lt;/li&gt;
&lt;li&gt;Webpack Encore, Vite, AssetMapper : n'importe quel bundler fait l'affaire&lt;/li&gt;
&lt;li&gt;Le corps du SVG est dumpé sous forme d'arbre, React, Preact et Vue le rendent en éléments : zéro &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;, zéro &lt;code&gt;v-html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;100 % du code couvert par les tests, et chaque combinaison de versions testée par la CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Voilà ! Vos icônes n'ont plus d'excuse pour exister en deux exemplaires. N'hésitez pas à laisser un commentaire si vous l'utilisez, ou si quelque chose coince. Et si le bundle vous fait gagner du temps, une petite ⭐ sur GitHub fait toujours plaisir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pour aller plus loin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/icons" rel="noopener noreferrer"&gt;La démo du tuto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ux-icons-js-bundle.silarhi.dev" rel="noopener noreferrer"&gt;La documentation du bundle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/ux-icons-js-bundle" rel="noopener noreferrer"&gt;Le code source du bundle sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://packagist.org/packages/silarhi/ux-icons-js-bundle" rel="noopener noreferrer"&gt;Le bundle sur Packagist&lt;/a&gt; et &lt;a href="https://www.npmjs.com/package/@silarhi/ux-icons-js" rel="noopener noreferrer"&gt;le package sur npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;Le code source de la démo sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/symfony/ux/pull/4072" rel="noopener noreferrer"&gt;La PR d'origine : symfony/ux#4072&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://symfony.com/bundles/ux-icons/current/index.html" rel="noopener noreferrer"&gt;La documentation de Symfony UX Icons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.silarhi.fr/presentation-picasso-bundle-symfony-image/" rel="noopener noreferrer"&gt;PicassoBundle, le composant image qui manquait à Symfony&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>symfony</category>
      <category>javascript</category>
      <category>php</category>
      <category>code</category>
    </item>
    <item>
      <title>L'authentification à double facteur avec Symfony (2FA)</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:09:33 +0000</pubDate>
      <link>https://dev.to/silarhi/lauthentification-a-double-facteur-avec-symfony-2fa-4g03</link>
      <guid>https://dev.to/silarhi/lauthentification-a-double-facteur-avec-symfony-2fa-4g03</guid>
      <description>&lt;p&gt;L’authentification à double facteur permet d’ajouter une sécurité supplémentaire à votre système de connexion. En plus d’un mot de passe, vous devrez fournir un code de vérification généré via une app de type Authenticator installée sur votre smartphone. On reviendra sur les termes et détails plus tard dans l'article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Démo
&lt;/h2&gt;

&lt;p&gt;Pour illustrer mes propos, voici le type de résultat que l'on peut obtenir grâce à cette librairie.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdaeovmvrym1a59gmxk8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdaeovmvrym1a59gmxk8.gif" alt="GIF de démo de l'authentification à double facteur" width="600" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La magnifique démo de cet article : 👉 &lt;a href="https://labs.silarhi.fr/login" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/login&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Pour commencer, il nous faut installer deux librairies. Dans un premier temps, pour la gestion du 2FA, nous allons utiliser &lt;a href="https://github.com/antonioribeiro/google2fa" rel="noopener noreferrer"&gt;Google2FA&lt;/a&gt; et pour la génération du QR Code pendant le setup, nous utiliserons &lt;a href="https://github.com/Bacon/BaconQrCode" rel="noopener noreferrer"&gt;BaconQrCode&lt;/a&gt;. Pour cela, nous allons utiliser composer :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require pragmarx/google2fa bacon/bacon-qr-code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mise en place
&lt;/h2&gt;

&lt;p&gt;Afin d'utiliser ces librairies, nous allons créer le controller &lt;code&gt;SecurityController&lt;/code&gt;. Dans ce controller, nous retrouvons la méthode qui permet l'authentification sur votre site, c'est Symfony qui se charge de vous le donner à l'aide de la commande &lt;code&gt;bin/console make:auth&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Allons plus loin et ajoutons 2 méthodes :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;La première qui va génerer et afficher le QR Code.&lt;/li&gt;
&lt;li&gt;La seconde qui va faire appel à un Authenticator afin de vérifier le code soumis.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pour scanner le QR Code vous aurez besoin d'une application mobile. Plusieurs sont disponibles, comme Authy, Google Authenticator, Microsoft Authenticator, etc. Toutes ces applications implémentent le même protocole et utilisent un QR code pour la mise en place du 2FA pour vote site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;//src/Controller/SecurityController.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;BaconQrCode\Renderer\Image\ImagickImageBackEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;BaconQrCode\Renderer\ImageRenderer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;BaconQrCode\Renderer\RendererStyle\RendererStyle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;BaconQrCode\Writer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PragmaRX\Google2FA\Google2FA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Bundle\FrameworkBundle\Controller\AbstractController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Attribute\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Authentication\AuthenticationUtils&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecurityController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;QR_CODE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'_qr_code_secret'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="na"&gt;#[Route(path: '/login', name: 'app_login')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;AuthenticationUtils&lt;/span&gt; &lt;span class="nv"&gt;$authenticationUtils&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// get the login error if there is one&lt;/span&gt;
        &lt;span class="nv"&gt;$error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$authenticationUtils&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLastAuthenticationError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// last username entered by the user&lt;/span&gt;
        &lt;span class="nv"&gt;$lastUsername&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$authenticationUtils&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLastUsername&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'security/login.html.twig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'last_username'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$lastUsername&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[Route(path: '/setup-2FA', name: 'app_security_setup_fa')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;AuthenticationUtils&lt;/span&gt; &lt;span class="nv"&gt;$authenticationUtils&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$authenticationUtils&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLastAuthenticationError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$google2fa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Google2FA&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QR_CODE_KEY&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$secretKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$google2fa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generateSecretKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QR_CODE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secretKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$secretKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QR_CODE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;//Generate QR CODE based on secretKey&lt;/span&gt;
        &lt;span class="nv"&gt;$qrCodeUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$google2fa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getQRCodeUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'2FA DEMO (Silarhi)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'hello@silarhi.fr'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$secretKey&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageRenderer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RendererStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImagickImageBackEnd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$qrCodeImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$writer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;writeString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$qrCodeUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'security/setup-2fa.html.twig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'qrCodeImage'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$qrCodeImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'secretKey'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[Route(path: '/2FA-protected', name: 'app_security_authentification_protected')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;authentificationProtected&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'security/protected.html.twig'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ici, dans la méthode &lt;code&gt;setup&lt;/code&gt;, on stocke la génération d'une clé secrète dans la session utilisateur, car nous en aurons besoin dans l'Authenticator Symfony.&lt;br&gt;&lt;br&gt;
Suite à cela, on génère l'image du QR Code associé, que l'on donne en paramètre à la vue twig pour l'afficher. On prévoit ensuite de créer un input pour venir vérifier le code qui sera généré par l'application d'authentification choisie par l'utilisateur. On délègue ensuite à la classe &lt;code&gt;TwoFactorsAuthenticator&lt;/code&gt; la responsabilité de vérifier le code qui vient d'être saisi !&lt;/p&gt;
&lt;h3&gt;
  
  
  security.yaml
&lt;/h3&gt;

&lt;p&gt;Nous allons principalement ajouter 2 authenticators : 1 pour gérer l'authentification de base (formulaire de login classique), l'autre qui s'activera lorsque l'utilisateur aura saisi le code du QR code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/packages/security.yaml&lt;/span&gt;

&lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords&lt;/span&gt;
    &lt;span class="na"&gt;password_hashers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;plaintext'&lt;/span&gt;
    &lt;span class="c1"&gt;# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider&lt;/span&gt;
    &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;users_in_memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;test'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;roles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ROLE_USER'&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;firewalls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^/(_(profiler|wdt)|css|images|js|assets)/&lt;/span&gt;
            &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;lazy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;users_in_memory&lt;/span&gt;
            &lt;span class="na"&gt;logout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/logout&lt;/span&gt;
            &lt;span class="na"&gt;entry_point&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;App\Security\LoginFormAuthenticator&lt;/span&gt;
            &lt;span class="na"&gt;custom_authenticator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;App\Security\LoginFormAuthenticator&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;App\Security\TwoFactorsAuthenticator&lt;/span&gt;

    &lt;span class="c1"&gt;# Easy way to control access for large sections of your site&lt;/span&gt;
    &lt;span class="c1"&gt;# Note: Only the *first* access control that matches will be used&lt;/span&gt;
    &lt;span class="na"&gt;access_control&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;^/setup-2FA&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;roles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ROLE_USER&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  L'authenticator
&lt;/h3&gt;

&lt;p&gt;Afin de vérifier si l'utilisateur a rentré le code de vérification, nous utilisons la méthode valideAuthentification, la route de cette méthode va être ajouté dans la constante &lt;code&gt;LOGIN_ROUTE&lt;/code&gt; de la classe &lt;code&gt;TwoFactorsAuthenticator&lt;/code&gt; que nous allons créer tout de suite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;//src/Security/TwoFactorsAuthenticator.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Controller\SecurityController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\EventSubscriber\DoubleAuthentificationSubscriber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PragmaRX\Google2FA\Google2FA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Generator\UrlGeneratorInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Core\Authentication\Token\NullToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Core\Authentication\Token\TokenInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Core\Exception\UserNotFoundException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Authenticator\Passport\Passport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Util\TargetPathTrait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TwoFactorsAuthenticator&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractLoginFormAuthenticator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;TargetPathTrait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;LOGIN_ROUTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'app_security_setup_fa'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;TokenStorageInterface&lt;/span&gt; &lt;span class="nv"&gt;$tokenStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;UrlGeneratorInterface&lt;/span&gt; &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SecurityController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QR_CODE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Passport&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//Get user from login form&lt;/span&gt;
        &lt;span class="nv"&gt;$existingToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tokenStorage&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$existingToken&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$existingToken&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;NullToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserNotFoundException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$existingToken&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$qrCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPayload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'qrCode'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$secretKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SecurityController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QR_CODE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$google2fa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Google2FA&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$google2fa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secretKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$google2fa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;verifyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$google2fa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSecret&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$qrCode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomUserMessageAuthenticationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'This code is not valid'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUserIdentifier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SelfValidatingPassport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserBadge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Passport&lt;/span&gt; &lt;span class="nv"&gt;$passport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$firewallName&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;TokenInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$currentToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$passport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$firewallName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$currentToken&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRoleNames&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DoubleAuthentificationSubscriber&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ROLE_2FA_SUCCEED&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostAuthenticationToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$currentToken&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nv"&gt;$firewallName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$roles&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getLoginUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LOGIN_ROUTE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;onAuthenticationSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;TokenInterface&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$firewallName&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$targetPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTargetPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$firewallName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;removeTargetPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$firewallName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$targetPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app_security_authentification_protected'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dans cette classe, nous récupérons les données postées du formulaire (donc le code de validation), dans la méthode &lt;code&gt;authenticate&lt;/code&gt;, on récupère la clé secrète stockée en session pour vérifier si tout correspond. Si tel est le cas, alors on lui attribut le rôle de &lt;code&gt;2FA_SUCCEED&lt;/code&gt; qui appartient à la classe &lt;code&gt;DoubleAuthentificationSubscriber&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Le Subscriber
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;//src/EventSubscriber/DoubleAuthentificationSubscriber.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\EventSubscriber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\EventDispatcher\EventSubscriberInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpKernel\Event\RequestEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpKernel\KernelEvents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\RouterInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Core\Authentication\Token\TokenInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DoubleAuthentificationSubscriber&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;EventSubscriberInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ROLE_2FA_SUCCEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ROLE_2FA_SUCCEED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;FIREWALL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;RouterInterface&lt;/span&gt; &lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;TokenStorageInterface&lt;/span&gt; &lt;span class="nv"&gt;$tokenStorage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getSubscribedEvents&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;KernelEvents&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;REQUEST&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'onKernelRequest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;onKernelRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RequestEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isMainRequest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'_route'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;\in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'app_security_authentification_protected'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$currentToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tokenStorage&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$currentToken&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;PostAuthenticationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app_login'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$currentToken&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FIREWALL_NAME&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$currentToken&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getFirewallName&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$currentToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ROLE_2FA_SUCCEED&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedirectResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app_security_setup_fa'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;hasRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;TokenInterface&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;\in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRoleNames&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cet EventSubscriber s'éxécute à chaque fois qu'une page est chargée, il va venir vérifier plusieurs éléments, comme la route qui est demandée ou encore, ce qui nous interesse, si l'utilisateur a le rôle &lt;code&gt;2FA_SUCCEED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;L'ajout du rôle est important, il permet à l'utilisateur de ne pas contourner la page où l'on vérifie le code de vérification. Car si le rôle n'est pas attribué, alors il sera redirigé vers le formulaire qui permet d'entrer le code de vérification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pour aller plus loin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/login" rel="noopener noreferrer"&gt;La démo du tuto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;Le code source utilisé sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>symfony</category>
      <category>php</category>
      <category>code</category>
    </item>
    <item>
      <title>Gérez vos sites de production avec Traefik, Docker &amp; Let's Encrypt</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:07:06 +0000</pubDate>
      <link>https://dev.to/silarhi/gerez-vos-sites-de-production-avec-traefik-docker-lets-encrypt-28p3</link>
      <guid>https://dev.to/silarhi/gerez-vos-sites-de-production-avec-traefik-docker-lets-encrypt-28p3</guid>
      <description>&lt;p&gt;Vous avez une vieille app legacy en PHP 5 sur votre serveur de production qui vous empêche de déployer votre belle app Symfony 5 ? Vous montez un nouveau serveur de production et vous vous demandez comment gérer facilement des stacks différentes et l'ajout de nouveaux sites ? &lt;strong&gt;Vous voulez faire la fête comme cet ananas sur la cover ?&lt;/strong&gt; Docker est peut-être la solution. En le combinant avec Traefik, vous avez une solution &lt;strong&gt;simple et rapide&lt;/strong&gt; pour gérer vos apps de production et même le renouvellement automatique de vos certificats SSL avec &lt;a href="https://letsencrypt.org/" rel="noopener noreferrer"&gt;Let's Encrypt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pré-requis : Je pars du principe que vous avez un minimum de notions avec Docker et docker-compose (au moins des notions de Dockerfile et de routage de ports entre le conteneur et le host). Je pars aussi du principe que vous êtes quelqu'un de bien et que votre serveur de production est sous Linux.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Traefik, c'est quoi ?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b4z4udsfk4zt1prayvt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b4z4udsfk4zt1prayvt.png" alt="Schéma d'architecture Traefik" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Crédits : &lt;a href="https://traefik.io/" rel="noopener noreferrer"&gt;https://traefik.io/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;En fait, Traefik va jouer le rôle de reverse proxy. C'est à dire qu'il va être le récepteur frontal de toutes les requêtes HTTP qui vont venir &lt;a href="https://www.youtube.com/watch?v=mN-L_QC8oGc" rel="noopener noreferrer"&gt;des Internets&lt;/a&gt; et va les rediriger vers les bonnes instances de vos conteneurs Docker. Et c'est bien normal de mettre en place un reverse proxy quand vous avez plus d'une app Docker à exposer : vous ne pouvez pas toutes les faire écouter sur le port 80 ou 443. Vous voulez aussi un joli nom de domaine associé à votre instance Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation de Traefik
&lt;/h2&gt;

&lt;p&gt;La première étape est bien sûr d'&lt;a href="https://docs.docker.com/install/linux/docker-ce/debian/" rel="noopener noreferrer"&gt;installer Docker&lt;/a&gt;. Une fois chose faite, on va gérer &lt;strong&gt;tout le reste&lt;/strong&gt; avec Docker Compose, désormais un plugin de Docker : la commande devient &lt;code&gt;docker compose&lt;/code&gt;, sans tiret. L'installation est assez simple selon la &lt;a href="https://docs.docker.com/compose/install/" rel="noopener noreferrer"&gt;documentation &lt;/a&gt; de Docker :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;ca-certificates curl
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/debian/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="c"&gt;# Add the repository to Apt sources:&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On va commencer par créer un network qui sera partagé entre tous les conteneurs de nos applications. Je l'ai appelé "web" parce que j'aime être original mais aussi parce que c'est dur de trouver des noms, comme le rappelle ce brave &lt;em&gt;Phil&lt;/em&gt; :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are only two hard things in Computer Science: cache invalidation and naming things.&lt;br&gt;&lt;br&gt;
-- Phil Karlton&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On crée ensuite notre fichier docker-compose.yml (j'ai pris pour habitude de mettre la configuration des conteneurs directement dans un dossier &lt;strong&gt;/apps&lt;/strong&gt; à la racine du serveur de production, mais vous pouvez bien sûr changer l'emplacement selon vos habitudes) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /apps/traefik/docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;reverse-proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PWD/traefik.toml:/etc/traefik/traefik.toml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$PWD/acme.json:/acme.json&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les directives vont créer une instance à partir de &lt;a href="https://hub.docker.com/_/traefik" rel="noopener noreferrer"&gt;l'image officielle&lt;/a&gt; de Traefik. On veut qu'il réponde à la fois pour HTTP et HTTPS parce qu'&lt;strong&gt;en 2025, y'a plus d'excuses pour ne pas proposer par défaut des apps sécurisées&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Le conteneur est intégré au sein du network "web" que l'on vient de créer juste avant (c'est pourquoi on le tag comme external car il est géré en dehors de docker-compose).&lt;/p&gt;

&lt;p&gt;🤓 « &lt;em&gt;C'est quoi ce port 8080 d'ouvert ?&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;Bien vu. Traefik dispose aussi d'une petite interface d'administration pour les services qu'il gère. Nous verrons en fin d'article comment sécuriser ce dashboard.&lt;/p&gt;

&lt;p&gt;Pour l'explication des volumes utilisés, il y a 3 raisons.&lt;br&gt;&lt;br&gt;
La première : on utilise Traefik avec Docker. Traefik peut également fonctionner avec d'autres &lt;a href="https://docs.traefik.io/providers/overview/" rel="noopener noreferrer"&gt;providers&lt;/a&gt; (Kubernetes pour les chauds, Consul, une configuration par fichiers, etc), mais dans notre cas on va directement se brancher sur le socket interne de l'engine Docker pour être notifié à chaque création / destruction de conteneurs. Ça va être super pratique pour monter un nouveau site en moins de 5 secondes.&lt;br&gt;&lt;br&gt;
La deuxième : il nous faut modifier la configuration par défaut de Traefik pour qu'elle colle parfaitement à nos besoins. C'est pourquoi j'ai créé un mapping pour le fichier traefik.toml, que je vais détailler juste après.&lt;br&gt;&lt;br&gt;
Dernière raison : Let's Encrypt génère des "challenges" pour les domaines qu'il a réussi à authentifier. C'est pourquoi il nous faut conserver le résultat de ces challenges en dehors de la portée éphémère d'un conteneur Docker.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuration de Traefik
&lt;/h2&gt;

&lt;p&gt;Pour gérer HTTPS une fois pour toutes avec Let's Encrypt, on va modifier légèrement la configuration de base de Traefik. Ça se traduit par la création d'un fichier traefik.toml :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# /apps/traefik/traefik.toml&lt;/span&gt;
&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;insecure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entryPoints&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entryPoints&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;":80"&lt;/span&gt;
    &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entryPoints&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entryPoints&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redirections&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entryPoints&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redirections&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entryPoint&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
          &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"websecure"&lt;/span&gt;
          &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"https"&lt;/span&gt;
          &lt;span class="n"&gt;permanent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entryPoints&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;websecure&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;":443"&lt;/span&gt;
      &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entryPoints&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;websecure&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;certResolver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"default"&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;exposedByDefault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"web"&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;certificatesResolvers&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;certificatesResolvers&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;certificatesResolvers&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acme&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"contact@superman.com"&lt;/span&gt;
      &lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"acme.json"&lt;/span&gt;
    &lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;certificatesResolvers&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acme&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tlsChallenge&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Attention&lt;/strong&gt;, ne recopiez pas cette configuration sans d'abord comprendre ces lignes ! Personnellement, je ne suis pas fan du format TOML mais le contenu reste plutôt simple à lire.&lt;br&gt;&lt;br&gt;
Dans ce setup tout le trafic HTTP est automatiquement redirigé vers HTTPS. Si vous avez encore des apps que vous ne pouvez pas simplement passer en HTTPS (apps legacy, url claquée en dur dans le programme, etc), vous pouvez faire sauter les lignes sous &lt;code&gt;[entryPoints.web.http]&lt;/code&gt;. Vous pouvez également commenter temporairement la ligne &lt;code&gt;permanent = true&lt;/code&gt; le temps de faire vos tests si vous n'êtes pas trop sûr.e de ce que vous faites !&lt;/p&gt;

&lt;p&gt;Traefik va gérer automatiquement pour nous la communication avec les serveurs de Let's encrypt et le renouvellement des certificats. C'est principalement les lignes sous &lt;code&gt;[certificatesResolvers]&lt;/code&gt; qui s'occupent de ce job.&lt;br&gt;&lt;br&gt;
La section &lt;code&gt;[providers]&lt;/code&gt; indique à Traefik d'aller écouter le socket Docker.&lt;br&gt;&lt;br&gt;
Allez, il nous faut encore créer le fichier acme.json avant de pouvoir admirer le résultat :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; /apps/traefik/acme.json
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /apps/traefik/acme.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le plus dur est fait ! Il ne nous reste plus qu'à builder et démarrer notre conteneur :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si tout s'est bien passé, rendez-vous sur votresupersite.fr:8080. Vous tombez alors sur l'interface d'admin de Traefik. L'infrastructure de base est mise en place. &lt;strong&gt;YAY.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  L'ajout des apps Docker dans Traefik
&lt;/h2&gt;

&lt;p&gt;Traefik utilise avec les &lt;strong&gt;labels&lt;/strong&gt; de Docker que l'on positionne dans le fichier docker-compose.yml de notre app pour lui indiquer comment gérer le trafic de cette instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Du concret
&lt;/h3&gt;

&lt;p&gt;Parce qu'un exemple vaut 1000 lignes d'explications, je vais prendre l'exemple de la configuration de ce blog (qui tourne avec &lt;a href="https://ghost.org/" rel="noopener noreferrer"&gt;Ghost 6&lt;/a&gt;) et qui tient en &lt;strong&gt;moins de 30 lignes :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /apps/blog.silarhi.fr/docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghost:6-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blog&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://blog.silarhi.fr&lt;/span&gt;
        &lt;span class="na"&gt;database__client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
        &lt;span class="na"&gt;database__connection__host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
        &lt;span class="na"&gt;database__connection__user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;database__connection__password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supermotdepasse&lt;/span&gt;
        &lt;span class="na"&gt;database__connection__database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blog&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;content:/var/lib/ghost/content&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.blog.rule=Host(`blog.silarhi.fr`)"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.blog.entrypoints=websecure"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les informations vraiment importantes ici sont celles des &lt;code&gt;labels&lt;/code&gt; (le reste étant standard).&lt;br&gt;&lt;br&gt;
Depuis Ghost 5, MySQL est obligatoire en production : les variables &lt;code&gt;database__*&lt;/code&gt; pointent vers l'instance MySQL mutualisée que l'on va créer juste après (le service &lt;code&gt;mysql&lt;/code&gt;, sur le même network &lt;code&gt;web&lt;/code&gt;).&lt;br&gt;&lt;br&gt;
Le premier label &lt;code&gt;traefik.enable=true&lt;/code&gt; indique à Traefik qu'il doit gérer ce conteneur. Cette ligne est à ajouter si vous avez laissé la configuration &lt;code&gt;exposedByDefault = false&lt;/code&gt; dans votre fichier TOML. Dans le cas contraire, vous n'avez pas à vous en occuper.&lt;br&gt;&lt;br&gt;
Le label &lt;code&gt;traefik.http.routers.blog.rule=Host('blog.silarhi.fr')&lt;/code&gt; permet de créer un routeur Traefik (que j'ai appelé &lt;code&gt;blog&lt;/code&gt;) qui redirige vers CE conteneur TOUT le trafic qu'il reçoit venant des &lt;strong&gt;milliards&lt;/strong&gt; de visiteurs quotidiens de &lt;a href="https://blog.silarhi.fr/" rel="noopener noreferrer"&gt;blog.silarhi.fr&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
Le dernier label &lt;code&gt;traefik.http.routers.blog.entrypoints=websecure&lt;/code&gt; indique que ce conteneur sera disponible sur le port HTTPS (étant donné que Traefik redirige déjà le traffic HTTP vers HTTPS).&lt;/p&gt;

&lt;p&gt;🤓 « &lt;em&gt;Et si j'ai une base de données ?&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;Facile ! Là encore tout se passe grâce à Docker. Vous pouvez choisir d'utiliser une instance de base de données par app ou bien de mutualiser l'instance pour toutes vos apps. Personnellement, j'ai fais le choix de mutualiser l'instance de MySQL étant donné que j'utilise exclusivement ce SGBD et la même version d'une app à l'autre.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /apps/database/docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supermotdepasse&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;data:/var/lib/mysql&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;

  &lt;span class="na"&gt;phpmyadmin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phpmyadmin/phpmyadmin&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phpmyadmin&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supermotdepasse&lt;/span&gt;
        &lt;span class="na"&gt;PMA_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
        &lt;span class="na"&gt;UPLOAD_LIMIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128M&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.phpmyadmin.rule=Host(`pma.monsupersite.fr`)"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.phpmyadmin.entrypoints=websecure"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/sessions&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En prime, je vous ai ajouté un conteneur phpMyAdmin (même si c'est &lt;strong&gt;vivement déconseillé&lt;/strong&gt; pour la production). Pour la partie MySQL, on utilise là encore &lt;a href="https://hub.docker.com/_/mysql" rel="noopener noreferrer"&gt;l'image officielle&lt;/a&gt; basée sur la version 8.4.&lt;br&gt;&lt;br&gt;
Il ne faut pas ajouter le label &lt;code&gt;traefik.enable=true&lt;/code&gt; dans la configuration MySQL ! Le but étant d'exclure ce conteneur de la gestion de Traefik. En effet, on ne veut pas que MySQL soit accessible depuis l'extérieur, on va donc le laisser tranquillement dans son network local "web".&lt;/p&gt;
&lt;h3&gt;
  
  
  Modifier les informations de connexion à la base de données
&lt;/h3&gt;

&lt;p&gt;La petite modification à faire dans le code de vos apps est de modifier les informations de connexion à la base. Vous devez remplacer en effet 127.0.0.1 par mysql (ou le nom que vous avez donné à votre service dans le fichier docker-compose.yml de MySQL). Eh oui, les conteneurs Docker sont isolés les uns des autres, par conséquence 127.0.0.1 fait référence au &lt;strong&gt;localhost du conteneur&lt;/strong&gt; et non plus à celui du host.&lt;/p&gt;
&lt;h2&gt;
  
  
  Aller plus loin avec Traefik
&lt;/h2&gt;

&lt;p&gt;Pour celles et ceux qui veulent pousser un peu l'utilisation de Traefik, on va voir ensemble quelques patterns communs que vous pourrez utiliser et reproduire.&lt;/p&gt;
&lt;h3&gt;
  
  
  Sécuriser le dashboard Traefik
&lt;/h3&gt;

&lt;p&gt;Avec la configuration utilisée plus haut, on a autorisé l'accès publique au dashboard de Traefik sur le port 8080. Voyons un peu comment améliorer tout ça !&lt;/p&gt;

&lt;p&gt;On va d'abord commencer par enlever le mode &lt;code&gt;insecure&lt;/code&gt; et modifier la configuration :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# /apps/traefik/traefik.toml
[api]
  dashboard = true
&lt;span class="gd"&gt;- insecure = true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# /apps/traefik/docker-compose.yml 
 services:
   reverse-proxy:
     image: traefik:v3
     container_name: traefik
     ports:
       - "80:80"
       - "443:443"
&lt;span class="gd"&gt;-      - "8080:8080"
&lt;/span&gt;     volumes:
       - /var/run/docker.sock:/var/run/docker.sock
       - $PWD/traefik.toml:/etc/traefik/traefik.toml
       - $PWD/acme.json:/acme.json
     restart: always
&lt;span class="gi"&gt;+    labels:
+      - "traefik.enable=true"
+      - "traefik.http.routers.api.rule=Host(`monsupersite.fr`) &amp;amp;&amp;amp; (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
+      - "traefik.http.routers.api.service=api@internal"
+      - "traefik.http.routers.api.entrypoints=websecure"
&lt;/span&gt;     networks:
       - web
 networks:
   web:
     external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relancez le conteneur Traefik (&lt;code&gt;docker compose up -d&lt;/code&gt;) et vous devriez pouvoir accéder à &lt;a href="https://monsupersite.fr/dashboard/" rel="noopener noreferrer"&gt;https://monsupersite.fr/dashboard/&lt;/a&gt;. On progresse, mais le dashboard est toujours accessible publiquement, ce qui n'est pas terrible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Les middlewares à la rescousse
&lt;/h3&gt;

&lt;p&gt;On va sécuriser l'accès au dashboard en utilisant un middleware. Vous avez déjà du entendre ce nom, c'est un pattern qui revient souvent en programmation. Dans notre cas on va placer une logique qui s'execute entre le routeur du dashboard Traefik (&lt;code&gt;api&lt;/code&gt;) et le service associé (&lt;code&gt;api@internal&lt;/code&gt;).&lt;br&gt;&lt;br&gt;
Il y a plusieurs manières d'ajouter des middlewares. Pour ma part, je suis partisan de la configuration via les labels de Docker, directement dans le docker-compose.yml de Traefik.&lt;/p&gt;
&lt;h3&gt;
  
  
  Forcer l'authentification avec &lt;code&gt;auth&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# /apps/traefik/docker-compose.yml
&lt;span class="p"&gt;services:
&lt;/span&gt;  reverse-proxy:
    image: traefik:v3
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/traefik.toml:/etc/traefik/traefik.toml
      - $PWD/acme.json:/acme.json
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`monsupersite.fr`) &amp;amp;&amp;amp; (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.api.service=api@internal"
      - "traefik.http.routers.api.entrypoints=websecure"
&lt;span class="gi"&gt;+     - "traefik.http.routers.api.middlewares=auth"
+     - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$6ydic0em$ZAhWtxDpkgZhpBOGT7ibu."
&lt;/span&gt;    networks:
      - web
&lt;span class="p"&gt;networks:
&lt;/span&gt;  web:
    external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Avec cette configuration, vous avez protégé l'accès au dashboard avec l'équivalent d'un fichier htpasswd. Veillez à bien doubler les &lt;code&gt;$&lt;/code&gt; dans la chaîne de caractère du mot de passe !&lt;/p&gt;
&lt;h3&gt;
  
  
  Rediriger www vers non-www avec &lt;code&gt;redirectregex&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Vous l'avez compris, on va à nouveau ajouter un middleware dans le docker-compose.yml de Traefik !&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# /apps/traefik/docker-compose.yml
&lt;span class="p"&gt;services:
&lt;/span&gt;  reverse-proxy:
    image: traefik:v3
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/traefik.toml:/etc/traefik/traefik.toml
      - $PWD/acme.json:/acme.json
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`silarhi.fr`) &amp;amp;&amp;amp; (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.api.service=api@internal"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$6ydic0em$ZAhWtxDpkgZhpBOGT7ibu."
&lt;span class="gi"&gt;+     - "traefik.http.middlewares.strip-www.redirectregex.regex=^https?://(www\\.)(.+)"
+     - "traefik.http.middlewares.strip-www.redirectregex.replacement=https://$${2}"
+     - "traefik.http.middlewares.strip-www.redirectregex.permanent=true"
&lt;/span&gt;    networks:
      - web
&lt;span class="p"&gt;networks:
&lt;/span&gt;  web:
    external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On va maintenant appeler le middleware dans notre site (j'ai repris l'exemple du blog) et ajouter la gestion du www (depuis Traefik v3, une règle &lt;code&gt;Host&lt;/code&gt; ne prend plus qu'un seul domaine : on en combine deux avec &lt;code&gt;||&lt;/code&gt;) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# /apps/blog.silarhi.fr/docker-compose.yml
&lt;span class="p"&gt;services:
&lt;/span&gt;  app:
    image: ghost:6-alpine
    container_name: blog
    environment:
        url: https://blog.silarhi.fr
        database__client: mysql
        database__connection__host: mysql
        database__connection__user: root
        database__connection__password: supermotdepasse
        database__connection__database: blog
    volumes:
        - content:/var/lib/ghost/content
    networks:
        - web
    labels:
         - "traefik.enable=true"
&lt;span class="gd"&gt;-        - "traefik.http.routers.blog.rule=Host(`blog.silarhi.fr`)"
&lt;/span&gt;&lt;span class="gi"&gt;+        - "traefik.http.routers.blog.rule=Host(`blog.silarhi.fr`) || Host(`www.blog.silarhi.fr`)"
&lt;/span&gt;         - "traefik.http.routers.blog.entrypoints=websecure"
&lt;span class="gi"&gt;+        - "traefik.http.routers.blog.middlewares=strip-www"
&lt;/span&gt;    restart: always
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;volumes:
&lt;/span&gt;  content:
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;networks:
&lt;/span&gt;  web:
    external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gérer des sous-repertoires avec &lt;code&gt;stripprefix&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Tout à l'heure, je vous ai montré comment exposer un conteneur phpMyAdmin avec un sous-domaine particulier. On va voir ici comment se passer de la création d'un sous-domaine en le placant plutôt dans un sous repertoire (en plus de le sécuriser avec une authentification !).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;# /apps/database/docker-compose.yml
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;services:
&lt;/span&gt;  mysql:
    image: mysql:8
    container_name: mysql
    environment:
        MYSQL_ROOT_PASSWORD: supermotdepasse
    volumes:
        - data:/var/lib/mysql
    networks:
        - web
    restart: always
&lt;span class="err"&gt;
&lt;/span&gt;  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: phpmyadmin
    environment:
        MYSQL_ROOT_PASSWORD: supermotdepasse
        PMA_HOST: mysql
&lt;span class="gi"&gt;+       PMA_ABSOLUTE_URI: "https://monsupersite.fr/_phpmyadmin/"
&lt;/span&gt;        UPLOAD_LIMIT: 128M
     labels:
         - "traefik.enable=true"
&lt;span class="gd"&gt;-        - "traefik.http.routers.phpmyadmin.rule=Host(`pma.monsupersite.fr`)"
&lt;/span&gt;&lt;span class="gi"&gt;+        - "traefik.http.routers.phpmyadmin.rule=Host(`monsupersite.fr`) &amp;amp;&amp;amp; PathPrefix(`/_phpmyadmin/`)"
&lt;/span&gt;         - "traefik.http.routers.phpmyadmin.entrypoints=websecure"
&lt;span class="gi"&gt;+        - "traefik.http.routers.phpmyadmin.middlewares=phpmyadmin,auth"
+        - "traefik.http.middlewares.phpmyadmin.stripprefix.prefixes=/_phpmyadmin"
&lt;/span&gt;    restart: always
    volumes:
        - /sessions
    networks:
        - web
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;volumes:
&lt;/span&gt;  data:
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;networks:
&lt;/span&gt;  web:
    external: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vous êtes maintenant un pro de Docker et Traefik ! N'hésitez pas à me faire part de vos retours / succès / échecs dans les commentaires.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ressources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/labs.silarhi.fr" rel="noopener noreferrer"&gt;D'autres exemples concrets de configuration Traefik&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.silarhi.fr/image-docker-php-apache-parfaite/" rel="noopener noreferrer"&gt;Vous êtes d'attaque ? Créez votre image PHP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.traefik.io/middlewares/overview/" rel="noopener noreferrer"&gt;La documentation des middlewares Traefik&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>code</category>
      <category>docker</category>
    </item>
    <item>
      <title>Le déploiement continu en production pour Symfony 8 avec Docker et CircleCI</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:06:23 +0000</pubDate>
      <link>https://dev.to/silarhi/le-deploiement-continu-en-production-pour-symfony-8-avec-docker-et-circleci-32ok</link>
      <guid>https://dev.to/silarhi/le-deploiement-continu-en-production-pour-symfony-8-avec-docker-et-circleci-32ok</guid>
      <description>&lt;p&gt;Vous avez une super app Symfony qui tourne (pas encore ?) avec Docker et vous voulez déclencher automatiquement sa mise en production lorsque vous faites un push dans une branche de votre repo. Respirez un grand coup, détendez-vous, vous êtes au bon endroit.&lt;/p&gt;

&lt;p&gt;Avant de commencer, vérifions les bases. Simple, basique. Basique, simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le déploiement continu, c'est quoi ?
&lt;/h2&gt;

&lt;p&gt;Accueillez à nouveau le mot CD dans votre vocabulaire. Pas pour désigner le CD du groupe de rock de votre enfance, non, les choses ont bien changé. On parle ici de Continuous Deployment, l'automatisation du déploiement de vos apps sur vos serveurs de production en appuyant sur un gros bouton rouge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjd9b5iip9rnomdcbsdy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjd9b5iip9rnomdcbsdy.gif" alt="Bouton déploiement continu" width="428" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Elle est pas belle la vie ?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Le cas d'étude
&lt;/h2&gt;

&lt;p&gt;Sans plus attendre, on va s'intéresser à la stack technique suivante : Docker, Symfony 8, PHP 8.5, Tabler (Bootstrap 5), React et TypeScript. Pour pimenter un peu le tout, j'ai utilisé Vite avec Symfony Reprise pour me rapprocher d'un cas concret. J'ai donc des dépendances JS et SCSS à installer avec pnpm, à compiler, à minifier, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9tt3qoggb1u61b7w43f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9tt3qoggb1u61b7w43f.png" alt="Démo labs.sainthillier.fr" width="800" height="819"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La magnifique démo de cet article : 👉&lt;a href="https://labs.silarhi.fr/" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;L'idée est qu'à chaque push sur une branche git (&lt;em&gt;main&lt;/em&gt; en l'occurrence), le serveur de CD va build la nouvelle version de l'image Docker qui correspond à notre app, le déployer sur le &lt;a href="https://hub.docker.com/r/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;Hub Docker&lt;/a&gt; et appeler un &lt;a href="https://github.com/silarhi/labs.silarhi.fr/blob/main/ci/deploy.sh" rel="noopener noreferrer"&gt;script de déploiement&lt;/a&gt; sur notre serveur via SSH. En gros, le workflow ressemble à ça :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7rufvpf5ijcusmxrhaf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7rufvpf5ijcusmxrhaf.png" alt="Diagramme de CI/CD" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Crédits : OpenClassrooms x Paint&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sans plus attendre, voyons à quoi ressemble le Dockerfile de production et/ou staging de notre app :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;#syntax=docker/dockerfile:1.28-labs&lt;/span&gt;
&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;

&lt;span class="c"&gt;# Versions&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;silarhi/php-apache:8.5-frankenphp-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;php_upstream&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:24-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_upstream&lt;/span&gt;

&lt;span class="c"&gt;# Node base with pnpm (it switches itself to the version pinned in package.json "packageManager")&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_upstream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_base&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; pnpm@12

&lt;span class="c"&gt;# Base with extensions&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;php_upstream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;php_base&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;install-php-extensions exif gd imagick

&lt;span class="c"&gt;# Composer deps (cached)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;php_base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;php_builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link composer.json composer.lock symfony.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.composer &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-interaction&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--no-scripts&lt;/span&gt; &lt;span class="nt"&gt;--prefer-dist&lt;/span&gt;

&lt;span class="c"&gt;# Node deps (cached separately from build)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_deps&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=php_builder --link /app/vendor ./vendor&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link package.json pnpm-lock.yaml pnpm-workspace.yaml ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/pnpm/store &lt;span class="se"&gt;\
&lt;/span&gt;    pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt; &lt;span class="nt"&gt;--store-dir&lt;/span&gt; /pnpm/store

&lt;span class="c"&gt;# Asset build&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=node_deps --link /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=node_deps --link /app/vendor ./vendor&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link package.json pnpm-lock.yaml pnpm-workspace.yaml vite.config.ts ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link assets ./assets&lt;/span&gt;
&lt;span class="c"&gt;# PurgeCSS keeps the selectors used in the Twig templates&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link templates ./templates&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; public &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm build

&lt;span class="c"&gt;# Final&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; php_base&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP_VERSION=dev&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; GIT_COMMIT=master&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; APP_VERSION="${APP_VERSION}"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GIT_COMMIT="${GIT_COMMIT}"&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=php_builder --link /app/vendor ./vendor&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=node_builder --link /app/public/build /app/public/build&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link --exclude=assets --exclude=docker . .&lt;/span&gt;
&lt;span class="c"&gt;# ux_icon() reads the SVG files at runtime (and on cache warmup)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link assets/icons ./assets/icons&lt;/span&gt;

&lt;span class="c"&gt;# Config&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --link docker/php.ini $PHP_INI_DIR/conf.d/app.ini&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; var var/storage &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    composer dump-autoload &lt;span class="nt"&gt;--optimize&lt;/span&gt; &lt;span class="nt"&gt;--classmap-authoritative&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--no-interaction&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod bin/console cache:clear &lt;span class="nt"&gt;--no-warmup&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod bin/console cache:warmup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod bin/console ux:icons:warm-cache &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="c"&gt;# We don't use DotEnv component as docker-compose will provide real environment variables&lt;/span&gt;
    echo "&amp;lt;?php return [];" &amp;gt; .env.local.php &amp;amp;&amp;amp; \
    chown -R www-data:www-data var &amp;amp;&amp;amp; \
    rm -rf /root/.cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avec ce Dockerfile multi-stage, on a un container tout chaud prêt pour la production : les dépendances Composer et pnpm sont installées dans des stages dédiés (et mises en cache par BuildKit), les fichiers JS et CSS sont compilés et minifiés par Vite, et le cache Symfony (icônes comprises) est déjà généré. Le tout tourne sur FrankenPHP, prêt à être délivré à vos millions de visiteurs par votre nouveau Raspberry Pi flambant neuf.&lt;/p&gt;

&lt;p&gt;L'objectif est maintenant de réussir à build cette image automatiquement lors d'un nouveau push dans git, et à déployer l'app dans la foulée.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dans le vif du sujet de la CD
&lt;/h2&gt;

&lt;p&gt;Il existe pléthore d'outils pour arriver à nos fins :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.jenkins.io/" rel="noopener noreferrer"&gt;Jenkins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.travis-ci.com/" rel="noopener noreferrer"&gt;Travis CI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jetbrains.com/teamcity/" rel="noopener noreferrer"&gt;TeamCity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://circleci.com/" rel="noopener noreferrer"&gt;CircleCI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/" rel="noopener noreferrer"&gt;GitLab CI/CD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aujourd'hui, on va aborder CircleCI. N'hésitez pas à laisser un commentaire si vous voulez un tuto avec un autre outil de CI !&lt;/p&gt;

&lt;h3&gt;
  
  
  La CD avec CircleCI
&lt;/h3&gt;

&lt;p&gt;CircleCI a l'avantage d'être gratuit dans son offre de base. On peut faire pas mal de choses avec cet outil, mais on va se concentrer sur les 4 opérations clés de notre flow :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redescendre du code dans une VM éphémère&lt;/li&gt;
&lt;li&gt;Build l'image Docker&lt;/li&gt;
&lt;li&gt;Push l'image sur &lt;a href="https://hub.docker.com/r/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;le Docker Hub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Appeler un script shell de déploiement sur le serveur de production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pour ça, on va créer un fichier &lt;code&gt;.circleci/config.yml&lt;/code&gt; à la racine du projet :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .circleci/config.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.1&lt;/span&gt;

&lt;span class="na"&gt;orbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;circleci/docker@4.0.1&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cimg/base:2026.10&lt;/span&gt;
              &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;IMAGE_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;silarhi/symfony-docker-ci&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt; &lt;span class="c1"&gt;# Étape 1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setup_remote_docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;docker_layer_caching&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

            &lt;span class="c1"&gt;# Étapes 2 &amp;amp; 3&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;docker/check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;docker_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DOCKER_PWD&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;docker/build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$IMAGE_NAME&lt;/span&gt;
                  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.$CIRCLE_BUILD_NUM,latest&lt;/span&gt;
                  &lt;span class="c1"&gt;# --pull : sans lui, docker_layer_caching réutiliserait une vieille image silarhi/php-apache&lt;/span&gt;
                  &lt;span class="na"&gt;extra_build_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--pull --build-arg APP_VERSION=1.$CIRCLE_BUILD_NUM --build-arg GIT_COMMIT=${CIRCLE_SHA1:0:7}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;docker/push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$IMAGE_NAME&lt;/span&gt;
                  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.$CIRCLE_BUILD_NUM&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                      &lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;&amp;lt;&amp;lt; pipeline.git.branch &amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;docker/push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                            &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$IMAGE_NAME&lt;/span&gt;
                            &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
                            &lt;span class="na"&gt;step_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Docker push latest&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Étape 4&lt;/span&gt;
        &lt;span class="na"&gt;machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-2404:current&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;add_ssh_keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;fingerprints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;de:c3:47:8e:28:31:55:01:9d:f7:08:f9:df:8e:79:e0'&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;production'&lt;/span&gt;
                  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                      &lt;span class="s"&gt;ssh ${PRODUCTION_SERVER_USER}@${PRODUCTION_SERVER_IP} "cd ${PRODUCTION_SERVER_PATH} &amp;amp;&amp;amp; ./deploy.sh"&lt;/span&gt;

&lt;span class="c1"&gt;# On exécute ces étapes lors d'un commit sur la branche main uniquement&lt;/span&gt;
&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
                  &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                      &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                          &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ne fuyez pas en voyant ce fichier, on va le détailler un peu :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;version: 2.1&lt;/code&gt; permet d'utiliser les &lt;a href="https://circleci.com/orbs/" rel="noopener noreferrer"&gt;orbs&lt;/a&gt;, des paquets de configuration réutilisables. Ici, l'orb officiel &lt;a href="https://circleci.com/developer/orbs/orb/circleci/docker" rel="noopener noreferrer"&gt;&lt;code&gt;circleci/docker&lt;/code&gt;&lt;/a&gt; fournit des commandes toutes prêtes pour build et push nos images.&lt;/li&gt;
&lt;li&gt;À chaque push, un container Docker est créé par CircleCI. On spécifie l'image qu'on veut utiliser avec la directive &lt;code&gt;image: cimg/base:2026.10&lt;/code&gt;. CircleCI nous laisse le choix d'utiliser des images déjà toutes prêtes conçues spécialement pour la CI/CD, autant les utiliser.&lt;/li&gt;
&lt;li&gt;Étape 1 : &lt;code&gt;checkout&lt;/code&gt;. Il faut bien sûr redescendre le code de GitHub dans le container si on veut l'utiliser après. &lt;code&gt;setup_remote_docker&lt;/code&gt; nous donne ensuite un moteur Docker distant, avec le cache des layers activé (&lt;code&gt;docker_layer_caching&lt;/code&gt;) pour accélérer les builds suivants.&lt;/li&gt;
&lt;li&gt;Étapes 2 &amp;amp; 3 : &lt;code&gt;docker/check&lt;/code&gt; se connecte au Docker Hub, &lt;code&gt;docker/build&lt;/code&gt; construit l'image et &lt;code&gt;docker/push&lt;/code&gt; la pousse sur le registre. Le tag &lt;code&gt;1.&amp;lt;numéro de build&amp;gt;&lt;/code&gt; est toujours poussé, le tag &lt;code&gt;latest&lt;/code&gt; uniquement sur la branche &lt;code&gt;main&lt;/code&gt; grâce à la condition &lt;code&gt;when&lt;/code&gt;. Il y a cependant 2 choses à remarquer : 

&lt;ul&gt;
&lt;li&gt;L'utilisation de &lt;code&gt;extra_build_args&lt;/code&gt; pour passer des options à &lt;code&gt;docker build&lt;/code&gt;. &lt;code&gt;--pull&lt;/code&gt; force le téléchargement de la dernière image de base : sans lui, le cache des layers de CircleCI garderait indéfiniment une vieille version de &lt;code&gt;silarhi/php-apache&lt;/code&gt;. Les &lt;code&gt;--build-arg&lt;/code&gt;, eux, sont complètement optionnels : c'est juste pour vous montrer qu'on peut utiliser le numéro de commit dans notre app Symfony via une variable d'environnement. Ça peut être utile par exemple si vous utilisez &lt;a href="https://docs.sentry.io/workflow/releases/?platform=php" rel="noopener noreferrer"&gt;les releases de Sentry&lt;/a&gt; pour tracker quels commits sont particulièrement générateurs de problèmes.&lt;/li&gt;
&lt;li&gt;L'utilisation de variables non déclarées telles que &lt;code&gt;PRODUCTION_SERVER_IP&lt;/code&gt; ou &lt;code&gt;DOCKER_PWD&lt;/code&gt;. En fait, pour des raisons de sécurité, CircleCI permet de définir des variables d'environnement secrètes qui ne seront pas affichées dans les logs de CircleCI. Vous pouvez les définir dans l'interface de CircleCI :&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0j0o7nq659ykedp2sf1p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0j0o7nq659ykedp2sf1p.png" alt="Les variables d'environnement dans CircleCI" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Les variables d'environnement dans CircleCI&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Étape 4 : 

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;fingerprints&lt;/code&gt; permet d'utiliser la clé privée de votre serveur de production dans le container éphémère pour pouvoir se connecter par SSH sans utiliser de password. &lt;strong&gt;Veillez à bien sécuriser vos accès à CircleCI, car celui-ci devient désormais un élément critique de votre infra. Celui qui a accès à votre CircleCI peut avoir accès à votre serveur de production.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;On exécute le script de déploiement sur le serveur de prod via la commande &lt;code&gt;cd ${PRODUCTION_SERVER_PATH} &amp;amp;&amp;amp; ./deploy.sh&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Le script de déploiement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;#Download new image version&lt;/span&gt;
docker compose pull

&lt;span class="c"&gt;#Set maintenance mode to perform critical operations (database, upgrades, ...)&lt;/span&gt;
&lt;span class="nv"&gt;APP_MAINTENANCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="c"&gt;#i.e: docker compose exec -T app bin/console doctrine:migration:migrate -n&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;10&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;#for demo purpose&lt;/span&gt;

&lt;span class="c"&gt;#back to prod&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  L'hébergement sur la production
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /apps/labs.silarhi.fr/ci/docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;silarhi/symfony-docker-ci:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lab_ci_app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;storage:/app/var/storage&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_MAINTENANCE&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app.env&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.labs-ci.rule=Host(`labs.silarhi.fr`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.labs-ci.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.labs-ci.tls=true"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🤓 « &lt;em&gt;C'est quoi ces labels bizarres ?!&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;J'utilise Traefik pour gérer les instances de Docker sur la production. Si ça vous intéresse, n'hésitez pas à jeter un oeil à &lt;a href="https://blog.silarhi.fr/docker-compose-traefik-https/" rel="noopener noreferrer"&gt;l'article que j'ai écrit à ce sujet&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  La gestion de la mise à jour
&lt;/h3&gt;

&lt;p&gt;Durant le déploiement, on définit une variable d'environnement &lt;code&gt;APP_MAINTENANCE&lt;/code&gt;. Un petit tweak sur le contrôleur frontal de Symfony (&lt;code&gt;public/index.php&lt;/code&gt;) permet de renvoyer une page de maintenance (avec un code HTTP 503) durant la mise en prod, et le tour est joué.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// public/index.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Kernel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="nf"&gt;\dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload_runtime.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'APP_MAINTENANCE'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../maintenance.html'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP_SERVICE_UNAVAILABLE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'APP_ENV'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'APP_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voilà ! Votre app est désormais fin prête à être déployée automatiquement pour votre plus grand bonheur. N'hésitez pas à laisser un commentaire si vous utilisez un autre workflow pour vos déploiements !&lt;/p&gt;

&lt;h3&gt;
  
  
  Pour aller plus loin
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/" rel="noopener noreferrer"&gt;La démo du tuto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;Le code source utilisé sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/labs.silarhi.fr/blob/main/ci/deploy.sh" rel="noopener noreferrer"&gt;Le script de déploiement sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/r/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;L'image Docker générée par la CI sur le Hub Docker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>php</category>
      <category>code</category>
    </item>
    <item>
      <title>PicassoBundle : le composant image qui manquait à Symfony</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Fri, 02 Oct 2026 22:11:38 +0000</pubDate>
      <link>https://dev.to/silarhi/picassobundle-le-composant-image-qui-manquait-a-symfony-fbl</link>
      <guid>https://dev.to/silarhi/picassobundle-le-composant-image-qui-manquait-a-symfony-fbl</guid>
      <description>&lt;p&gt;Il y a quelques années, je vous parlais &lt;a href="https://blog.silarhi.fr/symfony-miniature-image-glide-php/" rel="noopener noreferrer"&gt;de la gestion des miniatures avec Glide&lt;/a&gt;. L'article tient toujours la route, mais le Web a bougé depuis. AVIF est arrivé. Les Core Web Vitals comptent pour le référencement, et le LCP vous tape sur les doigts dès qu'une image de 3 Mo traîne en haut de page. Côté React, Next.js a popularisé son composant &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt;, qui fait à peu près tout le boulot à votre place.&lt;/p&gt;

&lt;p&gt;Pendant ce temps, côté Symfony, on écrit toujours ce genre de joyeuseté à la main quand on veut bien faire les choses :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt;
        &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero-640.avif 640w, /images/hero-1080.avif 1080w, /images/hero-1920.avif 1920w"&lt;/span&gt;
        &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt;
        &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero-640.webp 640w, /images/hero-1080.webp 1080w, /images/hero-1920.webp 1920w"&lt;/span&gt;
        &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
        &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero-1080.jpg"&lt;/span&gt;
        &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"/images/hero-640.jpg 640w, /images/hero-1080.jpg 1080w, /images/hero-1920.jpg 1920w"&lt;/span&gt;
        &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
        &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1920"&lt;/span&gt;
        &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1080"&lt;/span&gt;
        &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
        &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et encore, là on a fait l'effort ! Dans la vraie vie de la vraie prod avec de vrais humains dessus, on ne fait (presque) jamais tout ça. Un &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tout seul, pas de dimensions (bonjour le CLS), pas d'AVIF ni de WebP parce que "c'est déjà bien d'avoir un JPG". Et un Lighthouse à 60 qui vous rappelle gentiment que vous pourriez faire mieux. Le fameux "je le ferai plus tard", sauf que plus tard n'arrive jamais : il y a une feature à livrer la semaine prochaine.&lt;/p&gt;

&lt;p&gt;Bref, à force de recopier ce bloc de projet en projet, j'ai fini par en faire un bundle. Open source, évidemment : je vous présente &lt;a href="https://github.com/silarhi/picasso-bundle" rel="noopener noreferrer"&gt;&lt;strong&gt;PicassoBundle&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  En une ligne de Twig
&lt;/h2&gt;

&lt;p&gt;Voilà ce que vous écrivez dans votre template :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;twig:Picasso:Image&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt;
    &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1920"&lt;/span&gt;
    &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1080"&lt;/span&gt;
    &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà ce que vous obtenez : &lt;strong&gt;le gros bloc HTML du début de l'article&lt;/strong&gt;, sans en écrire une ligne. Avec en prime les bonnes valeurs de &lt;code&gt;loading&lt;/code&gt;, les dimensions détectées depuis le fichier source si vous ne les passez pas, et des URLs signées pour éviter qu'un petit malin vous spamme de transformations en 9000x9000 pixels.&lt;/p&gt;

&lt;p&gt;L'idée est piquée sans complexe à &lt;code&gt;next/image&lt;/code&gt; côté React : un seul composant, et c'est lui qui se tape le boulot.&lt;/p&gt;

&lt;p&gt;La magnifique démo de cet article : 👉 &lt;a href="https://labs.silarhi.fr/picasso" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/picasso&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🤓 « &lt;em&gt;Encore un bundle image ?!&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;Bien vu. Mais regardez ce qui existe : &lt;a href="https://glide.thephpleague.com/" rel="noopener noreferrer"&gt;Glide&lt;/a&gt; transforme les images et s'arrête là. &lt;a href="https://github.com/liip/LiipImagineBundle" rel="noopener noreferrer"&gt;LiipImagineBundle&lt;/a&gt; est puissant, mais il ne génère pas le HTML à votre place. Et on a tous un bundle maison qui traîne dans un placard. Il manquait la brique du dessus : celle qui va de l'&lt;strong&gt;image source&lt;/strong&gt; jusqu'au &lt;strong&gt;HTML final&lt;/strong&gt;, en passant par les formats modernes et les placeholders.&lt;/p&gt;

&lt;p&gt;En résumé, avant / après :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sans PicassoBundle&lt;/th&gt;
&lt;th&gt;Avec PicassoBundle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Formats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Les &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; AVIF/WebP/JPEG à la main&lt;/td&gt;
&lt;td&gt;Automatique depuis la config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Srcset responsive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fabriqué à la main par breakpoint&lt;/td&gt;
&lt;td&gt;Généré depuis l'attribut &lt;code&gt;sizes&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Placeholders flous&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DIY ou on zappe&lt;/td&gt;
&lt;td&gt;Intégrés (LQIP, BlurHash, ou maison)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;En dur ou oubliées&lt;/td&gt;
&lt;td&gt;Détectées depuis l'image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loading&lt;/code&gt; et &lt;code&gt;fetchpriority&lt;/code&gt; à la main&lt;/td&gt;
&lt;td&gt;Un seul prop &lt;code&gt;priority&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sources d'images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Filesystem uniquement&lt;/td&gt;
&lt;td&gt;Filesystem, Flysystem, Vich, URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;À construire soi-même&lt;/td&gt;
&lt;td&gt;Imgix intégré, ou votre CDN maison&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  C'est parti
&lt;/h2&gt;

&lt;p&gt;Rien d'exotique, c'est un bundle Symfony classique :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require silarhi/picasso-bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il vous faut ensuite &lt;strong&gt;au moins un transformer&lt;/strong&gt;, c'est-à-dire ce qui va générer les variantes de vos images. Deux sont fournis :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Option A : Glide (transformation locale avec GD ou Imagick)&lt;/span&gt;
composer require league/glide

&lt;span class="c"&gt;# Option B : Imgix (transformation par le CDN)&lt;/span&gt;
&lt;span class="c"&gt;# Rien à installer, juste l'URL de base à configurer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si vous hébergez tout vous-même, Glide est le choix le plus simple. Si vous avez déjà un compte Imgix, branchez-le et profitez de leur cache. Cloudinary, ImageKit ou votre CDN maison ? Rien ne vous en empêche, il suffit d'écrire votre propre transformer.&lt;/p&gt;

&lt;p&gt;La configuration minimale, avec un loader qui lit les images dans &lt;code&gt;public/uploads&lt;/code&gt; et Glide qui signe les URLs :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/packages/picasso.yaml&lt;/span&gt;
&lt;span class="na"&gt;picasso&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;loaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%kernel.project_dir%/public/uploads'&lt;/span&gt;
    &lt;span class="na"&gt;transformers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;glide&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;sign_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%env(PICASSO_SIGN_KEY)%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avec un seul loader et un seul transformer, pas besoin de &lt;code&gt;default_loader&lt;/code&gt; ni de &lt;code&gt;default_transformer&lt;/code&gt; : le bundle les devine tout seul.&lt;/p&gt;

&lt;p&gt;Pour que Glide serve les images transformées, il faut aussi importer ses routes :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes/picasso.yaml&lt;/span&gt;
&lt;span class="na"&gt;picasso&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@PicassoBundle/config/routes.php'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le plus dur est fait ! Le premier exemple vu plus haut génère maintenant :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;un &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; avec une source AVIF et une source WebP&lt;/li&gt;
&lt;li&gt;un &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; de secours en JPEG, avec son srcset calculé depuis les breakpoints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loading="lazy"&lt;/code&gt;, sauf si vous lui passez &lt;code&gt;priority&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;les dimensions, détectées depuis le fichier si vous ne les fournissez pas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le tout sans écrire une ligne de HTML. &lt;strong&gt;TOP.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Les détails qui m'ont donné du fil à retordre
&lt;/h2&gt;

&lt;p&gt;Sur ce genre de composant, l'essentiel du temps part dans les détails. En voici quelques-uns.&lt;/p&gt;

&lt;h3&gt;
  
  
  La détection des dimensions
&lt;/h3&gt;

&lt;p&gt;Les attributs &lt;code&gt;width&lt;/code&gt; et &lt;code&gt;height&lt;/code&gt; d'un &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; ne servent pas à dimensionner l'image (ça, c'est le rôle du CSS). Ils disent au navigateur &lt;strong&gt;quelle forme a l'image&lt;/strong&gt; avant qu'elle soit chargée, pour qu'il lui réserve la bonne place. Sans eux, la page "saute" quand l'image arrive : c'est le CLS (&lt;em&gt;Cumulative Layout Shift&lt;/em&gt;), l'un des trois Core Web Vitals.&lt;/p&gt;

&lt;p&gt;Le problème, c'est que personne ne renseigne les dimensions à la main sur chaque image. Et le ticket "corriger le CLS" traîne dans le backlog depuis des mois.&lt;/p&gt;

&lt;p&gt;PicassoBundle peut lire les dimensions directement depuis l'image, avec l'option &lt;code&gt;resolve_metadata&lt;/code&gt;. Elle est activée par défaut pour le loader filesystem (lire un fichier local ne coûte rien), mais pas pour les loaders distants : inutile de taper le réseau à chaque génération de page. Vous pouvez la forcer au cas par cas, ou fournir les dimensions en dur si vous les connaissez déjà :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="c"&gt;{# Hauteur calculée depuis le ratio de l'image source #}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;twig:Picasso:Image&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"800"&lt;/span&gt; &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Photo"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;{# Dimensions source fournies en dur, l'image n'est pas lue #}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;twig:Picasso:Image&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"photo.jpg"&lt;/span&gt;
    &lt;span class="na"&gt;:sourceWidth=&lt;/span&gt;&lt;span class="s"&gt;"4000"&lt;/span&gt;
    &lt;span class="na"&gt;:sourceHeight=&lt;/span&gt;&lt;span class="s"&gt;"3000"&lt;/span&gt;
    &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"800"&lt;/span&gt;
    &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt;
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Photo"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Petite subtilité : &lt;code&gt;width&lt;/code&gt; et &lt;code&gt;height&lt;/code&gt; ne sont rendus que si &lt;strong&gt;les deux&lt;/strong&gt; sont connus. Si vous n'en fournissez qu'un et que le bundle ne trouve pas l'autre, aucun n'est posé sur le &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. Une mauvaise dimension, c'est un CLS garanti. Autant ne rien mettre.&lt;/p&gt;

&lt;h3&gt;
  
  
  Les images prioritaires et le LCP
&lt;/h3&gt;

&lt;p&gt;Pour une image en haut de page, typiquement une bannière, il faut dire au navigateur "celle-là, charge-la tout de suite". Sinon elle attend gentiment son tour derrière les vingt autres images de la page, et votre LCP (&lt;em&gt;Largest Contentful Paint&lt;/em&gt;) fait pleurer Google.&lt;/p&gt;

&lt;p&gt;Ici, ça tient en un prop :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;twig:Picasso:Image&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero-banner.jpg"&lt;/span&gt;
    &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1920"&lt;/span&gt;
    &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1080"&lt;/span&gt;
    &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"100vw"&lt;/span&gt;
    &lt;span class="na"&gt;:priority=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero banner"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;priority&lt;/code&gt; pose &lt;code&gt;loading="eager"&lt;/code&gt; et &lt;code&gt;fetchpriority="high"&lt;/code&gt;, et &lt;strong&gt;désactive le placeholder&lt;/strong&gt; sur cette image. Une image prioritaire doit s'afficher le plus vite possible : afficher une version floue pendant une demi-seconde, c'est exactement l'inverse de ce qu'on veut.&lt;/p&gt;

&lt;h3&gt;
  
  
  Les placeholders
&lt;/h3&gt;

&lt;p&gt;Les placeholders, c'est le chargement progressif des connexions 56k, version 2026. Pendant que l'image se charge, on affiche une version minuscule et floutée qui donne une idée de ce qui arrive. Medium et Unsplash le font, et la page paraît plus rapide même quand les Core Web Vitals ne bougent pas.&lt;/p&gt;

&lt;p&gt;Ils ne sont pas activés par défaut. Le plus simple est le placeholder "transformer" (aussi appelé LQIP, pour &lt;em&gt;Low Quality Image Placeholder&lt;/em&gt;) : une version en 10x10 pixels de l'image, floutée, générée par votre transformer. Aucune dépendance en plus :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/packages/picasso.yaml&lt;/span&gt;
&lt;span class="na"&gt;picasso&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;default_placeholder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blur&lt;/span&gt;
    &lt;span class="na"&gt;placeholders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;blur&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;transformer&lt;/span&gt;
            &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
            &lt;span class="na"&gt;blur&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
            &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si vous préférez un rendu en dégradé, il y a aussi un placeholder &lt;a href="https://blurha.sh/" rel="noopener noreferrer"&gt;BlurHash&lt;/a&gt; (il demande &lt;code&gt;kornrunner/blurhash&lt;/code&gt; et &lt;code&gt;imagine/imagine&lt;/code&gt;). Et pour ThumbHash, une couleur dominante ou n'importe quoi qui génère une data URI, vous écrivez le vôtre avec l'attribut &lt;code&gt;#[AsPlaceholder]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Dans tous les cas, le placeholder est inliné en &lt;code&gt;background-image&lt;/code&gt; sur le &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; et retiré une fois l'image chargée. Il ne joue pas sur le CLS (ça, c'est le rôle de &lt;code&gt;width&lt;/code&gt; et &lt;code&gt;height&lt;/code&gt;) : il occupe juste la place en attendant la vraie image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Des loaders pour la vraie vie
&lt;/h3&gt;

&lt;p&gt;Un bundle qui ne lit que dans &lt;code&gt;public/uploads&lt;/code&gt;, ça ne tient pas longtemps sur un vrai projet. Les loaders fournis :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem&lt;/strong&gt;, le classique, un dossier local par loader&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flysystem&lt;/strong&gt;, donc S3, GCS, Azure, FTP et tout ce qui ressemble à du stockage objet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VichUploaderBundle&lt;/strong&gt;, parce que dès qu'un projet Symfony a des uploads, c'est quasi toujours Vich derrière&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL&lt;/strong&gt;, pour des images hébergées ailleurs dont vous voulez quand même un srcset&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avec Vich, le loader porte le nom du mapping, et vous lui passez l'entité :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/packages/picasso.yaml&lt;/span&gt;
&lt;span class="na"&gt;picasso&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;loaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;product_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;vich&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;twig:Picasso:Image&lt;/span&gt;
    &lt;span class="na"&gt;loader=&lt;/span&gt;&lt;span class="s"&gt;"product_image"&lt;/span&gt;
    &lt;span class="na"&gt;:context=&lt;/span&gt;&lt;span class="s"&gt;"{ entity: product }"&lt;/span&gt;
    &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;
    &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt;
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Photo produit"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le bundle retrouve tout seul le fichier depuis le mapping. Et si votre source d'images est trop exotique pour tout ça, vous écrivez votre loader avec l'attribut &lt;code&gt;#[AsImageLoader]&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Et pour les frontends headless ?
&lt;/h3&gt;

&lt;p&gt;Si votre Symfony sert une API JSON à du React, du Vue ou une app mobile, &lt;code&gt;ImageHelperInterface::imageData()&lt;/code&gt; renvoie tout ce que génère le composant : URLs par format, srcset, placeholder, dimensions. Le front n'a plus qu'à reconstruire son &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt;. Et pour une simple URL (balise Open Graph, background CSS, email), il y a la fonction Twig &lt;code&gt;picasso_image_url()&lt;/code&gt;. Les détails sont dans &lt;a href="https://github.com/silarhi/picasso-bundle#readme" rel="noopener noreferrer"&gt;le README&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  La purge du cache
&lt;/h3&gt;

&lt;p&gt;Votre client change sa photo de profil (ça arrive toujours) : il faut invalider toutes les variantes générées depuis l'ancienne. Les deux transformers fournis savent le faire. Glide nettoie son cache local, Imgix appelle son API de purge (avec une clé d'API dans la config). Côté code :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Service/ImageManager.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Silarhi\PicassoBundle\Service\ImagePipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImageManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;ImagePipeline&lt;/span&gt; &lt;span class="nv"&gt;$pipeline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;deleteImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ça a l'air anecdotique. Jusqu'au jour où votre client vous appelle parce que sa nouvelle photo ne s'affiche pas en prod. Cache invalidation, toujours elle : &lt;a href="https://blog.silarhi.fr/docker-compose-traefik-https/" rel="noopener noreferrer"&gt;Phil Karlton vous avait prévenu&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ça donne quoi au final ?
&lt;/h2&gt;

&lt;p&gt;La démo de cet article : 👉 &lt;a href="https://labs.silarhi.fr/picasso" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/picasso&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vous y trouverez une grille responsive avec &lt;code&gt;sizes&lt;/code&gt;, les différents modes de &lt;code&gt;fit&lt;/code&gt; sur la même image, une bannière en &lt;code&gt;priority&lt;/code&gt;, et quelques réglages par image (qualité, placeholder désactivé…). Le tout servi par Glide avec des URLs signées. Le code est sur &lt;a href="https://github.com/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;silarhi/symfony-docker-ci&lt;/a&gt;, le projet qui me sert depuis des années pour les démos de ce blog.&lt;/p&gt;

&lt;p&gt;La page historique qui utilise Glide directement est toujours là : &lt;a href="https://labs.silarhi.fr/images" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/images&lt;/a&gt;. Comparez la quantité de Twig nécessaire entre les deux (spoiler : ce n'est pas vraiment la même).&lt;/p&gt;

&lt;p&gt;Pour les curieux, sous le capot :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.2+, Symfony 6.4, 7.x et 8.x&lt;/li&gt;
&lt;li&gt;Loaders, transformers et placeholders déclarés avec des attributs PHP (&lt;code&gt;#[AsImageLoader]&lt;/code&gt;, &lt;code&gt;#[AsImageTransformer]&lt;/code&gt;, &lt;code&gt;#[AsPlaceholder]&lt;/code&gt;) : zéro XML, zéro YAML de service, tout est autowiré&lt;/li&gt;
&lt;li&gt;Un cache PSR-6 pour les dimensions détectées et les BlurHash&lt;/li&gt;
&lt;li&gt;Compatible FrankenPHP en mode worker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Voilà ! Vos images n'ont plus d'excuse pour plomber votre LCP. N'hésitez pas à laisser un commentaire si vous l'utilisez, ou si quelque chose coince. Et si le bundle vous fait gagner du temps, une petite ⭐ sur GitHub fait toujours plaisir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pour aller plus loin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/picasso" rel="noopener noreferrer"&gt;La démo du tuto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/picasso-bundle" rel="noopener noreferrer"&gt;Le code source du bundle sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/silarhi/picasso-bundle" rel="noopener noreferrer"&gt;Le package sur Packagist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;Le code source de la démo sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/components/image" rel="noopener noreferrer"&gt;L'inspiration : next/image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.silarhi.fr/symfony-miniature-image-glide-php/" rel="noopener noreferrer"&gt;La version "à l'ancienne" avec Glide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>symfony</category>
      <category>php</category>
      <category>code</category>
      <category>performance</category>
    </item>
    <item>
      <title>Gérer ses miniatures avec Symfony et Glide</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Sat, 13 Jun 2020 12:15:26 +0000</pubDate>
      <link>https://dev.to/silarhi/gerer-ses-miniatures-avec-symfony-et-glide-382i</link>
      <guid>https://dev.to/silarhi/gerer-ses-miniatures-avec-symfony-et-glide-382i</guid>
      <description>&lt;p&gt;🎉 Cet article a été cité dans le &lt;a href="https://symfony.com/blog/a-week-of-symfony-702-8-14-june-2020" rel="noopener noreferrer"&gt;Week of Symfony #702&lt;/a&gt; 🎉&lt;/p&gt;

&lt;p&gt;Aujourd'hui j'ai envie de vous parler d'un pattern récurrent dans un projet Web : la gestion des images et surtout des miniatures associées.&lt;/p&gt;

&lt;p&gt;Aujourd'hui encore, le poids des images dans une page Web est toujours un facteur clé dans l'optimisation du poids des pages. Combien d'entre-nous continue encore de servir aux internautes mobiles des images de 1200px de long ?&lt;/p&gt;

&lt;p&gt;On va donc voir comment mettre en place dans un projet Symfony la librairie PHP &lt;a href="https://glide.thephpleague.com/" rel="noopener noreferrer"&gt;Glide&lt;/a&gt; qui met à disposition une API pour gérer la manipulation d'image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Démo
&lt;/h2&gt;

&lt;p&gt;Avant de partir dans les explications, je vais déjà tenter de vous attendrir avec le type de rendu final que l'on peut obtenir avec cette librairie :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ql9jr53xf9e7t14i7pf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ql9jr53xf9e7t14i7pf.png" alt="Utiliser Glide avec Symfony" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La magnifique démo de cet article : 👉 &lt;a href="https://labs.silarhi.fr/images" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/images&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Sans plus attendre on va commencer par installer tout doucement la librairie et créer un service Symfony pour gérer tout ça. Le package &lt;code&gt;league/glide-symfony&lt;/code&gt; possède un &lt;code&gt;adapter&lt;/code&gt; pour Symfony qui intègre la gestion des &lt;code&gt;Response&lt;/code&gt; mais ne fournit pas de service par défaut.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require league/glide league/glide-symfony
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;On va modifier un peu la configuration actuelle pour ajouter le service Symfony et préparer l'API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/services.yaml&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app.public_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%kernel.project_dir%/public'&lt;/span&gt;
    &lt;span class="na"&gt;app.image_cache_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%kernel.project_dir%/var/storage/cache'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;_defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;#...&lt;/span&gt;
        &lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;$secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%kernel.secret%'&lt;/span&gt;

    &lt;span class="na"&gt;League\Glide\Server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;factory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;League\Glide\ServerFactory'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%app.public_dir%'&lt;/span&gt;
            &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%app.image_cache_dir%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mise en place
&lt;/h2&gt;

&lt;p&gt;L'idée est ensuite de créer un &lt;code&gt;Controller&lt;/code&gt; pour générer à la volée les miniatures (c'est notre "API"). On passera dans la requête le chemin relatif de l'image à utiliser ainsi que des pour indiquer la longueur / largeur désirée de la miniature, la couleur de fond, etc ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note sur la sécurité&lt;/strong&gt; : Pour empêcher des utilisateurs malveillants de demander à générer sur votre serveur des centaines de miniatures aux dimensions non voulues (Imaginez votre serveur devant répondre à 10 000 requêtes de miniatures au format 50000x50000), nous allons également ajouter un hash de signature qui sera unique pour les dimensions données et que le &lt;code&gt;Controller&lt;/code&gt; devra valider pour générer la miniature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Le controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;//src/Controller/AssetsController.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Glide\Filesystem\FileNotFoundException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Glide\Responses\SymfonyResponseFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Glide\Server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Glide\Signatures\SignatureException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Glide\Signatures\SignatureFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Bundle\FrameworkBundle\Controller\AbstractController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Attribute\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AssetsController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Route(path: '/assets/{path&amp;lt;(.+)&amp;gt;}', name: 'asset_url', methods: ['GET'])]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Server&lt;/span&gt; &lt;span class="nv"&gt;$glide&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;SignatureFactory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validateRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SignatureException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createNotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$glide&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setResponseFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SymfonyResponseFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$glide&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\InvalidArgumentException&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nc"&gt;FileNotFoundException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createNotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  L'extension Twig
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// src/Twig/AssetExtension.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Twig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Glide\Signatures\SignatureFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Generator\UrlGeneratorInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Twig\Attribute\AsTwigFunction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AssetExtension&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;UrlGeneratorInterface&lt;/span&gt; &lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[AsTwigFunction(name: 'app_asset', isSafe: ['html'])]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;appAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$referenceType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UrlGeneratorInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ABSOLUTE_PATH&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'fm'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pjpg'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str_ends_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'png'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'fm'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'png'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'s'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SignatureFactory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generateSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ltrim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'asset_url'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$referenceType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus besoin d'étendre &lt;code&gt;AbstractExtension&lt;/code&gt; : depuis Symfony 7.3 (et Twig 3.21), l'attribut &lt;code&gt;#[AsTwigFunction]&lt;/code&gt; déclare la fonction Twig directement sur la méthode. Avec ces 2 nouveaux fichiers, vous avez un système tout prêt de génération de miniatures sécurisé pour votre belle application !&lt;/p&gt;

&lt;h2&gt;
  
  
  Utilisation
&lt;/h2&gt;

&lt;p&gt;Il ne vous reste plus qu'à utiliser votre fonction twig dans vos templates. La version minimale pourrait ressembler à ceci :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="c"&gt;{# templates/images.html.twig #}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"img-fluid"&lt;/span&gt;
     &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;app_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'images/mon-image.jpg'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;200&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
     &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;
     &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt;
     &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Ma super image de 400x200"&lt;/span&gt;
     &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Ma super image de 400x200"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce qui produit l'URL suivante :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/assets/images/mon-image.jpg?w=400&amp;amp;h=200&amp;amp;fm=pjpg&amp;amp;s=d6ba0d9ef7622516b83fa3402527affe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce qui revient à demander au serveur :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Génère-moi la miniature du fichier &lt;code&gt;/images/mon-image.jpg&lt;/code&gt; (si elle n'est pas déjà générée) d'une longueur de 400px (paramètre &lt;code&gt;w&lt;/code&gt;), d'une hauteur de 200px (paramètre &lt;code&gt;h&lt;/code&gt;), au format JPG Progressive (paramètre &lt;code&gt;fm&lt;/code&gt;). Pour te prouver que je suis de bonne foi, je te donne le hash de vérification de ces 4 paramètres (paramètre &lt;code&gt;s&lt;/code&gt;). Merci bisous.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Gestion des écrans retina
&lt;/h3&gt;

&lt;p&gt;La gestion des écrans retina devient beaucoup plus simple vu qu'il n'y a plus qu'à passer les bons paramètres au contrôleur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="c"&gt;{# templates/images.html.twig #}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"img-fluid"&lt;/span&gt;
     &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;app_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'images/mon-image.jpg'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;200&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
     &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;app_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'images/mon-image.jpg'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dpr'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="err"&gt;2&lt;/span&gt;&lt;span class="na"&gt;x&lt;/span&gt;
     &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;
     &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt;
     &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Ma super image de 400x200"&lt;/span&gt;
     &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Ma super image de 400x200"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;J'ai juste ajouté avec le paramètre &lt;code&gt;dpr&lt;/code&gt; (&lt;em&gt;Device pixel ratio&lt;/em&gt;) pour générer une image 2 fois plus dense (en l'occurence 800x400).&lt;/p&gt;

&lt;h3&gt;
  
  
  Aller plus loin
&lt;/h3&gt;

&lt;p&gt;Le bon développeur ou la bonne développeuse que vous êtes a sûrement détecté un pattern à factoriser pour améliorer la maintenabilité de tout ça. Je vous joins les 2 macros que j'utilise dans quasiment tous mes projets Symfony :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="c"&gt;{# templates/macros.html.twig #}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;app_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;params&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;{}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
         &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;app_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;params&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;{}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dpr'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; 2x"&lt;/span&gt;
         &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;width&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
         &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
         &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
        &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;="&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;e&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endfor&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endmacro&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;

&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nv"&gt;fixedHeightImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"img-fluid &lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;attrs.class&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
         &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;app_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
         &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;app_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dpr'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; 2x"&lt;/span&gt;
         &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
        &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%}{{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;="&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;e&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endfor&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endmacro&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="c"&gt;{# templates/images.html #}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s1"&gt;'macros.html.twig'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;macros&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;

&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;macros.image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'images/mon-image.jpg'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'img-fluid'&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voilà ! Vous êtes maintenant un pro des miniatures ! N'hésitez pas à regarder le code source de la démo qui intègre Vite en plus de tout ça.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pour aller plus loin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/images" rel="noopener noreferrer"&gt;La démo du tuto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/symfony-docker-ci" rel="noopener noreferrer"&gt;Le code source utilisé sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://glide.thephpleague.com/" rel="noopener noreferrer"&gt;La documentation de Glide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.silarhi.fr/presentation-picasso-bundle-symfony-image/" rel="noopener noreferrer"&gt;PicassoBundle, qui pousse le concept plus loin (AVIF, WebP, srcset automatique)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>symfony</category>
      <category>php</category>
      <category>code</category>
    </item>
    <item>
      <title>Coder sans jQuery ? Retour d'expérience</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Thu, 16 May 2019 06:49:15 +0000</pubDate>
      <link>https://dev.to/silarhi/adieu-jquery-bootstrap-5-annonce-la-tendance-retour-d-experience-5feb</link>
      <guid>https://dev.to/silarhi/adieu-jquery-bootstrap-5-annonce-la-tendance-retour-d-experience-5feb</guid>
      <description>&lt;p&gt;C'est officiel, &lt;a href="https://getbootstrap.com/" rel="noopener noreferrer"&gt;Bootstrap&lt;/a&gt; va se séparer de sa dépendance la plus forte : &lt;a href="https://jquery.com/" rel="noopener noreferrer"&gt;jQuery&lt;/a&gt;. C'est ce qu'annonce &lt;a href="https://github.com/twbs/bootstrap/pull/23586" rel="noopener noreferrer"&gt;cette PR&lt;/a&gt; sur le Github du plus célèbre Framework CSS. A la place, pas de nouveau framework Javascript, pas de super lib next gen : du Vanilla JS, tout simplement. Cette PR montre pour moi la tendance Javascript de 2019, qui se confirme en 2020 : jQuery a-t-il encore une raison d'exister ?&lt;/p&gt;

&lt;p&gt;Si l'on remonte de quelques années aux temps où IE 6 était roi, jQuery avait du sens lorsque les navigateurs avaient d'énormes différences dans l'implémentation des specs. Aujourd'hui, bien qu'encore présentes, ces différences tendent à se réduire de plus en plus :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;querySelector&lt;/code&gt; est devenu stable et consistent entre les navigateurs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;XMLHTTPRequest&lt;/code&gt; peut être remplacé par &lt;code&gt;fetch&lt;/code&gt; qui est devenu standard.&lt;/li&gt;
&lt;li&gt;Les animations peuvent être gérées en CSS3 et sont d'ailleurs plus performantes car elles utilisent le GPU du client.&lt;/li&gt;
&lt;li&gt;Microsoft a &lt;a href="https://blogs.windows.com/windowsexperience/2018/12/06/microsoft-edge-making-the-web-better-through-more-open-source-collaboration/#lYvJtFqtZ1pB83I7.97" rel="noopener noreferrer"&gt;annoncé&lt;/a&gt; l'arrêt de l'utilisation de son moteur de rendu en faveur de Chromium pour son navigateur Edge.&lt;/li&gt;
&lt;li&gt;etc etc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pour enfoncer le clou, GitHub &lt;a href="https://github.blog/2018-09-06-removing-jquery-from-github-frontend/" rel="noopener noreferrer"&gt;annonce lui aussi&lt;/a&gt; qu'il a supprimé jQuery de son interface frontend. Alors, faut-il bannir absolument jQuery de ses nouveaux projets ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Mon retour d'expérience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Un zeste de contexte
&lt;/h3&gt;

&lt;p&gt;J'ai récemment développé une application Symfony pour un client. Fort de cette tendance, j'ai donc décidé d'utiliser du pur Javascript dans le projet et voir si j'arrivais à m'en sortir.&lt;/p&gt;

&lt;p&gt;Pour ça, je me suis fortement inspiré de la structure frontend du site en-marche.fr qui était Open Source à l'époque (pro tip : vous n'avez pas besoin d'adhérer aux idées du parti politique pour pouvoir tirer le meilleur de l'architecture du site 🤫). Je trouve d'ailleurs que &lt;a href="https://github.com/tgalopin" rel="noopener noreferrer"&gt;Titouan Galopin&lt;/a&gt; a fait de l'excellent travail lorsqu'il était sur ce projet.&lt;/p&gt;

&lt;h3&gt;
  
  
  La stack technique
&lt;/h3&gt;

&lt;p&gt;Pour ce projet, j'ai utilisé Bootstrap, &lt;a href="https://github.com/axios/axios" rel="noopener noreferrer"&gt;axios&lt;/a&gt; pour les requêtes HTTP, &lt;a href="https://sweetalert2.github.io/" rel="noopener noreferrer"&gt;SweetAlert2&lt;/a&gt; pour les popups, &lt;a href="https://select2.org/" rel="noopener noreferrer"&gt;Select2&lt;/a&gt; pour des jolis select et &lt;a href="https://github.com/MoOx/pjax" rel="noopener noreferrer"&gt;pjax&lt;/a&gt; pour accélérer les changements de page. Le code front métier étant écrit en &lt;a href="http://es6-features.org/#Constants" rel="noopener noreferrer"&gt;ES6&lt;/a&gt; et transpilé avec Webpack et Babel.&lt;/p&gt;

&lt;p&gt;À cause de certaines de ces libs, j'ai donc quand même été contraint d'inclure jQuery dans mon projet, mais j'ai décidé de restreindre son utilisation uniquement pour manipuler les plugins. Histoire de jouer le jeu pour de vrai.&lt;/p&gt;

&lt;h3&gt;
  
  
  Résultat
&lt;/h3&gt;

&lt;p&gt;Globalement, l'absence de jQuery ne s'est pas tant fait ressentir. C'est d'ailleurs &lt;strong&gt;gratifiant&lt;/strong&gt; de savoir que l'on peut développer rapidement des apps sans framework JS et leurs contraintes associées.&lt;/p&gt;

&lt;p&gt;En utilisant jQuery, je trouve aussi qu'on se place dans une zone de confort qui nous maintient à l’écart de l’univers JS. Et ça bouge vite, très vite. De nouveaux standards apparaissent quasiment chaque année, et apportent des évolutions intéressantes et de nouvelles façons de penser et d’écrire le Javascript. Il se passe des choses et il ne faut pas rater le coche, il n'y a qu’à voir du code écrit avec React pour se rendre compte du fossé qu'il y a avec jQuery, alors que les 2 s'appuient sur le même langage.&lt;/p&gt;

&lt;p&gt;Mais tout n'est pas rose, il y a quand même eu quelques points de frustration :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Il y a toujours beaucoup de libs qui ne marchent qu'avec jQuery (Bootstrap 4, select2, etc etc). C'est pourquoi même si je n'en voulais pas, jQuery s'est invité de force dans mon fichier &lt;code&gt;package.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/wix-engineering/why-i-wont-be-using-fetch-api-in-my-apps-6900e6c6fe78" rel="noopener noreferrer"&gt;fetch n'est pas si cool que ça&lt;/a&gt;. J'ai fini par utiliser axios après avoir perdu pas mal de temps à tenter de cerner ses subtilités.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Et vous ? Utilisez-vous encore jQuery en 2020 dans vos nouveaux projets ? Pensez-vous que jQuery va mourir ?&lt;/p&gt;

&lt;p&gt;PS : Le bug de l'an 2000 risque bien de se reproduire si les systèmes ne se mettent pas à jour avant le &lt;a href="https://en.wikipedia.org/wiki/Year_2038_problem" rel="noopener noreferrer"&gt;19 Janvier 2038 à 03h14 et 08s GMT&lt;/a&gt;, date à laquelle toutes les architectures d'ordinateurs qui sont encore en 32 bits cesseront de gérer correctement les dates. Mais ça reste entre nous 🤫&lt;/p&gt;

</description>
      <category>tech</category>
      <category>javascript</category>
      <category>jquery</category>
    </item>
    <item>
      <title>Utilisez Varnish et le cache ESI pour diviser le temps de réponse de votre site Web par 100</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Sat, 11 May 2019 13:21:29 +0000</pubDate>
      <link>https://dev.to/silarhi/divisez-le-temps-de-reponse-de-votre-site-web-par-100-avec-varnish-et-les-fragments-esi-46kd</link>
      <guid>https://dev.to/silarhi/divisez-le-temps-de-reponse-de-votre-site-web-par-100-avec-varnish-et-les-fragments-esi-46kd</guid>
      <description>&lt;p&gt;Le but de cet article est de vous faire comprendre les différents mécanismes qui peuvent être utilisés avec Varnish et le cache HTTP (TTL, Cache-Control, fragments ESI).&lt;br&gt;&lt;br&gt;
On va étudier un cas pratique avec une app PHP mais ces principes sont valables quel que soit le langage derrière, ce qui compte vraiment sont les entêtes de réponses que vous fournissez !&lt;/p&gt;

&lt;p&gt;On évoque souvent la mise en place du cache applicatif type Redis comme la première solution pour optimiser les temps de réponse des sites. Découvrez ici seulement la vraie solution pour réduire les temps de réponse de votre site Web que seul 1% des gens connait.&lt;/p&gt;

&lt;p&gt;Si vous lisez encore ces lignes, c'est que j'ai bien fait mon travail de teasing. Sans plus attendre, voici la solution ultime pour réduire les temps de réponse de votre site : &lt;strong&gt;Ne pas appeler votre site&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzs0vzzfds0vgl2kk8bl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzs0vzzfds0vgl2kk8bl.gif" alt="Mais quelle drôle d'idée." width="350" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mais quelle drôle d'idée.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mine de rien, démarrer un process Web et retourner une réponse au client, même avec un super framework récent, ça reste long et coûteux. Bootstrapping du framework, connexion à une base de données, à un serveur Redis, vérification des droits utilisateurs, moteur de templating, etc etc. Sans trop forcer, vous pouvez vite atteindre les 200 ms pour retourner du contenu.&lt;/p&gt;

&lt;p&gt;🤓 « &lt;em&gt;Alors comment faire pour réduire mes temps de réponse ? Est-ce que je dois investir, booster mon serveur de prod et doubler le montant ma facture ?&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pas du tout !&lt;/strong&gt; L'idée est d'utiliser à fond le cache HTTP pour stocker le rendu HTML généré par le backend. Pour ça, on va jouer avec &lt;a href="https://varnish-cache.org/" rel="noopener noreferrer"&gt;Varnish&lt;/a&gt;, un reverse proxy-ultra puissant écrit en C. On parle ici de temps de réponse de l'ordre de &lt;strong&gt;quelques millisecondes&lt;/strong&gt; si le contenu de la page est déjà présent dans le cache.&lt;/p&gt;

&lt;p&gt;Le principe de Varnish est plutôt simple : Le reverse-proxy est en première ligne des requêtes utilisateurs. S'il possède déjà la réponse à cette requête, il la délivre directement sans interroger votre app. Sinon, il forward la request au backend et stocke le résultat (si vos entêtes lui indiquent). C'est aussi simple que ça. On va voir comment mettre en place concrètement ce flux en place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81crgnd4tgawhh4e43uf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81crgnd4tgawhh4e43uf.jpg" alt="Le principe simple de Varnish" width="700" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Le principe simple de Varnish&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  C'est parti
&lt;/h2&gt;

&lt;p&gt;L'idée est donc de mettre Varnish en frontal des requêtes utilisateur. C'est lui qui va maintenant écouter sur le port 80 et envoyer les requêtes vers votre serveur Web qui était en frontal jusqu'ici.&lt;/p&gt;

&lt;p&gt;La première étape est de modifier les ports d'écoute de votre backend. Par exemple pour Apache on va modifier la configuration :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- Listen 80
&lt;/span&gt;&lt;span class="gi"&gt;+ Listen 8000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gardez en tête le nouveau port d'écoute de votre serveur Web. Ensuite, on va installer Varnish : soit avec les paquets de votre distribution sur &lt;a href="https://packagecloud.io/varnishcache" rel="noopener noreferrer"&gt;packagecloud&lt;/a&gt;, soit, le plus simple, avec &lt;a href="https://hub.docker.com/_/varnish" rel="noopener noreferrer"&gt;l'image Docker officielle&lt;/a&gt; (c'est ce qu'utilise la démo, on y revient à la fin).&lt;/p&gt;

&lt;h2&gt;
  
  
  Un premier exemple : du contenu statique
&lt;/h2&gt;

&lt;p&gt;Varnish utilise son propre langage de configuration (VCL) qu'il compile ensuite en C. C'est un poil bas niveau mais une fois bien configuré, vous allez mettre un moteur de Ferrari dans votre 2 CV.&lt;br&gt;&lt;br&gt;
On va commencer par un VCL minimaliste :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;# /etc/varnish/default.vcl
&lt;/span&gt;&lt;span class="n"&gt;vcl&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"8000"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Le&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="err"&gt;'é&lt;/span&gt;&lt;span class="n"&gt;coute&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;votre&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Apache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Nginx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;# Appelé à la fin de chaque réponse (en cache ou non)
&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="n"&gt;vcl_deliver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"HIT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MISS"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prenez le temps de vous familiariser avec la syntaxe et la configuration. Ici, on a indiqué à Varnish qu'il existe un backend (&lt;code&gt;default&lt;/code&gt;) accessible à l'adresse 127.0.0.1:8000 (souvenez-vous, c'est le nouveau port d'écoute de votre serveur Web).&lt;br&gt;&lt;br&gt;
On a aussi configuré une entête de réponse que l'on a nommé &lt;code&gt;X-Cache&lt;/code&gt; pour savoir si la réponse a été récupérée du cache ou non.&lt;br&gt;&lt;br&gt;
Vérifions le résultat en appelant la page d'accueil de notre site pour la première fois :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kv8brsqwt7ihpylxnlz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kv8brsqwt7ihpylxnlz.png" alt="Première requête : Varnish n'a rien en cache" width="590" height="668"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Première requête : Varnish n'a rien en cache&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Étant donné que c'est la première fois que l'on accède à la ressource, Varnish ne possède pas la réponse dans son cache, ce qui est logique. Il appelle donc le backend normalement et stocke le résultat pour une durée de 120 secondes (c'est la configuration par défaut de Varnish, vous pouvez bien sur le changer selon vos besoins). Si on actualise la page, on constate que Varnish réagit différemment :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ifvl8zbfz33wsw5izx6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ifvl8zbfz33wsw5izx6.png" alt="Deuxième requête : Varnish retourne directement la réponse" width="584" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Deuxième requête : Varnish retourne directement la réponse&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Parfait ! Varnish a directement retourné le résultat qu'il avait stocké lors de la première requête. Le backend n'a purement pas été sollicité, vous n'aurez aucune trace d'accès dans les logs de votre serveur Web. &lt;strong&gt;Ça commence à ressembler à quelque chose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🤓 « &lt;em&gt;Mais si mon contenu varie en fonction de l'utilisateur... Je ne veux pas retourner le même contenu pour tout le monde. Comment faire ?&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;C'est là qu'interviennent les &lt;strong&gt;fragments ESI&lt;/strong&gt;, qui sont selon moi une source de performance extrême tellement sous-côtée !&lt;/p&gt;
&lt;h2&gt;
  
  
  Mettre en cache des fragments de page avec le cache ESI
&lt;/h2&gt;

&lt;p&gt;Il est possible de mettre en cache &lt;strong&gt;une partie&lt;/strong&gt; de la page pendant une certaine durée grâce aux blocs de cache ESI (Edge Side Includes). Cette spécification a été écrite par Akamai et Oracle en 2001 dans le but de mettre en place des stratégies de cache différentes par "bloc". Par exemple : ma page possède un menu type &lt;em&gt;navbar&lt;/em&gt; avec le nom de l'utilisateur connecté. Le reste de ma page est identique pour tout le monde et affiche une liste de produits. Je peux donc mettre en place un bloc qui va contenir le menu et mettre en cache tout le reste.&lt;/p&gt;

&lt;p&gt;Assez de théorie, voyons comment ça se traduit dans une app :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Ma super app&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Le bloc du ESI est défini là --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;esi:include&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/mon-menu-prive.php"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C'est aussi simple que ça ! On peut le voir un peu comme un appel Ajax qui serait fait pour chaque bloc, sauf que c'est le reverse-proxy (Varnish) qui fait la requête, vous ne verrez pas de balise &amp;lt;esi&amp;gt; dans le code source généré.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un cas d'étude concret
&lt;/h3&gt;

&lt;p&gt;On va mettre en place une petite app PHP qui sera composée de plusieurs blocs. Certains blocs vont varier en fonction de l'utilisateur (ex: un menu), d'autres seront publics. La durée de mise en cache des blocs va aussi être variable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qfk4hc63sam17rd03k9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qfk4hc63sam17rd03k9.png" alt="Démo de labs.silarhi.fr/esi : la page et ses fragments ESI, chacun avec sa durée de cache" width="800" height="719"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La magnifique démo de cet article : 👉 &lt;a href="https://labs.silarhi.fr/esi/" rel="noopener noreferrer"&gt;https://labs.silarhi.fr/esi/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Entrons dans le vif du sujet, voici le code :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// index.php&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Surrogate-Control: abc=ESI/1.0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Reverse-Proxy-TTL: 10'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"fr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/@tabler/core@1.7.1/dist/css/tabler.min.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Ma super page&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container my-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;esi:include&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/menu.php"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;esi:include&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/time.php"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-lg-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Contenu public de la page&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Mis en cache à &lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'H:i:s'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt; (10s)&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-lg-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;esi:include&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/user.php"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;esi:include&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/sidebar.php"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ne vous focalisez pas encore trop sur le détail de ce code, l'idée est juste d'imaginer la mise en place de vos propres blocs dans votre application. La démo en ligne ajoute autour de chaque fragment un cadre qui affiche sa politique de cache et le temps qu'il lui reste : &lt;a href="https://github.com/silarhi/labs.silarhi.fr/tree/main/esi" rel="noopener noreferrer"&gt;le code complet est sur GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Outre ce design de qualité, vous avez sûrement identifié les 4 tags &amp;lt;esi&amp;gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Le menu du haut (menu.php)&lt;/li&gt;
&lt;li&gt;L'heure du serveur avec la légende (time.php)&lt;/li&gt;
&lt;li&gt;L'heure de la dernière connexion (user.php)&lt;/li&gt;
&lt;li&gt;Les widgets (sidebar.php).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;4 blocs pour 4 politiques de mise en cache différentes. C'est le mélange de la configuration dans Varnish et l'envoi d'entêtes HTTP de réponses par le backend qui vont nous permettre de mettre en place ces différentes politiques.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration dans Varnish
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;# /etc/varnish/default.vcl
&lt;/span&gt;&lt;span class="n"&gt;vcl&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"8000"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Le&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="err"&gt;'é&lt;/span&gt;&lt;span class="n"&gt;coute&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;votre&lt;/span&gt; &lt;span class="n"&gt;serveur&lt;/span&gt; &lt;span class="n"&gt;Apache&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;Nginx&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;# Appelé au début de chaque requête
&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="n"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cp"&gt;# Pas de mise en cache pour les méthodes type POST / DELETE
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"GET"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;restarts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hash_always_miss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cp"&gt;# Connexion / déconnexion : jamais en cache, elles modifient la session
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s"&gt;"^/(login|logout)\.php"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cp"&gt;# On supprime les cookies sur les pages publiques (cf vcl_hash)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;!~&lt;/span&gt; &lt;span class="s"&gt;"^/(menu|user)\.php"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unset&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cp"&gt;# On supprime tous les autres cookies que ESISESSID (la session de la démo) pour les pages privées
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ESISESSID"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_string&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;unset&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;# Appelé pour calculer un hash de la requête
&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="n"&gt;vcl_hash&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;hash_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hash_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hash_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hash_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cookie&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;# Appelé si le hash a été trouvé (= page en cache)
&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="n"&gt;vcl_hit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;# Appelé au retour de la réponse par le backend
&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="n"&gt;vcl_backend_response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cp"&gt;# L'entête est envoyée par index.php
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beresp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Surrogate&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s"&gt;"ESI/1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unset&lt;/span&gt; &lt;span class="n"&gt;beresp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Surrogate&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;beresp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;do_esi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cp"&gt;# Notre fameuse entête custom
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beresp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Reverse&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Proxy&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;TTL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;beresp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beresp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Reverse&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Proxy&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;TTL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;unset&lt;/span&gt; &lt;span class="n"&gt;beresp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Reverse&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Proxy&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;TTL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;# Appelé à la fin de chaque réponse (en cache ou non)
&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="n"&gt;vcl_deliver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"HIT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MISS"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il y a plusieurs concepts clés à comprendre dans cette configuration.&lt;br&gt;&lt;br&gt;
L'idée de fond est de générer un hash unique pour chaque requête / sous-requête. Ce hash doit dépendre du contexte utilisateur lorsque le contenu est censé être privé, en revanche il n'est pas nécessaire pour les blocs publics.&lt;br&gt;&lt;br&gt;
En PHP, l'utilisateur est lié à sa session via un cookie : &lt;code&gt;PHPSESSID&lt;/code&gt; par défaut, que la démo renomme en &lt;code&gt;ESISESSID&lt;/code&gt; (on verra pourquoi avec lib.php). C'est cette valeur de cookie que l'on doit conserver pour générer ce fameux hash unique dans les pages qui nous intéressent. Pour les autres pages, on peut se permettre de supprimer ce cookie dans le calcul du hash pour maximiser les chances de délivrer un contenu mis en cache par un autre utilisateur.&lt;/p&gt;

&lt;p&gt;On supprime également les cookies non utiles au backend (du type Google Analytics etc) qui vont faire baisser le taux de HIT pour rien. Depuis Varnish 6.4, le module &lt;code&gt;cookie&lt;/code&gt; est fourni avec Varnish : &lt;code&gt;cookie.keep()&lt;/code&gt; remplace avantageusement les &lt;code&gt;regsuball()&lt;/code&gt; illisibles que l'on trouvait partout. Adaptez bien sûr la fonction &lt;code&gt;vcl_recv&lt;/code&gt; en fonction de vos besoins.&lt;/p&gt;

&lt;p&gt;Les pages &lt;code&gt;login.php&lt;/code&gt; et &lt;code&gt;logout.php&lt;/code&gt; ne passent jamais par le cache (&lt;code&gt;return (pass)&lt;/code&gt;) : elles modifient la session, leur réponse ne doit être ni stockée ni partagée.&lt;/p&gt;

&lt;p&gt;L'illustration ci-dessous résume bien les différentes étapes pendant lesquelles nous devons agir :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.silarhi.fr%2Fcontent%2Fimages%2F2019%2F05%2Fsimplified_fsm.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.silarhi.fr%2Fcontent%2Fimages%2F2019%2F05%2Fsimplified_fsm.svg" alt="Workflow de Varnish" width="685" height="957"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Crédits : &lt;a href="https://book.varnish-software.com/4.0/chapters/VCL_Basics.html" rel="noopener noreferrer"&gt;Varnish Basics&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  lib.php
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// lib.php : inclus par les fragments privés, login.php et logout.php&lt;/span&gt;

&lt;span class="c1"&gt;// Pas PHPSESSID : une autre app PHP du même domaine utilise sûrement déjà ce nom&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SESSION_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ESISESSID'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;session_open&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;session_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SESSION_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;session_set_cookie_params&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'httponly'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'samesite'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Lax'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nb"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// En lecture seule, et seulement si le visiteur a déjà une session&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;session_read&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_COOKIE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;SESSION_NAME&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;session_open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;session_write_close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Deux pièges à éviter avec les sessions et un cache partagé.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Le nom du cookie&lt;/strong&gt; : si une autre application tourne sur le même domaine (la démo CI de labs.silarhi.fr, en Symfony), elle utilise elle aussi &lt;code&gt;PHPSESSID&lt;/code&gt;. Les deux apps se retrouvent alors avec le même identifiant de session, et se déconnectent l'une l'autre. Un nom de cookie propre à l'app règle le problème.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Le&lt;/strong&gt; &lt;code&gt;session_start()&lt;/code&gt; &lt;strong&gt;dans un fragment en cache&lt;/strong&gt; : pour un visiteur anonyme, il crée une session et envoie un &lt;code&gt;Set-Cookie&lt;/code&gt; que Varnish stocke avec le fragment. Quiconque appelle ce fragment directement pendant sa durée de cache reçoit alors le même identifiant de session. On ne lit donc la session que si le visiteur en a déjà une.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;sidebar.php&lt;/strong&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// sidebar.php&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Reverse-Proxy-TTL: 20'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Widgets publics&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Mis en cache à &lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'H:i:s'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt; (20s)&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Politique de mise en cache : Contenu public mis en cache pendant 20 secondes.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
C'est l'entête &lt;code&gt;X-Reverse-Proxy-TTL&lt;/code&gt; qui va donner la durée de mise en cache par Varnish. Cette entête &lt;strong&gt;n'est pas standard&lt;/strong&gt;. On aurait pu jouer avec l'entête standard &lt;code&gt;Cache-Control&lt;/code&gt;, mais elle possède un inconvénient majeur : &lt;strong&gt;On ne contrôle plus la propagation du cache&lt;/strong&gt; avec la directive &lt;code&gt;Cache-Control: public, s-maxage=20&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
Eh oui, il peut y avoir d'autres serveurs mandataires entre le client et votre serveur qui peuvent eux-aussi mettre en cache le contenu HTML avec cette directive. Et si vous mettez des grandes valeurs de TTL dans votre politique de Cache-Control, adieu la maîtrise de vos mises à jour. Mieux vaut rester maître de la diffusion de vos contenus.&lt;br&gt;&lt;br&gt;
En revanche, pour vos contenus type Images/CSS/JS, faites vous plaisir et laissez le cache-control public avec un grand TTL si vous maîtrisez le cache busting.&lt;/p&gt;
&lt;h3&gt;
  
  
  menu.php
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// menu.php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/lib.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Reverse-Proxy-TTL: 60'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$logged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;session_read&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s1"&gt;'logged'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body d-flex align-items-center justify-content-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge bg-success"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Connecté&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"logout.php"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Déconnexion&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge bg-secondary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Anonyme&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"login.php"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Connexion&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Mis en cache à &lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'H:i:s'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt; (60s)&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Politique de mise en cache : Contenu privé mis en cache pendant 60 secondes&lt;/em&gt;&lt;br&gt;&lt;br&gt;
La mise en cache du contenu en fonction de l'utilisateur sera gérée dans Varnish directement. Le truc étant d'inclure le &lt;code&gt;ESISESSID&lt;/code&gt; de l'utilisateur dans le calcul du "hash" d'une requête pour ce bloc-ci : vcl_recv ne garde que ce cookie, et vcl_hash l'ajoute au hash.&lt;/p&gt;
&lt;h3&gt;
  
  
  time.php
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// time.php&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Heure serveur : &lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'d/m/Y H:i:s'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt; (pas de cache)
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Politique de mise en cache : Contenu public sans mise en cache particulière&lt;/em&gt;.&lt;br&gt;&lt;br&gt;
Le backend est appelé systématiquement pour ce bloc (j'ai changé la configuration de base de Varnish pour mettre le TTL par défaut à 0s).&lt;/p&gt;
&lt;h3&gt;
  
  
  user.php
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// user.php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/lib.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;session_read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'logged'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
            Dernière connexion il y a &lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'last_login'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;s
        &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
            Pas de dernière connexion
        &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Politique de mise en cache : Contenu privé sans mise en cache particulière.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Le backend est appelé systématiquement pour ce bloc.&lt;/p&gt;
&lt;h3&gt;
  
  
  login.php
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// login.php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/lib.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;session_open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;session_regenerate_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'logged'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'last_login'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: ./'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Un système de login maison très classique, à deux détails près.&lt;br&gt;&lt;br&gt;
&lt;code&gt;session_regenerate_id()&lt;/code&gt; donne un nouvel identifiant de session à la connexion : c'est la protection classique contre la fixation de session, mais c'est aussi ce qui change la clé de cache de menu.php. Sans lui, un visiteur qui avait déjà une session continuerait à voir le menu « Anonyme » jusqu'à 60 secondes après s'être connecté.&lt;br&gt;&lt;br&gt;
La redirection est une &lt;code&gt;303&lt;/code&gt; et pas une &lt;code&gt;301&lt;/code&gt; : une redirection permanente peut être mise en cache par le navigateur, qui ne rappellerait alors plus jamais login.php.&lt;/p&gt;
&lt;h3&gt;
  
  
  logout.php
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// logout.php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/lib.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_COOKIE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;SESSION_NAME&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;session_open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nb"&gt;session_destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Même chemin et mêmes options que le cookie de session, sinon le navigateur le garde&lt;/span&gt;
    &lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;session_get_cookie_params&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lifetime'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nb"&gt;setcookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SESSION_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'expires'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: ./'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Il y a une particularité importante ici : &lt;strong&gt;On détruit entièrement le cookie&lt;/strong&gt; de la session PHP pour que Varnish ne nous retourne pas l'ancien contenu utilisateur mis en cache qui n'a plus de raison d'exister.&lt;/p&gt;
&lt;h2&gt;
  
  
  Et avec Docker ?
&lt;/h2&gt;

&lt;p&gt;La démo tourne avec Docker Compose : un conteneur PHP + Apache, et Varnish devant. Le backend du VCL n'est plus &lt;code&gt;127.0.0.1:8000&lt;/code&gt; mais le nom du service PHP : &lt;code&gt;.host = "esi-php"&lt;/code&gt; et &lt;code&gt;.port = "80"&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yaml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;esi-php&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;silarhi/php-apache:8.5&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./php:/app:ro&lt;/span&gt;

  &lt;span class="na"&gt;varnish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;varnish:8.0-alpine&lt;/span&gt;
    &lt;span class="c1"&gt;# -t 0 : TTL par défaut à 0s, seules les réponses avec X-Reverse-Proxy-TTL sont mises en cache&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;varnishd"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-F"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/varnish/default.vcl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-t"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;esi-php&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./varnish/default.vcl:/etc/varnish/default.vcl:ro&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8080:80'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;code&gt;depends_on&lt;/code&gt; n'est pas là pour faire joli : Varnish résout le nom du backend au chargement du VCL, il refuse donc de démarrer tant que le conteneur PHP n'existe pas. Pensez aussi à redémarrer Varnish quand vous modifiez le VCL : il n'est compilé qu'au démarrage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le mot de la fin
&lt;/h2&gt;

&lt;p&gt;On a vu comment mettre en place plusieurs stratégies de cache différentes en fonction des types de blocs de vos pages. N'hésitez pas à vous approprier le concept de la mise en cache HTTP avec les fragments ESI et à l'utiliser massivement pour réduire drastiquement le temps de réponse de vos belles pages Web.&lt;/p&gt;

&lt;p&gt;C'est également une architecture qui peut s'installer progressivement dans votre app. Envie d'ajouter un widget qui fait des appels externes à des pages ? Mettez en place un fragment ESI !&lt;/p&gt;

&lt;h2&gt;
  
  
  Ressources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/esi/" rel="noopener noreferrer"&gt;La démo de l'exemple utilisé&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/labs.silarhi.fr/tree/main/esi" rel="noopener noreferrer"&gt;Le code source de la démo pour bien comprendre (utilise docker-compose)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://varnish-cache.org/docs/8.0/users-guide/index.html" rel="noopener noreferrer"&gt;La documentation officielle de Varnish&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/esi-lang/" rel="noopener noreferrer"&gt;La spécification ESI Language 1.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>code</category>
      <category>varnish</category>
      <category>php</category>
      <category>docker</category>
    </item>
    <item>
      <title>Une image Docker PHP + Apache sur mesure pour la production</title>
      <dc:creator>Guillaume Sainthillier</dc:creator>
      <pubDate>Mon, 25 Feb 2019 14:53:00 +0000</pubDate>
      <link>https://dev.to/silarhi/une-image-docker-php-apache-sur-mesure-pour-la-production-15e2</link>
      <guid>https://dev.to/silarhi/une-image-docker-php-apache-sur-mesure-pour-la-production-15e2</guid>
      <description>&lt;h2&gt;
  
  
  C'est quoi une image Docker parfaite ?
&lt;/h2&gt;

&lt;p&gt;C'est avant tout une image établie selon &lt;strong&gt;votre besoin et votre expérience&lt;/strong&gt;. Vous avez identifié des &lt;strong&gt;patterns communs&lt;/strong&gt; de configuration qui se répètent au fil de vos développements. Et comme vous êtes quelqu'un de bien, vous voulez donc les regrouper dans une couche commune à toutes vos apps pour en faciliter la maintenance.&lt;/p&gt;

&lt;p&gt;À partir de ma propre expérience et du type d'applications que je développe la plupart du temps, une image PHP Apache parfaite selon moi c'est :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt; installé &lt;strong&gt;par défaut&lt;/strong&gt; (qui n'utilise pas de gestionnaire de dépendances de nos jours ?).&lt;/li&gt;
&lt;li&gt;PHP et Apache configurés pour la &lt;strong&gt;production&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Des modules PHP récurrents activés par défaut.&lt;/li&gt;
&lt;li&gt;De jolies page d'erreurs type 404 un peu plus silencieuses que celles par défaut d'Apache.&lt;/li&gt;
&lt;li&gt;Idéalement des variantes déjà prêtes pour des applications qui tournent avec Symfony.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Créer son image parfaite
&lt;/h2&gt;

&lt;p&gt;En premier lieu on va partir de &lt;a href="https://hub.docker.com/_/php/" rel="noopener noreferrer"&gt;l'image officielle&lt;/a&gt; de php, parce qu'on veut gagner du temps et ne pas réinventer la roue. Vous savez quoi ? L'image est même fournie avec un serveur Apache intégré. &lt;strong&gt;TOP.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🤓 « &lt;em&gt;Pourquoi utiliser Apache plutôt que nginx ?&lt;/em&gt; »&lt;/p&gt;

&lt;p&gt;Parce que cet article n'est pas neutre et qu'il faut trancher. Personnellement, je trouve la différence de performances négligeables entre les deux et Apache convient parfaitement pour la plupart des projets. C'est également plus serein pour les mises à jour de sécurité qui seront directement effectuées par la team PHP Docker. Il m'arrive par contre pour &lt;a href="https://github.com/guillaume-sainthillier/by-night.fr" rel="noopener noreferrer"&gt;des projets un peu plus importants&lt;/a&gt; d'utiliser nginx. Encore une fois, cela reste vraiment &lt;strong&gt;une question de besoin, d'expérience et de goûts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On va donc commencer notre Dockerfile avec la ligne suivante :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; php:8.3-apache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On build et on run tout ça pour vérifier que tout va bien :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; super/hero &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 super/hero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En vous rendant sur &lt;a href="http://127.0.0.1:8080" rel="noopener noreferrer"&gt;http://127.0.0.1:8080&lt;/a&gt; vous devez tomber sur une belle page 403. Ça veut bien dire que tout fonctionne, il n'y a pas encore vos sources donc Apache vous répond gentiement !&lt;/p&gt;

&lt;h3&gt;
  
  
  Composer par défaut
&lt;/h3&gt;

&lt;p&gt;Rien de plus simple, on installe la dernière version de composer et on rend la commande accessible partout dans le conteneur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; COMPOSER_ALLOW_SUPERUSER=1&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://getcomposer.org/installer | php &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--install-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/bin &lt;span class="nt"&gt;--filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;composer
&lt;span class="k"&gt;RUN &lt;/span&gt;composer &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration d'Apache
&lt;/h3&gt;

&lt;p&gt;Par défaut, l'image officielle est configurée pour avoir la racine du serveur Web dans &lt;code&gt;/var/www/html&lt;/code&gt;. &lt;strong&gt;Trop long !&lt;/strong&gt; On va ignorer tout ça et accueillir notre futur code source dans un dossier &lt;code&gt;/app&lt;/code&gt; directement à la racine du conteneur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# conf/vhost.conf
&amp;lt;VirtualHost *:80&amp;gt;
    ServerAdmin contact@monsupersite.fr

    DocumentRoot /app

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
&amp;lt;/VirtualHost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# conf/apache.conf
&amp;lt;Directory /app/&amp;gt;
    Options -Indexes +FollowSymLinks
    AllowOverride None
    Require all granted
&amp;lt;/Directory&amp;gt;

ServerTokens Prod
ServerSignature Off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; conf/vhost.conf /etc/apache2/sites-available/000-default.conf&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; conf/apache.conf /etc/apache2/conf-available/z-app.conf&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;a2enconf z-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a changé ici la configuration du vhost de base d'Apache (000-default.conf), qui délivre maintenant le contenu du dossier &lt;code&gt;/app&lt;/code&gt;. On configure aussi le dossier &lt;code&gt;/app&lt;/code&gt; pour ne pas autoriser Apache de lister les fichiers d'un répertoire avec &lt;code&gt;Options -Indexes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pour ma part, je n'aime pas configurer mes sites avec un .htaccess (pour des raisons de performance). C'est pourquoi j'ai placé la directive &lt;code&gt;AllowOverride None&lt;/code&gt;. Comme l'image est destinée pour le staging / production, j'ai aussi supprimé les infos sur l'infrastructure utilisée avec les directives &lt;code&gt;ServerTokens Prod&lt;/code&gt; et &lt;code&gt;ServerSignature Off&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;J'ai volontairement nommé la configuration &lt;code&gt;z-app&lt;/code&gt; pour être sûr qu'elle soit chargée en dernier et qu'aucun autre module ne vienne surcharger les instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration de PHP
&lt;/h3&gt;

&lt;p&gt;On va configurer PHP selon les besoins récurrents de nos futures images. Ne mettez pas de valeurs trop importantes ici, si un de vos conteneurs en particulier a besoin d'une configuration plus véloce, vous pourrez override cette configuration de base dans le Dockerfile propre à cette app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; conf/php.ini
&lt;/span&gt;&lt;span class="py"&gt;date.timezone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Europe/Paris&lt;/span&gt;

&lt;span class="py"&gt;opcache.enable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;opcache.enable_cli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;opcache.memory_consumption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;128&lt;/span&gt;
&lt;span class="py"&gt;opcache.revalidate_freq&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;apc.enable_cli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;On&lt;/span&gt;

&lt;span class="py"&gt;upload_max_filesize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;16M&lt;/span&gt;
&lt;span class="py"&gt;post_max_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;16M&lt;/span&gt;

&lt;span class="py"&gt;realpath_cache_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4096k&lt;/span&gt;
&lt;span class="py"&gt;realpath_cache_ttl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;7200&lt;/span&gt;

&lt;span class="py"&gt;display_errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Off&lt;/span&gt;
&lt;span class="py"&gt;display_startup_errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-qy&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    git &lt;span class="se"&gt;\
&lt;/span&gt;    gnupg &lt;span class="se"&gt;\
&lt;/span&gt;    unzip &lt;span class="se"&gt;\
&lt;/span&gt;    zip 
&lt;span class="k"&gt;RUN &lt;/span&gt;docker-php-ext-install &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; opcache pdo_mysql
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; conf/php.ini /usr/local/etc/php/conf.d/app.ini&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adaptez les modules par défaut qui vous semblent essentiels. Pour moi, la quasi totalité de mes projets utilisent PDO et MySQL. OPCache est bien sûr par défaut également.&lt;/p&gt;

&lt;h3&gt;
  
  
  Des jolies pages d'erreur par défaut
&lt;/h3&gt;

&lt;p&gt;Qui ne s'est jamais retrouvé devant la page 404 par défaut d'Apache ? C'est l'assurance de perdre la crédibilité du &lt;a href="https://www.mathieupassenaud.fr/un-developpeur-nest-pas-un-joueur/" rel="noopener noreferrer"&gt;&lt;strong&gt;ninja DevOp big data pro de baby-foot&lt;/strong&gt;&lt;/a&gt; et autres buzzwords que vous êtes auprès de votre client. On va donc modifier la conf existante pour ajouter la gestion des pages d'erreurs :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# conf/apache.conf
# ...
&amp;lt;Directory /errors/&amp;gt;
    Options -Indexes
    AllowOverride None
    Require all granted
&amp;lt;/Directory&amp;gt;

Alias /_errors/ /errors/
ErrorDocument 404 /_errors/404.html
ErrorDocument 403 /_errors/403.html
ErrorDocument 500 /_errors/500.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; errors /errors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pour ne pas causer de conflits avec les futures apps, j'ai placé le contenu des pages d'erreur dans un dossier à part (/errors à la racine du conteneur) et j'isole les requêtes HTTP spécifiques à ces pages via le chemin &lt;code&gt;/_errors&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Vous pouvez récupérer directement le contenu du dossier errors dans &lt;a href="https://github.com/silarhi/docker-php/tree/main/8.3/errors" rel="noopener noreferrer"&gt;le repo Github du tuto&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ça donne quoi au final ?
&lt;/h2&gt;

&lt;p&gt;Le Dockerfile a été optimisé pour réduire la taille des couches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; php:8.3-apache&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; COMPOSER_ALLOW_SUPERUSER=1&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# git, unzip &amp;amp; zip are for composer&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-qy&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    git &lt;span class="se"&gt;\
&lt;/span&gt;    gnupg &lt;span class="se"&gt;\
&lt;/span&gt;    unzip &lt;span class="se"&gt;\
&lt;/span&gt;    zip &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://getcomposer.org/installer | php &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--install-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/bin &lt;span class="nt"&gt;--filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;composer &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# PHP Extensions&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;docker-php-ext-install &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; opcache pdo_mysql
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; conf/php.ini /usr/local/etc/php/conf.d/app.ini&lt;/span&gt;

&lt;span class="c"&gt;# Apache&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; errors /errors&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; conf/vhost.conf /etc/apache2/sites-available/000-default.conf&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; conf/apache.conf /etc/apache2/conf-available/z-app.conf&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; index.php /app/index.php&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;a2enmod rewrite remoteip &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    a2enconf z-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# conf/apache.conf
&amp;lt;Directory /app/&amp;gt;
    Options -Indexes +FollowSymLinks
    AllowOverride None
    Require all granted

    SetEnvIf X_FORWARDED_PROTO https HTTPS=on
&amp;lt;/Directory&amp;gt;

ServerTokens Prod
ServerSignature Off

&amp;lt;Directory /errors/&amp;gt;
    Options -Indexes
    AllowOverride None
    Require all granted
&amp;lt;/Directory&amp;gt;

Alias /_errors/ /errors/
ErrorDocument 404 /_errors/404.html
ErrorDocument 403 /_errors/403.html
ErrorDocument 500 /_errors/500.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# conf/vhost.conf
&amp;lt;VirtualHost *:80&amp;gt;
    ServerAdmin guillaume@silarhi.fr

    DocumentRoot /app

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
&amp;lt;/VirtualHost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; conf/php.ini
&lt;/span&gt;&lt;span class="py"&gt;date.timezone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Europe/Paris&lt;/span&gt;

&lt;span class="py"&gt;opcache.enable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;opcache.enable_cli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;opcache.memory_consumption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;128&lt;/span&gt;
&lt;span class="py"&gt;opcache.revalidate_freq&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;apc.enable_cli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;On&lt;/span&gt;

&lt;span class="py"&gt;upload_max_filesize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;16M&lt;/span&gt;
&lt;span class="py"&gt;post_max_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;16M&lt;/span&gt;

&lt;span class="py"&gt;realpath_cache_size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4096k&lt;/span&gt;
&lt;span class="py"&gt;realpath_cache_ttl&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;7200&lt;/span&gt;

&lt;span class="py"&gt;display_errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Off&lt;/span&gt;
&lt;span class="py"&gt;display_startup_errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Curieux de voir le résultat ? J'ai mis en place rien que pour vous :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/php" rel="noopener noreferrer"&gt;La démo avec un super phpinfo en guise de bonjour&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://labs.silarhi.fr/php/liencass%C3%A9" rel="noopener noreferrer"&gt;Un exemple de page 404&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/silarhi/labs.silarhi.fr/tree/main/php" rel="noopener noreferrer"&gt;Le code source qui gère tout ça en production avec Traefik&lt;/a&gt; (&lt;a href="https://blog.silarhi.fr/docker-compose-traefik-https" rel="noopener noreferrer"&gt;C'est quoi Traefik ?!&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://labs.silarhi.fr" rel="noopener noreferrer"&gt;Une démo plus concrète basée sur Symfony&lt;/a&gt; (&lt;a href="https://blog.silarhi.fr/deploiement-continu-symfony-docker-circleci/" rel="noopener noreferrer"&gt;Comment on fait ça ?!&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ressources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/silarhi/docker-php" rel="noopener noreferrer"&gt;Le code source des images PHP (5.6 à 8.3) sur GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.silarhi.fr/docker-compose-traefik-https/" rel="noopener noreferrer"&gt;Gérez vos images Docker sur votre serveur de production avec Traefik&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/r/silarhi/php-apache" rel="noopener noreferrer"&gt;Le repository dans le Hub Docker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>code</category>
      <category>docker</category>
      <category>php</category>
    </item>
  </channel>
</rss>
