<?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>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\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\HttpFoundation\Session\SessionInterface&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\Annotation\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;SessionInterface&lt;/span&gt; &lt;span class="nv"&gt;$session&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="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="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/experimental_authenticators.html&lt;/span&gt;
    &lt;span class="na"&gt;enable_authenticator_manager&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;# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers&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;encoders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# this internal class is used by Symfony to represent in-memory users&lt;/span&gt;
        &lt;span class="na"&gt;Symfony\Component\Security\Core\User\User&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="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;app_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="n"&gt;request&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;'qrCode'&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;$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;$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="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="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="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. À l'heure où j'écris ces lignes, la version de docker-compose est la &lt;strong&gt;2.3.0&lt;/strong&gt;. 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 &lt;span class="nt"&gt;--version&lt;/span&gt;
&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="nn"&gt;[api]&lt;/span&gt;
  &lt;span class="py"&gt;dashboard&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;insecure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

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

  &lt;span class="nn"&gt;[entryPoints.websecure]&lt;/span&gt;
    &lt;span class="py"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;":443"&lt;/span&gt;
      &lt;span class="nn"&gt;[entryPoints.websecure.http.tls]&lt;/span&gt;
        &lt;span class="py"&gt;certResolver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;

&lt;span class="nn"&gt;[providers]&lt;/span&gt;
  &lt;span class="nn"&gt;[providers.docker]&lt;/span&gt;
    &lt;span class="py"&gt;watch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;exposedByDefault&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="py"&gt;network&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"web"&lt;/span&gt;

&lt;span class="nn"&gt;[certificatesResolvers]&lt;/span&gt;
  &lt;span class="nn"&gt;[certificatesResolvers.default]&lt;/span&gt;
    &lt;span class="nn"&gt;[certificatesResolvers.default.acme]&lt;/span&gt;
      &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"contact@superman.com"&lt;/span&gt;
      &lt;span class="py"&gt;storage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"acme.json"&lt;/span&gt;
      &lt;span class="py"&gt;caServer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://acme-v01.api.letsencrypt.org/directory"&lt;/span&gt;
    &lt;span class="nn"&gt;[certificatesResolvers.default.acme.tlsChallenge]&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 4&lt;/a&gt;) et qui tient en &lt;strong&gt;moins de 25 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:4-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;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;
Le premier label &lt;code&gt;traefik.frontend.enable&lt;/code&gt; définit que Traefik doit manager ce traefik. 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;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--default-authentication-plugin=mysql_native_password&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:v2.4
     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:v2.4
    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 :&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:4-alpine
    container_name: 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`, `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
    command: --default-authentication-plugin=mysql_native_password
    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) et React. 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 Yarn, à 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 : &lt;a href="https://openclassrooms.com/fr/courses/4087056-testez-et-suivez-letat-de-votre-application-php/4419471-en-quoi-consiste-lintegration-continue" rel="noopener noreferrer"&gt;OpenClassrooms&lt;/a&gt; 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.27-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;# 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_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_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 yarn.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/.yarn &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;YARN_CACHE_FOLDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/root/.yarn yarn &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;

&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_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_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 vite.config.js yarn.lock ./&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="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; yarn 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;# 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="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 Yarn 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 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="na"&gt;extra_build_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--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 &lt;code&gt;--build-arg&lt;/code&gt; à &lt;code&gt;docker build&lt;/code&gt;. Ces arguments 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;En 2021, 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 5 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="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;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;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\Routing\Annotation\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;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * @Route("/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="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="nf"&gt;\count&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="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="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\Extension\AbstractExtension&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\TwigFunction&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="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractExtension&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @var UrlGeneratorInterface */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var string */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$secret&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="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="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="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;=&lt;/span&gt; &lt;span class="nv"&gt;$router&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="o"&gt;=&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getFunctions&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TwigFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app_asset'&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="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'appAsset'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'is_safe'&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;'html'&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;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="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="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="s1"&gt;'png'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nb"&gt;substr&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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&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;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 Webpack 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;/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;&lt;/a&gt;&lt;a href="https://hub.packtpub.com/github-parts-ways-with-jquery-adopts-vanilla-js-for-its-frontend/" rel="noopener noreferrer"&gt;&lt;/a&gt;&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 &lt;a href="https://github.com/EnMarche/en-marche.fr/tree/master/front" rel="noopener noreferrer"&gt;la structure frontend du site en-marche.fr&lt;/a&gt; qui est Open Source (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 &lt;a href="https://github.com/EnMarche/en-marche.fr/blob/master/front/components/DataGrid.js#L293-L314" rel="noopener noreferrer"&gt;code écrit avec React&lt;/a&gt; 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 fais 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. Rendez vous sur packagecloud et suivez les instructions selon votre distribution : &lt;a href="https://packagecloud.io/varnishcache" rel="noopener noreferrer"&gt;https://packagecloud.io/varnishcache&lt;/a&gt;.&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 specification a été écrite par Akamai il y a une dizaine d'années 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 plage 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%2F0rggceypvh7ou6gp3pjw.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%2F0rggceypvh7ou6gp3pjw.png" alt="Démo de labs.silarhi.fr/esi" width="800" height="441"&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="s2"&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&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, shrink-to-fit=no"&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://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"&lt;/span&gt; &lt;span class="na"&gt;integrity=&lt;/span&gt;&lt;span class="s"&gt;"sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"&lt;/span&gt; &lt;span class="na"&gt;crossorigin=&lt;/span&gt;&lt;span class="s"&gt;"anonymous"&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&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"public cached p-2"&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&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-md-8 col-lg-9"&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;Page de contenu public&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lead"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    Mise en cache le 
                    &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 badge-secondary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &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;
                    &lt;span class="nt"&gt;&amp;lt;/span&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-md-4 col-lg-3"&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;/div&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.&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;directors&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;# 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="o"&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|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 PHPSESSID 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;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;regsuball&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="s"&gt;"; +"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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;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;regsuball&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="s"&gt;";(PHPSESSID)="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"; &lt;/span&gt;&lt;span class="se"&gt;\1&lt;/span&gt;&lt;span class="s"&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;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;regsuball&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="s"&gt;";[^ ][^;]*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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;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;regsuball&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="s"&gt;"^[; ]+|[; ]+$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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;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;regsuball&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="s"&gt;"^;\s*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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="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="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écéssaire pour les blocs publics.&lt;br&gt;&lt;br&gt;
En PHP, l'utilisateur est lié à la session du serveur via le cookie &lt;code&gt;PHPSESSID&lt;/code&gt;. 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éresse. 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. 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;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;
  
  
  &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="s2"&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"public cached p-2"&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;Widgets publics&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lead"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Mise en cache le 
        &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 badge-secondary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &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;
        &lt;span class="nt"&gt;&amp;lt;/span&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Politique de mise en cache : Contenu publique 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, smax-age=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="nb"&gt;session_start&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="s2"&gt;"X-Reverse-Proxy-TTL: 60"&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;"navbar navbar-expand-lg navbar-light bg-light private cached"&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;"container"&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;"collapse navbar-collapse"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"navbarSupportedContent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-nav mr-auto"&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="k"&gt;isset&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="cp"&gt;?&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-text"&lt;/span&gt;&lt;span class="nt"&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 badge-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;/li&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-item"&lt;/span&gt;&lt;span class="nt"&gt;&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;"nav-link"&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="nt"&gt;&amp;lt;/li&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;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-text"&lt;/span&gt;&lt;span class="nt"&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 badge-danger"&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;/li&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-item"&lt;/span&gt;&lt;span class="nt"&gt;&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;"nav-link"&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="nt"&gt;&amp;lt;/li&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;/ul&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;"navbar-text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Mise en cache le 
                &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 badge-secondary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &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;
                &lt;span class="nt"&gt;&amp;lt;/span&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;/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;PHPSESSID&lt;/code&gt; de l'utilisateur dans le calcul du "hash" d'une requête pour ce bloc-ci, ce que nous avons fait dans la méthode vcl_recv.&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-center p-2 public"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lead text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Heure serveur : 
        &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 badge-secondary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &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;
        &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        (pas de cache)
    &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Politique de mise en cache : Contenu publique 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="nb"&gt;session_start&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;br&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;"private p-2"&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;/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="nb"&gt;session_start&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;301&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rien de particulier ici, on est sur un système de login maison très classique.&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;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="nb"&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="p"&gt;)&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="nb"&gt;session_name&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;"deleted"&lt;/span&gt;&lt;span class="p"&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="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;);&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="nb"&gt;session_destroy&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;301&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;
  
  
  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/master/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/6.3/users-guide/index.html" rel="noopener noreferrer"&gt;La documentation officielle de Varnish&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>
