<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
	<channel>
		<title>Posts on Baptiste Leduc</title>
		<link>/posts/</link>
		<description>Recent content in Posts on Baptiste Leduc</description>
		<generator>Hugo -- 0.157.0</generator>
		<language>en-us</language>
		<lastBuildDate>Tue, 13 Jan 2026 00:00:00 +0000</lastBuildDate>
		<atom:link href="/posts/index.xml" rel="self" type="application/rss+xml" />
		
		
		<item>
			<title>✍️  Microservices et contrats d&#39;API : Jane comme source de vérité</title>
			<link>/posts/2026-01-13-microservices-et-contrat-api-jane-source-verite/</link>
			<pubDate>Tue, 13 Jan 2026 00:00:00 +0000</pubDate><guid>/posts/2026-01-13-microservices-et-contrat-api-jane-source-verite/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p>Disclaimer: This post is a copy of my <a href="https://jolicode.com/blog/microservices-et-contrats-d-api-jane-comme-source-de-verite">JoliCode blog post</a>.</p>
<p>Dans le développement d&rsquo;une API, nous sommes tous confrontés au même défi : maintenir la cohérence entre la documentation et le code.</p>
<p>Qui n&rsquo;a jamais perdu des heures à débugger une erreur parce que le champ <code>user_id</code> était devenu <code>userId</code> dans le code, mais pas dans la documentation ? C&rsquo;est ce qu&rsquo;on appelle le &ldquo;drift&rdquo;. À mesure que le projet évolue, le code change, mais la documentation (OpenAPI, Wiki, Postman) traîne souvent la patte, devenant une source d&rsquo;erreurs plutôt qu&rsquo;une aide.</p>
<p>Et si la solution n&rsquo;était pas de mettre à jour manuellement notre code pour coller à la doc, mais de générer automatiquement notre code <em>à partir</em> de la doc ? C&rsquo;est ici qu&rsquo;intervient <strong>Jane</strong>.</p>
<h2 id="découvrir-jane">Découvrir Jane<a href="#d%c3%a9couvrir-jane" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>En deux mots : Jane est une suite de librairies PHP dont la mission est de générer du code de qualité à partir de vos spécifications <strong><a href="https://json-schema.org/">JSON Schema</a></strong> ou <strong><a href="https://www.openapis.org/">OpenAPI</a></strong>.</p>
<p>Si l&rsquo;on devait résumer son rôle dans une API moderne, Jane agit comme le &ldquo;traducteur automatique&rdquo; entre vos contrats d&rsquo;interface (vos spécifications <code>.yaml</code> ou <code>.json</code>) et votre code PHP. Au lieu d&rsquo;écrire manuellement vos classes, vos validateurs et vos clients HTTP — une tâche répétitive et sujette à l&rsquo;erreur humaine — Jane les fabrique pour vous.</p>
<p>Concrètement, Jane analyse votre schéma et produit :</p>
<ul>
<li><strong>Des Modèles (DTO) :</strong> Des classes PHP simples (POPO - <em>Plain Old PHP Objects</em>) strictement typées qui représentent vos données ;</li>
<li><strong>Des Normalizers :</strong> Toute la logique nécessaire pour transformer ces objets en JSON et inversement, en s&rsquo;appuyant sur le composant <code>symfony/serializer</code> ;</li>
<li><strong>Un Client HTTP complet :</strong> (dans le cas du composant OpenAPI) Une implémentation prête à l&rsquo;emploi (compatible PSR-18) pour consommer l&rsquo;API, gérant les requêtes, les réponses et même les exceptions définies dans votre spec.</li>
</ul>
<p>L&rsquo;atout majeur de Jane n&rsquo;est pas seulement le gain de temps, c&rsquo;est la garantie de <strong>conformité</strong>. Puisque le code est généré directement depuis la source de vérité (le schéma), il est impossible d&rsquo;avoir une divergence (&ldquo;drift&rdquo;) entre ce que votre documentation prétend faire et ce que votre code fait réellement. Si le schéma change, vous régénérez le code, et le tour est joué.</p>
<h2 id="voyons-un-projet-dexemple"><strong>Voyons un projet d’exemple</strong><a href="#voyons-un-projet-dexemple" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Pour passer de la théorie à la pratique, nous allons construire un cas d&rsquo;usage classique : un <strong>tunnel d&rsquo;achat e-commerce</strong>.</p>
<p>Nous allons simuler la transformation d&rsquo;un panier en une commande validée. Pour cela, nous découpons la logique en deux micro-services distincts :</p>
<ol>
<li><strong>Le service Panier</strong> : Il matérialise l&rsquo;état d&rsquo;attente avant la commande. C&rsquo;est lui qui mémorise les articles choisis au fur et à mesure que le client parcourt le site, avant qu&rsquo;il ne décide (ou non) de passer à l&rsquo;achat ;</li>
<li><strong>Le service Commande</strong> : Il gère la finalisation de la vente et la collecte des informations client.</li>
</ol>
<p>Ce scénario nous permettra d&rsquo;explorer des cas concrets :</p>
<ul>
<li><strong>Côté Panier</strong>, nous utiliserons des UUID pour récupérer le contenu du panier ;</li>
<li><strong>Côté Commande</strong>, nous mettrons en place une validation stricte des données (regex pour le téléphone, énumération pour le pays) lors de la transformation du panier en commande, ainsi qu&rsquo;un endpoint pour les codes promo.</li>
</ul>
<p>Voyons maintenant comment formaliser tout cela dans nos contrats d&rsquo;API.</p>
<h2 id="créer-nos-micro-services-1--2--panier"><strong>Créer nos micro-services (1 / 2) : Panier</strong><a href="#cr%c3%a9er-nos-micro-services-1--2--panier" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Commençons par le service le plus simple : le <strong>Panier</strong>.</p>
<p>Ici, nous prenons le parti du <em>Design First</em> : avant d&rsquo;écrire la moindre ligne de PHP, nous allons figer la structure de nos échanges dans un fichier <code>cart.yaml</code>.</p>
<p>Pour ce service, le besoin est basique : nous voulons pouvoir récupérer un panier via son identifiant unique. Voici notre définition en OpenAPI 3.0.3 :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">openapi</span><span class="p">:</span><span class="w"> </span><span class="m">3.0.3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">info</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Service Panier&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">1.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">/carts/{id}:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">get</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Récupérer un panier&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">in</span><span class="p">:</span><span class="w"> </span><span class="l">path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">uuid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Le panier trouvé&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Cart&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">components</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schemas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Cart</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">uuid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">items</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">array</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">items</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span><span class="c"># simplifié pour l&#39;exemple</span><span class="w">
</span></span></span></code></pre></div><h3 id="ce-qu"><strong>Ce qu&rsquo;il faut retenir ici</strong><a href="#ce-qu" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>L&rsquo;utilisation du format <code>uuid</code> sur l&rsquo;identifiant (<code>id</code>) n&rsquo;est pas anodine. Jane exploite cette information pour enrichir le code généré avec des règles de validation strictes, garantissant que la donnée n&rsquo;est pas une simple chaîne de caractères mais bien un UUID valide.</p>
<p>Nous avons posé les bases du Panier. Attaquons-nous maintenant au second morceau du puzzle, qui va nous demander un peu plus de rigueur : le service Commande.</p>
<h2 id="créer-nos-micro-services-2--2--commande"><strong>Créer nos micro-services (2 / 2) : Commande</strong><a href="#cr%c3%a9er-nos-micro-services-2--2--commande" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Passons maintenant aux choses sérieuses avec le service <strong>Commande</strong>. Si le Panier était une simple lecture, la Commande implique de recevoir et valider des données utilisateur critiques.</p>
<p>C&rsquo;est l&rsquo;occasion idéale pour définir des règles strictes directement dans notre fichier <code>order.yaml</code>. Nous allons déclarer deux routes :</p>
<ol>
<li>Une route de création de commande, qui valide l&rsquo;adresse et le téléphone, données indispensables pour assurer la livraison ;</li>
<li>Une route pour appliquer un code de réduction à une commande (qui impactera le prix).</li>
</ol>
<p>Voici à quoi ressemble notre contrat :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">openapi</span><span class="p">:</span><span class="w"> </span><span class="m">3.0.3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">info</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Service Commande&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">1.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">/orders</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">post</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Créer une commande à partir d un panier&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">requestBody</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/OrderInput&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;201&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Commande créée&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Order&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="l">/orders/{id}/discount:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">post</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Ajouter un code promo&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">in</span><span class="p">:</span><span class="w"> </span><span class="l">path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">uuid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">requestBody</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">code</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Code promo appliqué&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">components</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schemas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">OrderInput</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;cartId&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;customer&#39;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">cartId</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">uuid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">customer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/CustomerAddress&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">CustomerAddress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;firstName&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;lastName&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;phoneNumber&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;countryCode&#39;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">firstName</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">lastName</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">phoneNumber</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">pattern</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;^\+33[1-9]\d{8}$&#39;</span><span class="w"> </span><span class="c"># Validation Regex pour numéros français</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description: &#39;Format</span><span class="p">:</span><span class="w"> </span><span class="l">+33xxxxxxxxx&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">countryCode</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">enum</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;FR&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;BE&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;LU&#39;</span><span class="p">]</span><span class="w"> </span><span class="c"># Limitation par énumération</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Order</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">uuid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">enum</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;pending&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;paid&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;shipped&#39;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">price</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">number</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Prix final après réduction éventuelle&#39;</span><span class="w">
</span></span></span></code></pre></div><h3 id="pourquoi-aller-aussi-loin-dans-la-spec-"><strong>Pourquoi aller aussi loin dans la spec ?</strong><a href="#pourquoi-aller-aussi-loin-dans-la-spec-" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Regardez bien le schéma <code>CustomerAddress</code>. Nous n&rsquo;avons pas seulement défini des chaînes de caractères, nous avons imposé des contraintes métiers :</p>
<ul>
<li><code>pattern</code> : Le champ <code>phoneNumber</code> doit correspondre à une regex précise (+33&hellip;).</li>
<li><code>enum</code> : Le champ <code>countryCode</code> ne peut être qu&rsquo;une valeur parmi une liste définie (France, Belgique, Luxembourg).</li>
</ul>
<p>Au lieu de coder ces validations &ldquo;à la main&rdquo; dans chaque contrôleur PHP, nous les déclarons une seule fois dans le contrat. Jane se chargera de traduire ces contraintes dans le code généré, garantissant que si une donnée ne respecte pas la spec, elle ne passera pas.</p>
<p>Nos contrats sont prêts. Il est temps de passer au PHP.</p>
<p><strong>Configuration de Jane pour le projet exemple</strong></p>
<p>Nos spécifications OpenAPI sont prêtes (<code>cart.yaml</code> et <code>order.yaml</code>). Il faut maintenant expliquer à Jane comment les transformer en code PHP.</p>
<p>Cela se passe via un fichier de configuration, généralement nommé <code>.jane-openapi.php</code> à la racine du projet.</p>
<p>Pour gérer nos deux micro-services (Panier et Commande) sans mélanger leur code, nous utilisons l&rsquo;option <code>mapping</code>. Elle permet de définir des règles de génération spécifiques pour chaque fichier OpenAPI au sein d&rsquo;une seule et même configuration.</p>
<p>Voici à quoi ressemble notre fichier <code>.jane-openapi.php</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">return</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;mapping&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/cart.yaml&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;namespace&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;App\Generated\Cart&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;directory&#39;</span> <span class="o">=&gt;</span> <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/generated/Cart&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/order.yaml&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;namespace&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;App\Generated\Order&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;directory&#39;</span> <span class="o">=&gt;</span> <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/generated/Order&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">‘validation’</span> <span class="o">=&gt;</span> <span class="k">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">];</span>
</span></span></code></pre></div><h3 id="les-options-disponibles"><strong>Les options disponibles</strong><a href="#les-options-disponibles" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Déchiffrons ensemble cette configuration :</p>
<ul>
<li><code>mapping</code> : C&rsquo;est la clé maîtresse qui nous permet d&rsquo;associer chaque fichier de spécification (la clé du sous-tableau, ici le chemin vers <code>cart.yaml</code> ou <code>order.yaml</code>) à sa propre configuration.</li>
<li><code>namespace</code> : C&rsquo;est ici que l&rsquo;organisation se joue. En donnant des namespaces différents (<code>App\Generated\Cart</code> vs <code>App\Generated\Order</code>), nous isolons complètement les domaines. Si un modèle s&rsquo;appelle <code>Error</code> dans les deux services, il n&rsquo;y aura aucun conflit de nom de classe dans notre projet.</li>
<li><code>directory</code> : Le dossier de destination où le code PHP généré pour chaque service va être écrit.</li>
<li><code>validation</code> : <strong>C&rsquo;est une option capitale.</strong> En la passant à <code>true</code>, nous demandons à Jane de traduire les contraintes du schéma OpenAPI (comme <code>minimum: 1</code>, <code>required</code>, <code>email</code>) en métadonnées de <strong>Symfony Validator</strong>. C&rsquo;est le socle qui nous permettra de garantir la robustesse des données sans avoir à réécrire manuellement les règles métiers en PHP. Nous verrons plus loin à quel point cela nous simplifie la vie.</li>
</ul>
<p>Avec cette structure, Jane va itérer sur chaque entrée du mapping et produire automatiquement deux clients HTTP (compatibles PSR-18) distincts et autonomes.</p>
<h2 id="et-les-erreurs--standardisation-et-gestion-avec-jane"><strong>Et les erreurs ? Standardisation et gestion avec Jane</strong><a href="#et-les-erreurs--standardisation-et-gestion-avec-jane" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Gérer le &ldquo;happy path&rdquo; (code 200), c&rsquo;est facile. Mais dans la vraie vie, une API échoue : validation invalide (400), accès refusé (403) ou crash serveur (500).</p>
<p>Si nous laissons Jane deviner, elle lèvera des exceptions génériques HTTP. Mais nous pouvons faire mieux : nous pouvons uniformiser le format de nos erreurs pour que notre SDK PHP renvoie des objets structurés, faciles à manipuler dans un <code>try/catch</code>.</p>
<h3 id="définir-un-schéma-d"><strong>Définir un schéma d&rsquo;erreur standard</strong><a href="#d%c3%a9finir-un-sch%c3%a9ma-d" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Dans nos fichiers OpenAPI (<code>cart.yaml</code> et <code>order.yaml</code>), nous allons ajouter une définition réutilisable dans la section <code>components</code>. Cela permet d&rsquo;avoir un format unique (par exemple : un message et un code) pour toutes nos erreurs.</p>
<p>Ajoutons ceci à la fin de nos fichiers YAML :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">components</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schemas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># ... nos autres modèles (Cart, Product...)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Notre modèle d&#39;erreur standardisé</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Error</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">required</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">message</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">code</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">message</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Le message d&#39;erreur pour le développeur</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">code</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">integer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Un code d&#39;erreur interne spécifique</span><span class="w">
</span></span></span></code></pre></div><h3 id="référencer-ce-schéma-dans-les-endpoints"><strong>Référencer ce schéma dans les endpoints</strong><a href="#r%c3%a9f%c3%a9rencer-ce-sch%c3%a9ma-dans-les-endpoints" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Maintenant que le modèle <code>Error</code> existe, nous devons dire à chaque endpoint : &ldquo;Si nous renvoyons une 400, 403 ou 500, le corps de la réponse respectera ce schéma&rdquo;.</p>
<p>Reprenons l&rsquo;exemple de l&rsquo;ajout d&rsquo;un code de réduction sur une commande:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">/orders/{id}/discount:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">post</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Ajouter un code promo&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># ... (requestBody, etc)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">responses</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Code promo appliqué&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Order&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Gestion des erreurs standardisée</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;400&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description: &#39;Données invalides (ex</span><span class="p">:</span><span class="w"> </span><span class="l">code non trouvé)&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Error&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;403&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Action non autorisée</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Error&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;404&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Commande introuvable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Error&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">&#39;500&#39;</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Erreur serveur</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">content</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">application/json</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">schema</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Error&#39;</span><span class="w">
</span></span></span></code></pre></div><h3 id="ce-que-jane-va-générer-pour-nous"><strong>Ce que Jane va générer pour nous</strong><a href="#ce-que-jane-va-g%c3%a9n%c3%a9rer-pour-nous" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>C&rsquo;est ici que la magie opère. Lors de la génération, Jane va détecter ces configurations et produire deux choses très utiles :</p>
<ol>
<li><strong>Un <!-- raw HTML omitted -->POPO<!-- raw HTML omitted --></strong> <code>Error</code> : Une classe PHP classique (<code>App\Generated\Order\Model\Error</code>) avec ses getters et setters.</li>
<li><strong>Des Exceptions spécifiques</strong> : Pour l&rsquo;endpoint ci-dessus, Jane va générer des exceptions comme <code>AddDiscountBadRequestException</code> ou <code>AddDiscountNotFoundException</code>.</li>
</ol>
<p>Ces exceptions auront une méthode <code>getError()</code> qui retournera&hellip; notre instance du modèle <code>Error</code> remplie avec les données de l&rsquo;API !</p>
<p>Cela permet d&rsquo;écrire un code client très propre :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$api</span><span class="o">-&gt;</span><span class="na">addDiscount</span><span class="p">(</span><span class="s1">&#39;C17891&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">OrdersIdDiscountPostBody</span><span class="p">(</span><span class="s1">&#39;NOEL2025&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">AddDiscountBadRequestException</span><span class="o">|</span><span class="nx">AddDiscountNotFoundException</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// On récupère notre objet Error proprement
</span></span></span><span class="line"><span class="cl">    <span class="nv">$errorModel</span> <span class="o">=</span> <span class="nv">$e</span><span class="o">-&gt;</span><span class="na">getResponse</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">echo</span> <span class="s2">&#34;Erreur fonctionnelle : &#34;</span> <span class="o">.</span> <span class="nv">$errorModel</span><span class="o">-&gt;</span><span class="na">getMessage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">ClientExceptionInterface</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Erreur réseau ou autre
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>En standardisant vos retours d&rsquo;erreurs dans l&rsquo;OpenAPI, vous garantissez que le code PHP généré est prévisible et facile à utiliser pour les développeurs qui consommeront votre SDK.</p>
<h2 id="lancer-la-génération-du-code-"><strong>Lancer la génération du code !</strong><a href="#lancer-la-g%c3%a9n%c3%a9ration-du-code-" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>C&rsquo;est ici que la magie opère. Fini d&rsquo;écrire des DTOs, des clients HTTP et des exceptions à la main. Jane va faire tout ce travail fastidieux pour nous.</p>
<p>Dans votre terminal, à la racine du projet, lancez simplement :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vendor/bin/jane-openapi generate --config-file .jane-openapi.php
</span></span></code></pre></div><p>Jane va lire le fichier <code>.jane-openapi.php</code>, détecter notre mapping et générer les fichiers correspondants. Si vous allez voir dans votre dossier <code>generated/</code>, nous avons deux nouveaux dossiers qui sont apparus : <code>Cart</code> et <code>Order</code>.</p>
<h3 id="une-étape-indispensable--l"><strong>Une étape indispensable : l&rsquo;autoloader</strong><a href="#une-%c3%a9tape-indispensable--l" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Avoir les fichiers PHP, c&rsquo;est bien, mais pouvoir les utiliser, c&rsquo;est mieux ! Comme ce sont de nouvelles classes dans un nouveau dossier, Composer ne les connaît pas encore.</p>
<p>Pour que Composer puisse charger ces nouvelles classes, nous devons mettre à jour le <code>composer.json</code>. Plutôt que de déclarer chaque service un par un, allons au plus simple : nous allons faire pointer le namespace parent <code>App\Generated\</code> directement vers le dossier <code>generated/</code>.</p>
<p>Ouvrez votre <code>composer.json</code> et ajoutez ces lignes dans la section <code>autoload</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;autoload&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;psr-4&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;App\\&#34;</span><span class="p">:</span> <span class="s2">&#34;src/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;App\\Generated\\&#34;</span><span class="p">:</span> <span class="s2">&#34;generated/&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span><span class="err">,</span>
</span></span></code></pre></div><p>Ensuite, pour que cette modification soit prise en compte, régénérez l&rsquo;autoloader :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">composer dump-autoload
</span></span></code></pre></div><p>C&rsquo;est tout ! Votre projet est maintenant prêt à utiliser les librairies générées.</p>
<h3 id="que-contient-ce-dossier-generated-"><strong>Que contient ce dossier generated ?</strong><a href="#que-contient-ce-dossier-generated-" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Si vous êtes curieux (et vous devriez l&rsquo;être), jetez un œil à l&rsquo;intérieur de <code>generated/Cart</code> ou <code>generated/Order</code>. Vous y trouverez une structure très organisée :</p>
<pre tabindex="0"><code>generated/
├── Order
│   ├── Client.php
│   ├── Endpoint
│   ├── Exception
│   ├── Model
│   ├── Normalizer
│   ├── Runtime
│   │   ├── Client
│   │   └── Normalizer
│   └── Validator
└── Cart
    ├── Same file structure as Order
    └── ...
</code></pre><ul>
<li><code>Client.php</code> : Votre <strong>point d&rsquo;entrée</strong> principal. C&rsquo;est la classe qui contient toutes les méthodes comme <code>createOrder()</code>, <code>addDiscount()</code>, etc.</li>
<li><code>Endpoint/</code> :  Chaque opération définie dans votre schéma OpenAPI (comme <code>GET /carts/{id}</code>) possède ici sa propre classe dédiée. C&rsquo;est elle qui orchestre l&rsquo;appel API : elle sait quelle méthode HTTP utiliser, quel corps de requête envoyer, comment transformer la réponse brute en un objet de votre <code>Model</code> et comment gérer les exceptions selon le code de retour.</li>
<li><code>Exception/</code> : C&rsquo;est ici que se trouvent nos <strong>erreurs typées</strong> (<code>AddDiscountBadRequestException</code>, etc.) basées sur les codes HTTP et nos schémas d&rsquo;erreurs.</li>
<li><code>Model/</code> : Vos <strong>POPO</strong> (Plain Old PHP Objects). Ce sont les classes <code>Cart</code>, <code>Order</code>, <code>OrderInput</code>, etc. Elles contiennent simplement des propriétés, des getters et des setters.</li>
<li><code>Normalizer/</code> : C&rsquo;est le cœur de Jane (basé sur le composant <strong>Serializer</strong> de Symfony). Ces classes savent comment transformer vos objets en JSON et inversement.</li>
<li><code>Runtime/</code> : C&rsquo;est la salle des machines. Ce dossier contient le code &ldquo;support&rdquo; nécessaire au fonctionnement global du SDK généré (configuration du client, normalizers de base). Ce sont des composants techniques qui font le lien entre votre code généré et les librairies tierces (comme le client HTTP PSR-18).</li>
<li><code>Validator/</code> : C&rsquo;est la particularité liée à notre option <code>&quot;validation&quot;: true</code>. Jane génère des classes de contraintes spécifiques (ex: <code>OrderInputConstraint</code>).</li>
<li><em>Détail technique :</em> Ces classes étendent <code>Compound</code> de Symfony Validator. Elles regroupent toutes les règles définies dans votre OpenAPI (<code>Required</code>, <code>Email</code>, <code>Regex</code> etc.) en une seule classe appelable. Cela garde vos modèles propres tout en garantissant une validation stricte.</li>
</ul>
<h2 id=""><a href="#" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<h2 id="utilisation-pratique--intégrer-les-modèles-jane-dans-votre-code"><strong>Utilisation pratique : intégrer les modèles Jane dans votre code</strong><a href="#utilisation-pratique--int%c3%a9grer-les-mod%c3%a8les-jane-dans-votre-code" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Maintenant que nos classes sont générées, comment les utiliser concrètement ?</p>
<p>Jane brille par sa rigueur : elle sécurise les échanges via ses Normalizers et force l&rsquo;utilisation d&rsquo;objets typés.</p>
<h3 id="la-validation-automatique">La validation automatique<a href="#la-validation-automatique" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>L&rsquo;un des atouts de Jane est qu&rsquo;elle intègre la validation directement dans le processus de <strong>sérialisation et de désérialisation</strong>.</p>
<p>Cela signifie que le contrôle des données est actif dans les deux sens :</p>
<ol>
<li><strong>En entrée (Désérialisation)</strong> : Vous ne pouvez pas créer un objet PHP invalide à partir d&rsquo;un JSON reçu.</li>
<li><strong>En sortie (Sérialisation)</strong> : Vous ne pouvez pas générer un JSON invalide à partir d&rsquo;un objet PHP (par exemple, si vous avez oublié de remplir un champ obligatoire avant de renvoyer la réponse).</li>
</ol>
<p>Vous n&rsquo;avez donc plus besoin d&rsquo;appeler le validateur manuellement. Regardez le code généré dans le <code>Normalizer</code> (ici <code>OrdersIdDiscountPostBodyNormalizer</code>) : il vérifie les contraintes <strong>pendant</strong> la transformation.</p>
<p>Si vous utilisez le Serializer de Symfony dans votre contrôleur, la validation est implicite :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">namespace</span> <span class="nx">App\Controller</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">App\Generated\Order\Model\OrdersIdDiscountPostBody</span><span class="p">;</span> <span class="c1">// Le DTO généré
</span></span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Symfony\Component\HttpFoundation\Request</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Symfony\Component\HttpFoundation\Response</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Symfony\Component\Serializer\SerializerInterface</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Symfony\Component\Serializer\Exception\ValidationFailedException</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">OrderController</span> <span class="k">extends</span> <span class="nx">AbstractController</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">addDiscount</span><span class="p">(</span><span class="nx">Request</span> <span class="nv">$request</span><span class="p">,</span> <span class="nx">SerializerInterface</span> <span class="nv">$serializer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// C&#39;est ici que la magie opère :
</span></span></span><span class="line"><span class="cl">            <span class="c1">// Jane désérialise ET valide en même temps.
</span></span></span><span class="line"><span class="cl">            <span class="c1">// Si le JSON est invalide (code manquant, trop court...), une exception est levée.
</span></span></span><span class="line"><span class="cl">            <span class="sd">/** @var OrdersIdDiscountPostBody $body */</span>
</span></span><span class="line"><span class="cl">            <span class="nv">$body</span> <span class="o">=</span> <span class="nv">$serializer</span><span class="o">-&gt;</span><span class="na">deserialize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$request</span><span class="o">-&gt;</span><span class="na">getContent</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                <span class="nx">OrdersIdDiscountPostBody</span><span class="o">::</span><span class="na">class</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s1">&#39;json&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">ValidationFailedException</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// On récupère les violations pour répondre une 400 propre via la constante Symfony
</span></span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">json</span><span class="p">(</span><span class="nv">$e</span><span class="o">-&gt;</span><span class="na">getViolations</span><span class="p">(),</span> <span class="nx">Response</span><span class="o">::</span><span class="na">HTTP_BAD_REQUEST</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Si on arrive ici, $body est un objet valide et typé !
</span></span></span><span class="line"><span class="cl">        <span class="nv">$code</span> <span class="o">=</span> <span class="nv">$body</span><span class="o">-&gt;</span><span class="na">getCode</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// ... traitement métier
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>Le gain ?</strong> Votre code métier ne manipule jamais de données &ldquo;sales&rdquo;. Et inversement, vous avez la certitude absolue que vos réponses API respectent toujours le contrat défini dans votre fichier YAML.</p>
<h3 id="gérer-les-erreurs-exceptions-typées">Gérer les erreurs (Exceptions typées)<a href="#g%c3%a9rer-les-erreurs-exceptions-typ%c3%a9es" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>C&rsquo;est ici que notre travail sur les schémas d&rsquo;erreurs <code>400</code> / <code>404</code> / <code>500</code> porte ses fruits. Jane a généré des exceptions spécifiques pour chaque cas d&rsquo;erreur documenté.</p>
<p>Fini les vérifications manuelles du status code, place aux <code>try/catch</code> explicites :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">App\Generated\Order\Exception\AddDiscountBadRequestException</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">App\Generated\Order\Exception\AddDiscountNotFoundException</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">App\Generated\Order\Model\OrdersIdDiscountPostBody</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$payload</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">OrdersIdDiscountPostBody</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$payload</span><span class="o">-&gt;</span><span class="na">setCode</span><span class="p">(</span><span class="s1">&#39;INVALID&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$apiClient</span><span class="o">-&gt;</span><span class="na">addDiscount</span><span class="p">(</span><span class="s1">&#39;order-12345&#39;</span><span class="p">,</span> <span class="nv">$payload</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">AddDiscountBadRequestException</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// C&#39;est une 400 spécifique à cette route (Response::HTTP_BAD_REQUEST)
</span></span></span><span class="line"><span class="cl">    <span class="c1">// On récupère notre objet Error structuré défini dans l&#39;OpenAPI
</span></span></span><span class="line"><span class="cl">    <span class="nv">$errorModel</span> <span class="o">=</span> <span class="nv">$e</span><span class="o">-&gt;</span><span class="na">getError</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Affiche : &#34;Code promo invalide ou expiré&#34;
</span></span></span><span class="line"><span class="cl">    <span class="nv">$logger</span><span class="o">-&gt;</span><span class="na">warning</span><span class="p">(</span><span class="nv">$errorModel</span><span class="o">-&gt;</span><span class="na">getMessage</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">AddDiscountNotFoundException</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// C&#39;est une 404 spécifique (Commande ou code de réduction introuvable)
</span></span></span><span class="line"><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">\Exception</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Erreur réseau ou 500 générique
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="communication-inter-services--quand-le-service-commande-appelle-le-service-panier">Communication inter-services : Quand le service Commande appelle le service Panier<a href="#communication-inter-services--quand-le-service-commande-appelle-le-service-panier" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Dans une architecture microservices, la communication synchrone (HTTP) est souvent le talon d&rsquo;Achille. On se retrouve vite avec des appels <code>curl</code> ou <code>Guzzle</code> éparpillés, des tableaux associatifs non typés et, surtout, une confiance aveugle envers le service appelé.</p>
<p>Maintenant, nous allons voir comment Jane sécurise l&rsquo;échange entre notre <strong>service Commande</strong> (le consommateur) et le <strong>service Panier</strong> (le producteur).</p>
<h3 id="le-contrat-comme-dépendance--réutiliser-le-schéma-openapi">Le contrat comme dépendance : Réutiliser le schéma OpenAPI<a href="#le-contrat-comme-d%c3%a9pendance--r%c3%a9utiliser-le-sch%c3%a9ma-openapi" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Au lieu de redéfinir manuellement une classe <code>PanierDTO</code> dans le service Commande (qui finirait inévitablement par diverger de la réalité), nous utilisons directement la définition OpenAPI du service Panier.</p>
<p>Concrètement, cela signifie que le fichier <code>openapi.yaml</code> du service Panier devient une &ldquo;dépendance&rdquo; du service Commande.</p>
<ul>
<li><strong>Configuration Jane</strong> : Dans le service Commande, nous configurons Jane pour pointer vers ce fichier (via une URL, un submodule git, …).</li>
<li><strong>Single Source of Truth</strong> : Si l&rsquo;équipe Panier met à jour son modèle (par exemple, en ajoutant un champ <code>promoCode</code>), le service Commande le saura dès la prochaine régénération du code.</li>
</ul>
<h3 id="génération-dun-client-http-typé-sdk-interne">Génération d&rsquo;un Client HTTP typé (SDK interne)<a href="#g%c3%a9n%c3%a9ration-dun-client-http-typ%c3%a9-sdk-interne" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>C&rsquo;est une des fonctionnalités clé de Jane pour la communication inter-services. En plus des modèles (DTOs), Jane a généré un Client HTTP complet prêt à l&rsquo;emploi (compatible PSR-18).</p>
<p>Pour l&rsquo;utiliser, on va instancier le client via sa méthode statique <code>create()</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">App\Generated\Panier\Client</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$panierClient</span> <span class="o">=</span> <span class="nx">Client</span><span class="o">::</span><span class="na">create</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nv">$panier</span> <span class="o">=</span> <span class="nv">$panierClient</span><span class="o">-&gt;</span><span class="na">getPanier</span><span class="p">(</span><span class="nv">$uuid</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// $panier est une instance de la classe \App\Generated\Panier\Model\Panier
</span></span></span><span class="line"><span class="cl"><span class="c1">// L&#39;IDE connaît toutes les propriétés :
</span></span></span><span class="line"><span class="cl"><span class="nv">$total</span> <span class="o">=</span> <span class="nv">$panier</span><span class="o">-&gt;</span><span class="na">getTotal</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">foreach</span> <span class="p">(</span><span class="nv">$panier</span><span class="o">-&gt;</span><span class="na">getItems</span><span class="p">()</span> <span class="k">as</span> <span class="nv">$item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Nous manipulons des objets PHP natifs, typés, générés spécifiquement pour cette interaction.</p>
<h3 id="validation-automatique-de-la-réponse">Validation automatique de la réponse<a href="#validation-automatique-de-la-r%c3%a9ponse" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Que se passe-t-il si le service Panier a un bug et renvoie un prix sous forme de chaîne de caractères <code>10,50€</code> au lieu d&rsquo;un nombre <code>10.50</code>, ou s&rsquo;il manque un champ obligatoire ?</p>
<p>Avec un client HTTP manuel, votre code planterait probablement plus loin, de manière obscure (<code>Call to member function on null</code> ou erreur de calcul), ou pire, corromprait vos données silencieusement.</p>
<p>Avec le client généré par Jane :</p>
<ul>
<li>Le client valide <strong>automatiquement</strong> la réponse HTTP reçue par rapport au schéma OpenAPI ;</li>
<li>Si la réponse ne respecte pas le contrat (type incorrect, champ manquant), Jane lève immédiatement une <code>UnexpectedValueException</code> ou une <code>InvalidResponseException</code> ;</li>
<li><strong>Fail Fast</strong> : L&rsquo;application s&rsquo;arrête net à la frontière du service, empêchant des données invalides de polluer votre logique métier de commande.</li>
</ul>
<p>En résumé, Jane transforme un appel HTTP incertain en un appel de méthode PHP sûr et typé.</p>
<h2 id="validation-stricte-dans-les-tests">Validation stricte dans les tests<a href="#validation-stricte-dans-les-tests" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Au lieu de charger vos données de test et ne jamais vérifier leur intégrité, passez-les dans la moulinette de Jane.</p>
<p>Si votre donnée ne correspond plus à la réalité de l&rsquo;API (changement de type, champ manquant), Jane le détecte immédiatement.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// 1. On charge la donnée brute (ici on prends un JSON en exemple
</span></span></span><span class="line"><span class="cl"><span class="c1">// mais un endpoint API fonctionnera pareil
</span></span></span><span class="line"><span class="cl"><span class="nv">$data</span> <span class="o">=</span> <span class="nx">json_decode</span><span class="p">(</span><span class="nx">file_get_contents</span><span class="p">(</span><span class="s1">&#39;panier_fixture.json&#39;</span><span class="p">),</span> <span class="k">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 2. On demande à Jane de créer l&#39;objet.
</span></span></span><span class="line"><span class="cl"><span class="c1">// C&#39;est ici que la magie opère : Jane vérifie TOUT le contrat via la validation
</span></span></span><span class="line"><span class="cl"><span class="c1">// dans le Normalizer généré
</span></span></span><span class="line"><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$panier</span> <span class="o">=</span> <span class="nv">$serializer</span><span class="o">-&gt;</span><span class="na">denormalize</span><span class="p">(</span><span class="nv">$data</span><span class="p">,</span> <span class="nx">Panier</span><span class="o">::</span><span class="na">class</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">Exception</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Le test échoue immédiatement si le JSON est périmé !
</span></span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">fail</span><span class="p">(</span><span class="s1">&#39;Vos données de test ne respectent plus le contrat OpenAPI : &#39;</span> <span class="o">.</span> <span class="nv">$e</span><span class="o">-&gt;</span><span class="na">getMessage</span><span class="p">());</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 3. On utilise un objet PHP valide pour configurer le mock
</span></span></span><span class="line"><span class="cl"><span class="c1">// ou autre test que vous voudriez faire
</span></span></span><span class="line"><span class="cl"><span class="nv">$mockService</span><span class="o">-&gt;</span><span class="na">method</span><span class="p">(</span><span class="s1">&#39;getPanier&#39;</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">willReturn</span><span class="p">(</span><span class="nv">$panier</span><span class="p">);</span>
</span></span></code></pre></div><p>Pourquoi c&rsquo;est puissant ?</p>
<p>Cela transforme vos tests unitaires en tests de contrat légers. Vous n&rsquo;avez plus peur que vos tests &ldquo;passent au vert&rdquo; alors qu&rsquo;ils utilisent des données obsolètes qui feront planter la production.</p>
<h2 id="évolution-et-versioning--faire-évoluer-votre-api-sans-tout-casser">Évolution et versioning : Faire évoluer votre API sans tout casser<a href="#%c3%a9volution-et-versioning--faire-%c3%a9voluer-votre-api-sans-tout-casser" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Une API n&rsquo;est jamais figée. Elle vit, elle change, elle s&rsquo;étend. Le cauchemar de tout développeur est de déployer une mise à jour qui casse la communication entre les services. Avec Jane, l&rsquo;impact d&rsquo;une modification du contrat OpenAPI devient immédiatement visible dans votre code PHP.</p>
<h3 id="ajout-dun-champ-optionnel-non-breaking-change">Ajout d&rsquo;un champ optionnel (Non-breaking change)<a href="#ajout-dun-champ-optionnel-non-breaking-change" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>C&rsquo;est le scénario idéal. Vous ajoutez, par exemple, une <code>description</code> facultative à votre objet <code>Commande</code>.</p>
<ul>
<li><strong>Dans l&rsquo;OpenAPI :</strong> Vous ajoutez le champ sans le marquer comme <code>required</code>.</li>
<li><strong>Après régénération Jane :</strong> Votre classe <code>Commande</code> gagne une propriété <code>protected ?string $description = null;</code> ainsi que ses getters et setters.</li>
<li><strong>Impact :</strong> <strong>Nul.</strong> Votre code existant continue de fonctionner parfaitement car le constructeur reste compatible (le nouveau champ est initialisé à <code>null</code> par défaut). C&rsquo;est une évolution en douceur.</li>
</ul>
<h3 id="ajout-dun-champ-obligatoire-breaking-change">Ajout d&rsquo;un champ obligatoire (Breaking change)<a href="#ajout-dun-champ-obligatoire-breaking-change" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>C&rsquo;est ici que Jane vous sauve la mise. Imaginons que le service Panier exige désormais un <code>customerId</code> pour chaque création de panier.</p>
<ul>
<li><strong>Dans l&rsquo;OpenAPI :</strong> Le champ <code>customerId</code> est ajouté à la liste <code>required</code>.</li>
<li><strong>Après régénération Jane :</strong> La signature du constructeur de la classe <code>Panier</code> change radicalement.
<ul>
<li><em>Avant :</em> <code>public function __construct(string $uuid)</code></li>
<li><em>Après :</em> <code>public function __construct(string $uuid, string $customerId)</code></li>
</ul>
</li>
<li><strong>Impact :</strong> <strong>Immédiat et visible.</strong> Partout dans votre code où vous instanciez un <code>Panier</code> sans ce nouvel argument, <strong>votre code ne fonctionne plus</strong>. Votre IDE (PhpStorm, VSCode) surligne les erreurs en rouge avant même que vous ne lanciez le moindre test.</li>
</ul>
<p><strong>Note clé :</strong> Jane transforme une erreur de &ldquo;runtime&rdquo; (qui arriverait en production) en une erreur de &ldquo;compile-time&rdquo; (qui arrive sur votre machine).</p>
<h3 id="régénération-et-impact-sur-le-code--la-boucle-de-sécurité">Régénération et impact sur le code : La boucle de sécurité<a href="#r%c3%a9g%c3%a9n%c3%a9ration-et-impact-sur-le-code--la-boucle-de-s%c3%a9curit%c3%a9" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Le workflow de mise à jour devient alors très sécurisant :</p>
<ol>
<li><strong>Mise à jour du contrat :</strong> Vous récupérez la nouvelle version de <code>openapi.yaml</code>.</li>
<li><strong>Régénération :</strong> Vous lancez la génération du code Jane.</li>
<li><strong>Analyse d&rsquo;impact :</strong> Vous lancez l&rsquo;analyse statique (PHPStan, Psalm) ou simplement vos tests.</li>
<li><strong>Correction :</strong> Jane vous a forcé à voir tous les endroits du code impactés par le changement. Vous ne pouvez pas &ldquo;oublier&rdquo; de traiter le nouveau champ obligatoire.</li>
</ol>
<p>En résumé, Jane agit comme un <strong>révélateur de dette technique</strong> immédiat lors des mises à jour d&rsquo;API.</p>
<h2 id="conclusion--pourquoi-passer-à-lapproche-contract-first-avec-jane-">Conclusion : Pourquoi passer à l&rsquo;approche &ldquo;Contract-First&rdquo; avec Jane ?<a href="#conclusion--pourquoi-passer-%c3%a0-lapproche-contract-first-avec-jane-" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Au fil de cet article, nous avons vu que Jane n&rsquo;est pas simplement un générateur de code, c&rsquo;est un changement de paradigme dans la façon de concevoir la communication entre vos services. En plaçant le schéma OpenAPI au centre du jeu, vous gagnez sur quatre tableaux majeurs :</p>
<h3 id="type-safety--larmure-de-votre-code">Type Safety : L&rsquo;armure de votre code<a href="#type-safety--larmure-de-votre-code" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Fini les array mystérieux qui circulent d&rsquo;un service à l&rsquo;autre. Avec Jane, chaque donnée entrante ou sortante est encapsulée dans un objet PHP fortement typé.</p>
<p>Vous savez exactement ce que vous manipulez : des entiers sont des int, des dates sont des \DateTime. PHPStan et votre IDE vous remercient, et votre code devient auto-documenté par sa structure même.</p>
<h3 id="documentation-vivante">Documentation Vivante<a href="#documentation-vivante" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Souvent, la documentation est le parent pauvre du projet : écrite au début, jamais mise à jour, et finalement trompeuse.</p>
<p>Ici, la spécification OpenAPI est votre Source de Vérité. Puisque c&rsquo;est elle qui génère le code qui tourne en production, elle est de facto toujours à jour.</p>
<p>Couplée à des outils de visualisation comme <strong>SwaggerUI</strong> ou <strong>ReDoc</strong>, cette documentation devient interactive et fiable pour toutes les équipes (frontend, mobile, partenaires). Ce que vous voyez dans la doc est exactement ce que le code attend.</p>
<h3 id="moins-de-bugs-en-production">Moins de Bugs en Production<a href="#moins-de-bugs-en-production" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>En transformant les erreurs de runtime (données mal formées, champs manquants) en erreurs de &ldquo;compile-time&rdquo; (le code généré change, l&rsquo;IDE signale l&rsquo;erreur), vous capturez les bugs le plus tôt possible.</p>
<p>La validation automatique des réponses agit comme un filet de sécurité : aucune donnée corrompue ne peut pénétrer silencieusement dans votre système pour causer des erreurs en cascade plus loin.</p>
<h3 id="une-dx-retrouvée">Une DX retrouvée<a href="#une-dx-retrouv%c3%a9e" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>C&rsquo;est peut-être le point le plus important au quotidien. Plus besoin de faire des allers-retours incessants entre le code et une documentation PDF externe.</p>
<ul>
<li>L&rsquo;autocomplétion de l&rsquo;IDE fonctionne instantanément.</li>
<li>Les refactorings sont sûrs.</li>
<li>Les tests sont plus simples à écrire et plus robustes.</li>
</ul>
<h3 id="performance-accrue">Performance accrue<a href="#performance-accrue" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Au-delà de la sécurité et du confort, le gain de performance est significatif. Le code généré par Jane étant spécifique à vos modèles, il est beaucoup plus performant que l&rsquo;utilisation générique de l&rsquo;<code>ObjectNormalizer</code> du Serializer de Symfony. En évitant d&rsquo;utiliser la Reflection lors de l&rsquo;exécution de votre code, vos applications consomment moins de ressources et répondent plus vite.</p>
<p><!-- raw HTML omitted -->Le mot de la fin :<!-- raw HTML omitted -->
Arrêtez d&rsquo;écrire vos clients HTTP et vos DTOs à la main. Laissez Jane faire, de manière optimisée, le travail répétitif et concentrez-vous sur ce qui a de la valeur : votre logique métier.</p>
]]></content>
		</item>
		
		<item>
			<title>✍️  À la découverte de PIE, l’alternative moderne à PECL pour les extensions PHP</title>
			<link>/posts/2025-04-11-a-la-decouverte-de-pie/</link>
			<pubDate>Fri, 11 Apr 2025 00:00:00 +0000</pubDate><guid>/posts/2025-04-11-a-la-decouverte-de-pie/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p>Disclaimer: This post is a copy of my <a href="https://jolicode.com/blog/a-la-decouverte-de-pie-lalternative-moderne-a-pecl-pour-les-extensions-php">JoliCode blog post</a>.</p>
<p>Récemment vous avez peut-être entendu parler de <strong>PIE</strong>, un nouveau binaire pour PHP. PIE c’est le diminutif de <strong>“PHP Installer for Extensions”</strong> et c’est donc le descendant de PECL.</p>
<h2 id="pourquoi-pie-">Pourquoi PIE ?<a href="#pourquoi-pie-" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>PHP, né en 1995, célèbre cette année ses 30 ans d&rsquo;existence 🎉. Durant ces trois décennies, l&rsquo;écosystème de ce langage s&rsquo;est considérablement enrichi avec l&rsquo;émergence d&rsquo;outils devenus indispensables aux développeurs.</p>
<p>En 1999, <strong>PEAR</strong> (PHP Extension and Application Repository) a posé les bases de la réutilisation de code en servant de premier gestionnaire de bibliothèques standardisés.</p>
<p>Puis en 2003, <strong>PECL</strong> (PHP Extension Community Library) a étendu les capacités du langage en facilitant l&rsquo;intégration d&rsquo;extensions compilées pour traiter efficacement des données complexes comme le XML ou les chaînes de caractères multioctets (mbstring).</p>
<p>La véritable révolution est survenue en 2012 avec Composer, qui a transformé radicalement la gestion des dépendances dans les projets PHP.</p>
<p>Plus récemment, après plus de 25 ans d&rsquo;existence de PECL, <strong>PIE</strong> (PHP Install Extensions) a fait son apparition comme alternative prometteuse. Encore en phase de développement, cette initiative, lancée par la <strong>PHP Foundation</strong>, apporte une approche moderne et simplifiée pour l&rsquo;installation et la gestion des extensions PHP. Son utilisation actuelle, bien que limitée par sa maturité, s’illustre déjà par sa simplicité que nous allons voir ensemble.</p>
<p>D&rsquo;ici quelque temps, vous installerez vos extensions PHP de cette façon.</p>
<h2 id="mise-en-place">Mise en place<a href="#mise-en-place" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Avant de commencer l&rsquo;installation de PIE, vous devez préparer votre environnement avec quelques outils de développement essentiels. Utilisez la commande suivante pour installer les pré-requis nécessaires (pour linux) :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install git autoconf automake libtool m4 make gcc
</span></span></code></pre></div><p>Puis l&rsquo;installation de l’outil est remarquablement simple, ne nécessitant qu&rsquo;une seule ligne de commande (oui c&rsquo;est un PHAR, comme Composer ou <a href="https://castor.jolicode.com/">Castor</a>) :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo curl -L --output /usr/local/bin/pie https://github.com/php/pie/releases/latest/download/pie.phar <span class="o">&amp;&amp;</span> sudo chmod +x /usr/local/bin/pie
</span></span></code></pre></div><p>Cette méthode d’installation est adaptée pour les distributions Linux. Si vous utilisez autre chose, vous pouvez voir dans la documentation de PIE <a href="https://github.com/php/pie/blob/main/docs/usage.md#installing-pie">pour les méthodes alternatives d’installation</a></p>
<p>Et voilà, avec ces deux commandes vous avez l’outil installé dans sa dernière version et prêt à être utilisé ! 🎉</p>
<h2 id="première-utilisation">Première utilisation<a href="#premi%c3%a8re-utilisation" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Après l&rsquo;installation, vous pouvez rapidement enrichir votre environnement PHP avec de nouvelles extensions.
Commençons par un exemple pratique. Supposons que vous souhaitiez ajouter l&rsquo;extension <code>uuid</code> pour gérer des identifiants uniques dans votre projet, alors il vous suffira de faire la commande suivante :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie install pecl/uuid
</span></span></code></pre></div><p>Selon votre niveau de privilèges sur ce système Linux, le programme pourrait vous demander d&rsquo;utiliser la commande sudo pour obtenir les droits nécessaires à l&rsquo;installation de l&rsquo;extension demandée.
Suite à cette installation, l’extension sera disponible globalement sur l&rsquo;ensemble du système et non limitée au projet actuel.
Vous pouvez découvrir toutes les extensions disponibles sur <a href="https://packagist.org/extensions"><strong>packagist</strong></a>.</p>
<h2 id="diverses-commandes">Diverses commandes<a href="#diverses-commandes" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>L&rsquo;outil propose une panoplie de commandes pour gérer vos extensions PHP. Par exemple, pour supprimer une extension précédemment installée :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie uninstall pecl/uuid
</span></span></code></pre></div><p>Ou encore pour lister les extensions installées :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie show
</span></span></code></pre></div><p>Et enfin pour afficher les détails d&rsquo;une extension en particulier :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie info pecl/uuid
</span></span></code></pre></div><h2 id="gérer-plusieurs-dépôts-dextensions">Gérer plusieurs dépôts d&rsquo;extensions<a href="#g%c3%a9rer-plusieurs-d%c3%a9p%c3%b4ts-dextensions" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>PIE ne se limite pas aux extensions disponibles sur Packagist. L&rsquo;outil offre une grande flexibilité en permettant l&rsquo;installation d&rsquo;extensions provenant de diverses sources : dépôts GitHub, archives locales, ou même des extensions développées en interne.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie repository:add path /path/to/your/local/extension
</span></span><span class="line"><span class="cl">pie repository:add vcs https://github.com/youruser/yourextension
</span></span><span class="line"><span class="cl">pie repository:add composer https://repo.packagist.com/your-private-packagist/
</span></span></code></pre></div><p>Et vous retrouverez aussi les commandes pour supprimer un dépôt :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie repository:remove /path/to/your/local/extension
</span></span></code></pre></div><p>Ou juste lister les dépôts que vous avez ajoutés :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie repository:list
</span></span></code></pre></div><p>Comme l’outil utilise symfony/console, vous pouvez avoir de l’autocompletion :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pie completion <span class="p">|</span> sudo tee /etc/bash_completion.d/pie
</span></span></code></pre></div><h2 id="conclusion">Conclusion<a href="#conclusion" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Lors de la relecture de cet article, une question m&rsquo;a été posée : <strong>pourquoi ne pas utiliser APT</strong> ou d&rsquo;autres gestionnaires de paquets (comme Brew, Yum, etc.) à la place ? À mon avis, PIE offre un avantage considérable : l&rsquo;accès à une bibliothèque d&rsquo;extensions PHP beaucoup plus vaste. Les gestionnaires de paquets traditionnels exigent des développeurs qu&rsquo;ils créent et compilent leur package pour différentes architectures, ce qui représente un travail conséquent. En revanche, PIE simplifie radicalement ce processus. Il récupère automatiquement le code source et les dépendances de l&rsquo;extension, puis la compile spécifiquement pour l&rsquo;appareil où il est installé. Pour les développeurs, la procédure est allégée : il leur suffit d&rsquo;enregistrer leur extension sur Packagist pour la rendre disponible à tous les utilisateurs de PIE.</p>
<p>Tout de même, PIE démontre déjà des fonctionnalités solides et intuitives pour la gestion des extensions PHP. Bien qu&rsquo;encore en développement, l&rsquo;outil offre une base prometteuse qui simplifie l&rsquo;installation et la gestion des extensions.</p>
<p>Nous sommes impatients de voir comment la communauté PHP va adopter et faire évoluer PIE dans les mois et années à venir.</p>
<p>Pour plus d&rsquo;informations n&rsquo;hésitez pas à consulter le <a href="https://github.com/php/pie">repository github de l&rsquo;outil</a>. Ou suivre le <a href="https://thephp.foundation/blog/">blog de la PHP Foundation</a> !</p>
]]></content>
		</item>
		
		<item>
			<title>✍️ PHP Object Lazy-Loading is More Than What You Think</title>
			<link>/posts/2024-09-16-php-object-lazy-loading-is-more-than-you-think/</link>
			<pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate><guid>/posts/2024-09-16-php-object-lazy-loading-is-more-than-you-think/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p>Disclaimer: This post is a copy of my <a href="https://jolicode.com/blog/php-object-lazy-loading-is-more-than-what-you-think">JoliCode blog post</a>.</p>
<p>We recently attended <a href="https://x.com/AFUP_Paris/status/1801306022994137460">a talk about lazy-loading</a> by <a href="https://github.com/nicolas-grekas/">Nicolas Grekas</a> and it inspired me this blogpost!</p>
<p>We can find lazy-loading in all modern PHP applications, in ORMs, for example. <strong>But is there more usage of lazy-loading?</strong></p>
<h2 id="what-is-lazy-loading">What is lazy-loading?<a href="#what-is-lazy-loading" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>In short: lazy-loading consists of delaying load or initialization of resources or objects until they&rsquo;re actually needed. It&rsquo;s something you will never see directly, the whole objective of lazy-loading is to be invisible so you can use your applications the way you always do.</p>
<p>And behind this invisible thing, we will give you <em>something</em> that will act as the object you want but will only load its content when required.</p>
<p>We can see two main pros about using lazy-loading:</p>
<ul>
<li>It will save memory and CPU until we load the data</li>
<li>It may never be called since you sometimes load nested objects that you don&rsquo;t need&hellip;</li>
</ul>
<p>What is lazy-loading? <a href="https://martinfowler.com/">Martin Fowler</a>, in his book &ldquo;<a href="https://martinfowler.com/books/eaa.html">Patterns of Enterprise Application Architecture</a>&rdquo;, explains this pattern very well. There are 4 types of lazy-loading patterns. Each of them have pros &amp; cons:</p>
<ul>
<li><strong>Lazy initialization</strong>, your object contains a null property and whenever you need that property, it will initialize it. In that case, your object needs to be aware that it is lazy-loaded;</li>
<li><strong>Value holders</strong>, it&rsquo;s an object that is responsible for initializing another object whenever you need it. It will require you to write a different class but the base class will stays the same;</li>
<li><strong>Virtual proxy</strong> is an object that is using the same interface (methods) that is used in the base object and whenever you use one of those methods, it will instantiate the base object and delegate to it;</li>
<li><strong>Ghost objects</strong> are objects that are to be loaded in a partial state. It may initially only contain the object&rsquo;s identifier, but it loads its own data the first time one of its properties or methods are accessed.</li>
</ul>
<p>In PHP we have some libraries that implement this pattern such as <a href="https://github.com/Ocramius/ProxyManager">ProxyManager</a>, <a href="https://github.com/FriendsOfPHP/proxy-manager-lts">Proxy Manager LTS</a> or even <a href="https://github.com/symfony/var-exporter">symfony/var-exporter</a>. I do personally recommend using the latter when possible.</p>
<p>One of the biggest drawbacks of Ghost objects is that you need to create a class that extends the proxified class. It can block you if you want to proxify a class that is final but will work in most cases and none of the consumers or the proxified class has to know there is a ghost object there.
More recently, we saw a <a href="https://wiki.php.net/rfc/lazy-objects">Lazy Object RFC</a> done by Nicolas Grekas &amp; <a href="https://github.com/arnaud-lb">Arnaud Le Blanc</a>, merged from the PHP team! Adding this feature to the core means we no longer need to create a proxy class, which is currently almost always required for lazy-loading.</p>
<p>Now that we&rsquo;ve seen what lazy-loading is and what libraries can leverage it in PHP, let&rsquo;s see how it is used in well known PHP libraries.</p>
<h2 id="symfony-dependencyinjection">Symfony DependencyInjection<a href="#symfony-dependencyinjection" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>In Symfony, you can define a service as needing to be lazy-loaded in multiple ways.
You can either define a service as being lazy-loaded or you can define one of the dependencies of a service as being lazy-loaded.</p>
<blockquote>
<p>For instance, imagine you have a <code>NewsletterManager</code> and you inject a <code>mailer</code> service into it. Only a few methods on your <code>NewsletterManager</code> actually use the <code>mailer</code>, but even when you don&rsquo;t need it, <code>mailer</code> service is always instantiated in order to construct your <code>NewsletterManager</code>.</p>
<p>Configuring lazy services is one answer to this. With a lazy service, a &ldquo;proxy&rdquo; of the <code>mailer</code> service is actually injected. It looks and acts like the <code>mailer</code>, except that the <code>mailer</code> isn&rsquo;t actually instantiated until you interact with the proxy in some way.</p>
</blockquote>
<p>To put it with examples, considering you have the following service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">readonly</span> <span class="k">class</span> <span class="nc">NewsletterManager</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">	<span class="k">private</span> <span class="nx">MailerInterface</span> <span class="nv">$mailer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>When we use this service, we could have methods that don&rsquo;t use the <code>$mailer</code> property. That&rsquo;s why we don&rsquo;t want to load it directly and we use lazy-loading. When you use the external mailer service, the mailer will be initialized and returned.</p>
<p>Based on the 4 types of lazy-loading we described, Symfony doesn&rsquo;t use a single type but adapts based on what it requires. You can find more about lazy-loading service in Symfony in the <a href="https://symfony.com/doc/current/service_container/lazy_services.html">related documentation page</a>.</p>
<h2 id="doctrine">Doctrine<a href="#doctrine" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>When you use Doctrine, you&rsquo;ll sometimes have a lazy-loaded object so that it can trigger a database query only when the object is accessed. By making a direct request to an object, Doctrine will return the hydrated object, but sometimes our objects have relationships with other objects, so these relationships will be lazy-loaded. By default, the relationships are lazy-loaded (except for OneToOne relations),  a proxy object will be returned instead of the real object, which will be loaded only if this object is accessed, which will also trigger a request to the database. You can avoid this behavior by using the <code>fetch</code> parameter <a href="https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/tutorials/extra-lazy-associations.html">with the value <code>EAGER</code></a>, you&rsquo;ll get a real object directly.</p>
<p>In practice, you will have the following code for your entity:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[ORM\Entity]
</span></span></span><span class="line"><span class="cl"><span class="c1">#[ORM\Table(name: &#39;customer&#39;)]
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// some columns …
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">#[ManyToOne(targetEntity: Address::class, nullable: true)]
</span></span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="o">?</span><span class="nx">Address</span> <span class="nv">$address</span> <span class="o">=</span> <span class="k">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>And whenever you request an User, the Address object will be proxified. Doctrine will generate the following proxy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Address</span> <span class="k">extends</span> <span class="nx">\App\Entity\Address</span> <span class="k">implements</span> <span class="nx">\Doctrine\ORM\Proxy\InternalProxy</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">use</span> <span class="nx">\Symfony\Component\VarExporter\LazyGhostTrait</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">initializeLazyObject</span> <span class="k">as</span> <span class="nx">__load</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">setLazyObjectAsInitialized</span> <span class="k">as</span> <span class="k">public</span> <span class="nx">__setInitialized</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">isLazyObjectInitialized</span> <span class="k">as</span> <span class="k">private</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">createLazyGhost</span> <span class="k">as</span> <span class="k">private</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">resetLazyObject</span> <span class="k">as</span> <span class="k">private</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="k">const</span> <span class="no">LAZY_OBJECT_PROPERTY_SCOPES</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;</span><span class="se">\0</span><span class="s2">&#34;</span><span class="o">.</span><span class="k">parent</span><span class="o">::</span><span class="na">class</span><span class="o">.</span><span class="s2">&#34;</span><span class="se">\0</span><span class="s2">&#34;</span><span class="o">.</span><span class="s1">&#39;id&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="k">parent</span><span class="o">::</span><span class="na">class</span><span class="p">,</span> <span class="s1">&#39;id&#39;</span><span class="p">,</span> <span class="k">null</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;</span><span class="se">\0</span><span class="s2">&#34;</span><span class="o">.</span><span class="k">parent</span><span class="o">::</span><span class="na">class</span><span class="o">.</span><span class="s2">&#34;</span><span class="se">\0</span><span class="s2">&#34;</span><span class="o">.</span><span class="s1">&#39;name&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="k">parent</span><span class="o">::</span><span class="na">class</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">,</span> <span class="k">null</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ... other fields
</span></span></span><span class="line"><span class="cl">  <span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// ... other methods
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>When you’re making a database query with Doctrine, the relations of the object you’re fetching will (depending on configuration) be proxified. Since those proxies extend your entity class, you will be able to use them the exact same way you usually use your entity.</p>
<p>Something different from Symfony&rsquo;s DependencyInjection here is that each Doctrine proxy will contain an identifier (usually a numeric id) that will allow Doctrine to link a proxy instance to a row for the given entity in the database. We call that a LazyGhost.</p>
<h2 id="and-in-your-projects">And in your projects?<a href="#and-in-your-projects" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Now that we&rsquo;ve seen the two most common uses of lazy-loading, how could you use this in your applications?</p>
<p>One of the best use cases I stumbled over upon my 13 years of development is <strong>curl requests</strong>. Not the simple curl request that you do to an external service, I mean thousands of curl requests that you probably will require at the same time… or maybe not.</p>
<h3 id="the-base-issue">The base issue<a href="#the-base-issue" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>curl is one of those projects we can&rsquo;t ignore in my developer’s life. There aren&rsquo;t many of them but curl is clearly in that list. And after using curl all this time, we found some strange issues, 5 years ago, when trying to make some HTTP calls to an API.</p>
<p>First of all, why do we have to make thousands of requests to an API? we are working for an e-commerce company that has multiple services, and specifically one that is a <a href="https://en.wikipedia.org/wiki/Product_information_management">PIM</a>. That API is a product catalog. When we show a product page, we reach that API for the specific product we&rsquo;re showing in order to gather data about this product. Also, when we show a collection page, we have multiple products so we have to reach that API for each product (and other stuff like product categories).</p>
<p>And the final encounter, exports. That company is often doing exports of all products that are currently in sale and thus making thousands of requests to that API.</p>
<p>In order to make all of this work, we could just wait for each HTTP call to be done on the pages or exports. But this could mean waiting for hundreds of calls to finish. At the moment our product detail call is between 200 to 300 ms, so for an export of 2000 products it would require 10min to wait <strong>just</strong> for API calls to be done, not counting deserialization of data because I’m using DTOs. For a collection page, we can have up to 200/300 products in the same page for the biggest ones which would take around 1 min for API calls only.</p>
<h3 id="linking-the-pim">Linking the PIM<a href="#linking-the-pim" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>In order to solve this issue, we have to create a link with that PIM API so we can lazy-load its data. As we said before, Doctrine is often using proxies so why not using an entity to make that link? The idea here is that when we load an entity, one of the properties will contain a proxy of how the API response should be, and once you access it we will trigger the API call to return the API model.</p>
<p>We will need something to identify what is the entity that contains the proxy object:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[ORM\Entity]
</span></span></span><span class="line"><span class="cl"><span class="c1">#[ORM\Table(name: &#39;product&#39;)]
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Product</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">#[ORM\Column]
</span></span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="nx">string</span> <span class="nv">$pimUuid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="nx">string</span> <span class="nv">$pimObject</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">setPimUuid</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$pimUuid</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">pimUuid</span> <span class="o">=</span> <span class="nv">$pimUuid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">getPimUuid</span><span class="p">()</span><span class="o">:</span> <span class="nx">string</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">pimUuid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">setPimObject</span><span class="p">(</span><span class="nx">object</span> <span class="nv">$pimObject</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">pimObject</span> <span class="o">=</span> <span class="nv">$pimObject</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">getPimObject</span><span class="p">()</span><span class="o">:</span> <span class="nx">object</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">pimObject</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In addition to that entity, we need to listen to the loading of the Product entity, which can be done with a Doctrine listener. That listener will handle the proxy generation and insert the proxy into the pimObject property so we can trigger the curl call when it is required:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[AsEntityListener(event: Events::postLoad)]
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PimListener</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">postLoad</span><span class="p">(</span><span class="nx">PostLoadEventArgs</span> <span class="nv">$args</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="sd">/** @var object|PimInterface $entity */</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$entity</span> <span class="o">=</span> <span class="nv">$args</span><span class="o">-&gt;</span><span class="na">getObject</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$entity</span> <span class="nx">instanceof</span> <span class="nx">Product</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sd">/** @var string $uuid */</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$uuid</span> <span class="o">=</span> <span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">getPimUuid</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">setPimObject</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">fetchPim</span><span class="p">(</span><span class="nv">$uuid</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="k">function</span> <span class="nf">fetchPim</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$uuid</span><span class="p">)</span><span class="o">:</span> <span class="nx">object</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$initializer</span> <span class="o">=</span> <span class="k">function</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nx">GhostObjectInterface</span> <span class="nv">$ghostObject</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">string</span> <span class="nv">$method</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="k">array</span> <span class="nv">$parameters</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="o">&amp;</span><span class="nv">$initializer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="k">array</span> <span class="nv">$properties</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="k">use</span> <span class="p">(</span><span class="nv">$uuid</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$initializer</span> <span class="o">=</span> <span class="k">null</span><span class="p">;</span> <span class="c1">// disable initialization
</span></span></span><span class="line"><span class="cl">      <span class="c1">// Checking if we got a result from PIM API
</span></span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$ghostObject</span> <span class="nx">instanceof</span> <span class="nx">ProductDTO</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="k">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1">// do the actual curl call
</span></span></span><span class="line"><span class="cl">      <span class="nv">$pimProduct</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">pimClient</span><span class="o">-&gt;</span><span class="na">getVariantItem</span><span class="p">(</span><span class="nv">$uuid</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="sd">/**
</span></span></span><span class="line"><span class="cl"><span class="sd">        * For all object properties, we take property name then we guess the mutator
</span></span></span><span class="line"><span class="cl"><span class="sd">        * method and we use it for each property in order to hydrate the object
</span></span></span><span class="line"><span class="cl"><span class="sd">        */</span>
</span></span><span class="line"><span class="cl">      <span class="k">foreach</span> <span class="p">(</span><span class="nx">\array_keys</span><span class="p">(</span><span class="nv">$properties</span><span class="p">)</span> <span class="k">as</span> <span class="nv">$property</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$parts</span> <span class="o">=</span> <span class="nx">explode</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\x00</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$property</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="sd">/** @var string $variable */</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$variable</span> <span class="o">=</span> <span class="nx">\array_pop</span><span class="p">(</span><span class="nv">$parts</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$mutator</span> <span class="o">=</span> <span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;get%s&#39;</span><span class="p">,</span> <span class="nx">\ucfirst</span><span class="p">(</span><span class="nv">$variable</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$properties</span><span class="p">[</span><span class="nv">$property</span><span class="p">]</span> <span class="o">=</span> <span class="nx">\call_user_func</span><span class="p">([</span><span class="nv">$pimProduct</span><span class="p">,</span> <span class="nv">$mutator</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">factory</span><span class="o">-&gt;</span><span class="na">createProxy</span><span class="p">(</span><span class="nx">ProductDTO</span><span class="o">::</span><span class="na">class</span><span class="p">,</span> <span class="nv">$initializer</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>With this implementation, whenever we get a Product entity from the database, we inject a GhostProxy into that entity. If we access a property of this object, we trigger the curl call. From this point, we could easily have PIM data within other parts of our application with ease, thanks to that hidden curl call, but that was only the beginning of issues.</p>
<p><img src="/media/original/2024/no_async.png" alt="Flow no async"></p>
<!-- raw HTML omitted -->
<p>For a simple product page, this would make the job easy but what about a showcase of multiple products or even exporting the whole product list? We would have to wait for the curl call to be made one by one … As explained before, we don’t want that.</p>
<h3 id="overcoming-curl-limits">Overcoming curl limits<a href="#overcoming-curl-limits" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>With this implementation we could see some strange issues: from time to time, some curl requests towards the PIM API were dropping for like, no reason!? We did many investigations but could never reproduce the exact issue. After some time, we could tell that the issues would always happens when we had more than 400/500 curl calls to do in a row and stumbled upon <a href="https://github.com/symfony/symfony/pull/38690">this issue within Symfony</a>.</p>
<p>When you&rsquo;re using <code>HttpClient</code>in Symfony, you will, by default, use the <code>CurlHttpClient</code> which uses <code>curl_multi</code> under the hood to launch HTTP calls. Thanks to <code>curl_multi</code>, we have something close to asynchronous HTTP calls because we can trigger a HTTP call and use it later, but in the meantime <code>curl_multi</code> could already have received the result?</p>
<p>Since we knew what the issue was (number of concurrent requests when using <code>curl_multi</code>), we did the simplest change to fix this issue: batching. The idea was that instead of getting the Products one by one, we would use the product list endpoint onto the PIM API. That way we could get Product 25 per 25. From 500 curl calls, we dropped to 20 calls.</p>
<p>But we still had to <strong>wait</strong> for 20 calls to be made before we could start building our page. One product list call takes around ~120ms, but 20 times that would be 2.4s. And that&rsquo;s only in the optimistic case where we only need products.</p>
<p>That&rsquo;s why we combined the strengths of lazy-loading with the <em>almost async</em> Symfony CurlHttpClient to make the &ldquo;preload&rdquo; feature. Think about HTTP preload: we will start fetching data before it&rsquo;s even used and when we access that data, it will probably already be there, or it will have started to call for it. That way we have the minimum possible time to wait for PIM data!</p>
<p>Let&rsquo;s see it with some code:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">final</span> <span class="k">class</span> <span class="nc">Result</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="nx">bool</span> <span class="nv">$initialized</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="nx">ResponseInterface</span> <span class="nv">$response</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="nx">string</span> <span class="nv">$modelClass</span><span class="p">,</span> <span class="c1">// DTO FQCN
</span></span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="nx">SerializerInterface</span> <span class="nv">$serializer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">initialized</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">isInitialized</span><span class="p">()</span><span class="o">:</span> <span class="nx">bool</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">initialized</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">getStatusCode</span><span class="p">()</span><span class="o">:</span> <span class="nx">int</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">initialized</span> <span class="o">=</span> <span class="k">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">response</span><span class="o">-&gt;</span><span class="na">getStatusCode</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">toObject</span><span class="p">()</span><span class="o">:</span> <span class="o">?</span><span class="nx">object</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">serializer</span><span class="o">-&gt;</span><span class="na">deserialize</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">getContents</span><span class="p">(),</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">modelClass</span><span class="p">,</span> <span class="s1">&#39;json&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">getContents</span><span class="p">()</span><span class="o">:</span> <span class="nx">string</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">initialized</span> <span class="o">=</span> <span class="k">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">response</span><span class="o">-&gt;</span><span class="na">getContent</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In the PimListener:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">public</span> <span class="k">function</span> <span class="nf">preload</span><span class="p">(</span><span class="k">array</span> <span class="nv">$entities</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">foreach</span> <span class="p">(</span><span class="nv">$entities</span> <span class="k">as</span> <span class="nv">$entity</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// some configuration pass based on the entity ...
</span></span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nv">$modelClass</span><span class="p">,</span> <span class="nv">$endpointClass</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">getPimModelConfiguration</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">\array_key_exists</span><span class="p">(</span><span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">getPimUuid</span><span class="p">(),</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">arrayResults</span><span class="p">[</span><span class="nv">$modelClass</span><span class="p">]))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">continue</span><span class="p">;</span> <span class="c1">// Do not fetch data we already have in current transaction
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$endpoint</span> <span class="o">=</span> <span class="k">new</span> <span class="nv">$endpointClass</span><span class="p">(</span><span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">getPimUuid</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$response</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">makeRequest</span><span class="p">(</span><span class="nv">$endpoint</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">arrayResults</span><span class="p">[</span><span class="nv">$modelClass</span><span class="p">][</span><span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">getPimUuid</span><span class="p">()]</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Result</span><span class="p">(</span><span class="nv">$response</span><span class="p">,</span> <span class="nv">$modelClass</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">serializer</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>With this Result class and the preload method, we can load data before we need it. So now in my page controller, the first line is a query that gets all the products we will use during the rendering part. And we send that product array into the preload method. That way we start the curl queries and later on when we try to, for example, get the image of a product, it will be instantaneous!</p>
<p><img src="/media/original/2024/with_async.png" alt="Flow with async"></p>
<!-- raw HTML omitted -->
<p>Full <code>PimListener</code> code is available on this gist: <a href="https://gist.github.com/Korbeil/7451afbff73da572f0500375d6f74805">https://gist.github.com/Korbeil/7451afbff73da572f0500375d6f74805</a></p>
<h2 id="conclusion">Conclusion<a href="#conclusion" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Now that you know what lazy-loading is and have an idea on how you can use it, we can&rsquo;t wait to see all the new things the PHP community will find to do with this awesome tool. We wanted to make this post because we saw many comments about the recent RFC being too &ldquo;niche&rdquo; and &ldquo;not something we need&rdquo;. Now we can all see how lazy-loading could help us and make our projects more performant!</p>
<h2 id="and-with-that-new-rfc">And with that new RFC?<a href="#and-with-that-new-rfc" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Now that <a href="https://wiki.php.net/rfc/lazy-objects">Lazy Object RFC</a> was accepted, this code will not be the same anymore. Here is a quick peek on how it should looks based on the simple PimListener we made before:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[AsEntityListener(event: Events::postLoad)]
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PimListener</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">postLoad</span><span class="p">(</span><span class="nx">PostLoadEventArgs</span> <span class="nv">$args</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="sd">/** @var object|PimInterface $entity */</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$entity</span> <span class="o">=</span> <span class="nv">$args</span><span class="o">-&gt;</span><span class="na">getObject</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$entity</span> <span class="nx">instanceof</span> <span class="nx">Product</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sd">/** @var string $uuid */</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$uuid</span> <span class="o">=</span> <span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">getPimUuid</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$entity</span><span class="o">-&gt;</span><span class="na">setPimObject</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">fetchPim</span><span class="p">(</span><span class="nv">$uuid</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="k">function</span> <span class="nf">fetchPim</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$uuid</span><span class="p">)</span><span class="o">:</span> <span class="nx">object</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$reflector</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ReflectionClass</span><span class="p">(</span><span class="nx">ProductDTO</span><span class="o">::</span><span class="na">class</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$initializer</span> <span class="o">=</span> <span class="k">static</span> <span class="k">function</span> <span class="p">(</span><span class="nx">ProductDTO</span> <span class="nv">$product</span><span class="p">)</span> <span class="k">use</span> <span class="p">(</span><span class="nv">$reflector</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$pimProduct</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">pimClient</span><span class="o">-&gt;</span><span class="na">getVariantItem</span><span class="p">(</span><span class="nv">$product</span><span class="o">-&gt;</span><span class="na">getUuid</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">      <span class="k">foreach</span> <span class="p">(</span><span class="nv">$reflector</span><span class="o">-&gt;</span><span class="na">getProperties</span><span class="p">()</span> <span class="k">as</span> <span class="nv">$property</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$mutator</span> <span class="o">=</span> <span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;get%s&#39;</span><span class="p">,</span> <span class="nx">\ucfirst</span><span class="p">(</span><span class="nv">$property</span><span class="o">-&gt;</span><span class="na">getName</span><span class="p">()));</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$reflector</span><span class="o">-&gt;</span><span class="na">getProperty</span><span class="p">(</span><span class="nv">$property</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">setValue</span><span class="p">(</span><span class="nv">$post</span><span class="p">,</span> <span class="nx">\call_user_func</span><span class="p">([</span><span class="nv">$pimProduct</span><span class="p">,</span> <span class="nv">$mutator</span><span class="p">]));</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$product</span> <span class="o">=</span> <span class="nv">$reflector</span><span class="o">-&gt;</span><span class="na">newLazyGhost</span><span class="p">(</span><span class="nv">$initializer</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$reflector</span><span class="o">-&gt;</span><span class="na">getProperty</span><span class="p">(</span><span class="s1">&#39;uuid&#39;</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">setRawValueWithoutLazyInitialization</span><span class="p">(</span><span class="nv">$product</span><span class="p">,</span> <span class="nv">$uuid</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$product</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>See how simple the implementation is with the RFC! And on top of that, you won’t need any third-party library to do that, it will be embedded into the PHP core! 🎉</p>
]]></content>
		</item>
		
		<item>
			<title>✍️ Maîtrisez la planification des tâches avec Symfony Scheduler</title>
			<link>/posts/2023-12-05-master-task-scheduling-symfony-scheduler/</link>
			<pubDate>Tue, 05 Dec 2023 00:00:00 +0000</pubDate><guid>/posts/2023-12-05-master-task-scheduling-symfony-scheduler/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p>Disclaimer: This post is a copy of my <a href="https://jolicode.com/blog/maitrisez-la-planification-des-taches-avec-symfony-scheduler">JoliCode blog post</a>.</p>
<p>Aujourd’hui, utiliser une crontab pour nos tâches récurrentes est assez courant mais pas très pratique car complètement déconnecté de notre application. Le composant Scheduler se présente comme une excellente alternative. Il a été introduit en 6.3 par Fabien Potencier lors de sa keynote d’ouverture du <a href="/blog/notre-retour-sur-le-symfonylive-paris-2023">SymfonyLive Paris 2023</a>. Le composant est maintenant réputé comme stable depuis la sortie de Symfony 6.4.
Regardons comment l’utiliser !</p>
<h2 id="installation">Installation<a href="#installation" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Installons le composant :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">composer require symfony/messenger symfony/scheduler
</span></span></code></pre></div><p>Comme toutes les fonctionnalités du composant se basent sur Messenger, il est nécessaire de l’installer aussi.</p>
<h2 id="une-première-tâche">Une première tâche<a href="#une-premi%c3%a8re-t%c3%a2che" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Créons un premier message à planifier :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// src/Message/Foo.php
</span></span></span><span class="line"><span class="cl"><span class="nx">readonly</span> <span class="k">final</span> <span class="k">class</span> <span class="nc">Foo</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// src/Handler/FooHandler.php
</span></span></span><span class="line"><span class="cl"><span class="c1">#[AsMessageHandler]
</span></span></span><span class="line"><span class="cl"><span class="nx">readonly</span> <span class="k">final</span> <span class="k">class</span> <span class="nc">FooHandler</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="fm">__invoke</span><span class="p">(</span><span class="nx">Foo</span> <span class="nv">$foo</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>De la même manière qu&rsquo;un Message dispatché dans Messenger, nous dispatchons ici un Message, que Scheduler traitera de façon similaire à Messenger, excepté que le déclenchement du traitement se fera sur une base temporelle</p>
<p>En plus du couple Message/Handler, nous avons besoin de définir un &ldquo;Schedule&rdquo; :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[AsSchedule(name: &#39;default&#39;)]
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Scheduler</span> <span class="k">implements</span> <span class="nx">ScheduleProviderInterface</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">getSchedule</span><span class="p">()</span><span class="o">:</span> <span class="nx">Schedule</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="k">new</span> <span class="nx">Schedule</span><span class="p">())</span><span class="o">-&gt;</span><span class="na">add</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;2 days&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">Foo</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Il va permettre d’indiquer à notre application que nous avons un Schedule &ldquo;default&rdquo; qui contient un message lancé tous les deux jours. Ici, la fréquence est simple, mais il est tout à fait possible de configurer cela plus finement :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;1 second&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;15 day&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># format relatif
</span></span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;next friday&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;first sunday of next month&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># se lance à un horaire spécifique tous les jours
</span></span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;1 day&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">,</span> <span class="nx">from</span><span class="o">:</span> <span class="s1">&#39;14:42&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># vous pouvez donner un objet DateTime aussi
</span></span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;1 day&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">from</span><span class="o">:</span> <span class="k">new</span> <span class="nx">\DateTimeImmutable</span><span class="p">(</span><span class="s1">&#39;14:42&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">\DateTimeZone</span><span class="p">(</span><span class="s1">&#39;Europe/Paris&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># définir la fin de la récurrence
</span></span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;1 day&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">,</span> <span class="nx">until</span><span class="o">:</span> <span class="s1">&#39;2023-09-21&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># vous pouvez aussi utiliser des expressions cron
</span></span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">cron</span><span class="p">(</span><span class="s1">&#39;42 14 * * 2&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">)</span> <span class="c1">// every Tuesday at 14:42
</span></span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">cron</span><span class="p">(</span><span class="s1">&#39;#midnight&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">cron</span><span class="p">(</span><span class="s1">&#39;#weekly&#39;</span><span class="p">,</span> <span class="nv">$msg</span><span class="p">)</span>
</span></span></code></pre></div><p>Ici, nous pouvons voir des formats relatifs; vous trouverez plus d’informations sur ce format en PHP sur la <a href="https://www.php.net/manual/fr/datetime.formats.php#datetime.formats.relative">page de documentation</a>.</p>
<p>Pour les syntaxes <code>cron</code>, il vous faudra installer une librairie tierce qui permet à Scheduler de les interpréter :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">composer require dragonmantank/cron-expression
</span></span></code></pre></div><p>Une fois votre Schedule défini, comme pour un transport Messenger, il vous faudra un worker qui va écouter sur le Schedule de la façon suivante:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">bin/console messenger:consume -v scheduler_default
</span></span></code></pre></div><p>Le préfix <code>scheduler_</code> est le nom générique du transport pour tous les Schedule, auquel nous ajoutons le nom du Schedule créé.</p>
<h2 id="les-collisions">Les collisions<a href="#les-collisions" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Plus nous avons de tâches, plus nous avons de chances d’avoir des tâches qui vont arriver au même moment. Mais si une collision arrive, comment Scheduler va-t-il gérer ça ? Imaginons le cas suivant :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="p">(</span><span class="k">new</span> <span class="nx">Schedule</span><span class="p">())</span><span class="o">-&gt;</span><span class="na">add</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;2 days&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">Foo</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;3 days&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">Foo</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>Tous les 6 jours, les deux messages vont entrer en collision :</p>
<p><img src="/media/original/2023/articles/scheduler/image_1_fr.png" alt="Schedule collision"></p>
<p>Si nous avons qu&rsquo;un seul worker, alors il prendra la première tâche configurée dans le Schedule <strong>puis</strong>, une fois la première tâche finie, il exécutera la seconde tâche. Autrement dit, l&rsquo;heure d&rsquo;exécution de la 2ème tâche est dépendante de la durée d&rsquo;exécution de la 1ère.</p>
<p>Souvent nous voulons que nos tâches soient exécutées à un moment précis, pour régler ce soucis il existe deux solutions:</p>
<ul>
<li>La bonne pratique serait de préciser la date et heure d&rsquo;exécution de notre tâche grâce au paramètre <code>from</code>, par exemple: <code>RecurringMessage::every('1 day', $msg, from: '14:42')</code> pour un des messages et fixer à <code>15:42</code> pour l’autre tâche (aussi possible avec une syntaxe <code>cron</code>) ;</li>
<li>Avoir plusieurs workers qui tournent: si vous avez 2 workers, alors il pourra gérer 2 tâches en même temps !</li>
</ul>
<h2 id="plusieurs-workers-">Plusieurs workers ?<a href="#plusieurs-workers-" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Mais aujourd&rsquo;hui, si nous lançons 2 workers, notre tâche sera exécutée deux fois !</p>
<p><img src="/media/original/2023/articles/scheduler/image_2_fr.png" alt="Schedule workers"></p>
<p>Scheduler fournit les outils pour éviter ça ! Mettons un peu à jour notre Schedule :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[AsSchedule(name: &#39;default&#39;)]
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Scheduler</span> <span class="k">implements</span> <span class="nx">ScheduleProviderInterface</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="k">private</span> <span class="nx">readonly</span> <span class="nx">CacheInterface</span> <span class="nv">$cache</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="k">private</span> <span class="nx">readonly</span> <span class="nx">LockFactory</span> <span class="nv">$lockFactory</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">getSchedule</span><span class="p">()</span><span class="o">:</span> <span class="nx">Schedule</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="k">new</span> <span class="nx">Schedule</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">            <span class="o">-&gt;</span><span class="na">add</span><span class="p">(</span><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">every</span><span class="p">(</span><span class="s1">&#39;2 days&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">Foo</span><span class="p">(),</span> <span class="nx">from</span><span class="o">:</span> <span class="s1">&#39;04:05&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="o">-&gt;</span><span class="na">add</span><span class="p">(</span><span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">cron</span><span class="p">(</span><span class="s1">&#39;15 4 */3 * *&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">Foo</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">            <span class="o">-&gt;</span><span class="na">stateful</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">cache</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">-&gt;</span><span class="na">lock</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">lockFactory</span><span class="o">-&gt;</span><span class="na">createLock</span><span class="p">(</span><span class="s1">&#39;scheduler-default&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Nous récupèrons un service pour gérer son cache et pour créer des locks (penser à installer <code>symfony/lock</code> auparavant). Puis nous indiquons que notre schedule peut maintenant bénéficier d’un état et possède un lock grâce à ces nouveaux éléments.</p>
<p>Et voilà 🎉, maintenant nous pouvons avoir autant de workers que nous voulons, ils ne lanceront pas plusieurs fois le même message :)</p>
<p><img src="/media/original/2023/articles/scheduler/image_3_fr.png" alt="Schedule stateful workers"></p>
<h2 id="du-tooling-">Du tooling !<a href="#du-tooling-" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<h3 id="debug-de-nos-schedules">Debug de nos Schedules<a href="#debug-de-nos-schedules" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Une commande console a été ajoutée depuis <a href="https://github.com/symfony/symfony/pull/51795">cette PR</a>, elle permet de lister toutes les tâches des Schedules que vous avez créé !</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ bin/console debug:scheduler
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">Scheduler</span>
</span></span><span class="line"><span class="cl"><span class="o">=========</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">default
</span></span><span class="line"><span class="cl">-------
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> -------------- -------------------------------------------------- ---------------------------------
</span></span><span class="line"><span class="cl">  Trigger    	Provider                                       	Next Run                    	
</span></span><span class="line"><span class="cl"> -------------- -------------------------------------------------- ---------------------------------
</span></span><span class="line"><span class="cl">  every <span class="m">2</span> days   App<span class="se">\M</span>essenger<span class="se">\F</span>oo<span class="o">(</span>O:17:<span class="s2">&#34;App\Messenger\Foo&#34;</span>:0:<span class="o">{})</span>   Sun, <span class="m">03</span> Dec <span class="m">2023</span> 04:05:00 +0000
</span></span><span class="line"><span class="cl">  <span class="m">15</span> <span class="m">4</span> */3 * *   App<span class="se">\M</span>essenger<span class="se">\F</span>oo<span class="o">(</span>O:17:<span class="s2">&#34;App\Messenger\Foo&#34;</span>:0:<span class="o">{})</span>   Mon, <span class="m">04</span> Dec <span class="m">2023</span> 04:15:00 +0000
</span></span><span class="line"><span class="cl"> -------------- -------------------------------------------------- ---------------------------------
</span></span></code></pre></div><p>En plus de voir les tâches de vos Schedules, vous aurez aussi la prochaine date d&rsquo;exécution.</p>
<h3 id="changer-le-transport-de-vos-tâches">Changer le transport de vos tâches<a href="#changer-le-transport-de-vos-t%c3%a2ches" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Parfois un message peut prendre du temps à être traité. Il est donc possible de dire dans son Schedule que notre message doit être traité par un transport donné. Par exemple :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="p">(</span><span class="k">new</span> <span class="nx">Schedule</span><span class="p">())</span><span class="o">-&gt;</span><span class="na">add</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nx">RecurringMessage</span><span class="o">::</span><span class="na">cron</span><span class="p">(</span><span class="s1">&#39;15 4 */3 * *&#39;</span><span class="p">,</span> <span class="k">new</span> <span class="nx">RedispatchMessage</span><span class="p">(</span><span class="k">new</span> <span class="nx">Foo</span><span class="p">(),</span> <span class="nx">‘async’</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>Ici, quand le message doit être distribué, le worker va le renvoyer vers le transport <code>async</code> qui s’occupera alors de le traiter. Très pratique pour les tâches lourdes car cela libérera le worker <code>scheduler_default</code> pour traiter les prochains messages.</p>
<h3 id="gérer-les-erreurs">Gérer les erreurs<a href="#g%c3%a9rer-les-erreurs" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Scheduler permet d’écouter plusieurs événements via le composant <code>EventDispatcher</code>. Il existe 3 événements écoutables: <code>PreRunEvent</code>, <code>PostRunEvent</code> et <code>FailureEvent</code>. Les deux premiers seront déclenchés, respectivement, avant et après chaque tâche exécutée. Le dernier, quant à lui, sera lancé en cas d’exception dans une tâche. Cela peut être très pratique pour monitorer de façon efficace vos erreurs :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[AsEventListener(event: FailureEvent::class)]
</span></span></span><span class="line"><span class="cl"><span class="k">final</span> <span class="k">class</span> <span class="nc">ScheduleListener</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="fm">__invoke</span><span class="p">(</span><span class="nx">FailureEvent</span> <span class="nv">$event</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// triggers email to yourself when your schedules have issues
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Avec ce code, lorsqu’un événement <code>FailureEvent</code> arrive, vous pourrez vous envoyer un email ou rajouter des logs pour mieux comprendre le soucis.</p>
<h3 id="console-as-scheduler">Console as Scheduler<a href="#console-as-scheduler" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Une des fonctionnalités les plus intéressantes de Scheduler selon moi : les attributs <code>AsCronTask</code> et <code>AsPeriodicTask</code> ! Ceux-ci permettent de transformer une commande console en une tâche périodique de façon très simple ! <code>AsPeriodicTask</code> permet de définir une tâche via une récurrence simple: <code>2 days</code> par exemple, et <code>AsCronTask</code> permet de faire la même chose via une expression cron.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[AsCommand(name: &#39;app:foo&#39;)]
</span></span></span><span class="line"><span class="cl"><span class="c1">#[AsPeriodicTask(&#39;2 days&#39;, schedule: &#39;default&#39;)]
</span></span></span><span class="line"><span class="cl"><span class="k">final</span> <span class="k">class</span> <span class="nc">FooCommand</span> <span class="k">extends</span> <span class="nx">Command</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">execute</span><span class="p">(</span><span class="nx">InputInterface</span> <span class="nv">$input</span><span class="p">,</span> <span class="nx">OutputInterface</span> <span class="nv">$output</span><span class="p">)</span><span class="o">:</span> <span class="nx">int</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// run you command
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">Command</span><span class="o">::</span><span class="na">SUCCESS</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Et voilà, la commande sera exécutée dans le Schedule <code>default</code> tous les 2 jours !</p>
<p>Nous retrouvons souvent des doublons entre les commandes console et vos tâches récurrentes, c’est la fonctionnalité parfaite pour faire le lien entre les deux !</p>
<h2 id="conclusion">Conclusion<a href="#conclusion" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Le composant Scheduler s&rsquo;impose comme un outil essentiel pour intégrer efficacement les tâches récurrentes dans Symfony. Sa simplicité d&rsquo;utilisation, sa flexibilité, la gestion des expressions cron, ainsi que son intégration transparente avec les commandes console en font un choix incontournable.</p>
]]></content>
		</item>
		
		<item>
			<title>✍️ Astuces pour traiter des gros volumes de données dans Symfony</title>
			<link>/posts/2023-04-03-tips-and-tricks-big-data-in-symfony/</link>
			<pubDate>Mon, 03 Apr 2023 00:00:00 +0000</pubDate><guid>/posts/2023-04-03-tips-and-tricks-big-data-in-symfony/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p>Disclaimer: This post is a copy of my <a href="https://jolicode.com/blog/astuces-pour-traiter-des-gros-volumes-de-donnees-dans-symfony">JoliCode blog post</a>.</p>
<p>Dans la vie d&rsquo;un développeur, il arrive forcément un moment où l&rsquo;on doit traiter un volume important de données via une ligne de commande. Et les premières fois, ça fait BOOM 💥, on utilise du <code>memory_limit=-1</code>, <a href="/blog/redis-et-la-memoire-de-php-sont-dans-un-bateau-il-coule">des optimisations</a>… Dans cet article, je dresse une liste des différents points d&rsquo;attention qui m&rsquo;ont déjà été utiles, ainsi que des astuces qui peuvent grandement aider à l&rsquo;utilisation et à la maintenance de votre traitement.
Attention, cette liste n&rsquo;est pas exhaustive et les différents points peuvent être applicables ou non selon votre cas.</p>
<h2 id="astuces-génériques">Astuces génériques<a href="#astuces-g%c3%a9n%c3%a9riques" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>En premier, des conseils génériques qui peuvent s&rsquo;appliquer dans la majorité des cas :</p>
<ul>
<li><strong>Avoir un repère d’avancement.</strong> Cela peut paraître trivial, mais avoir une barre d’avancement ou un pourcentage permet de voir rapidement et facilement  la quantité d&rsquo;éléments à traiter et l&rsquo;état d&rsquo;avancement. Par exemple, Symfony propose une classe ProgressBar dans son composant Console qui permet de &ldquo;calculer&rdquo; le temps de traitement passé ou restant - estimé en fonction de la vitesse à laquelle nos éléments ont été traités jusqu&rsquo;à présent - et la consommation mémoire. Voici un exemple :</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">ProgressBar</span><span class="o">::</span><span class="na">setFormatDefinition</span><span class="p">(</span><span class="s1">&#39;custom&#39;</span><span class="p">,</span> <span class="s1">&#39;%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s% %message%&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$bar</span> <span class="o">=</span> <span class="nv">$io</span><span class="o">-&gt;</span><span class="na">createProgressBar</span><span class="p">(</span><span class="nv">$count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$bar</span><span class="o">-&gt;</span><span class="na">setMessage</span><span class="p">(</span><span class="s1">&#39;Starting&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$bar</span><span class="o">-&gt;</span><span class="na">setFormat</span><span class="p">(</span><span class="s1">&#39;custom&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$bar</span><span class="o">-&gt;</span><span class="na">start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">foreach</span> <span class="p">(</span><span class="nv">$items</span> <span class="k">as</span> <span class="nv">$item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nv">$bar</span><span class="o">-&gt;</span><span class="na">setMessage</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;Idem ID: %s&#39;</span><span class="p">,</span> <span class="nv">$item</span><span class="o">-&gt;</span><span class="na">getId</span><span class="p">()));</span>
</span></span><span class="line"><span class="cl">	<span class="nv">$bar</span><span class="o">-&gt;</span><span class="na">advance</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$bar</span><span class="o">-&gt;</span><span class="na">finish</span><span class="p">();</span>
</span></span></code></pre></div><p>Utiliser cette technique pose aussi deux soucis. Il faut pouvoir compter les éléments en amont, ce qui n’est pas toujours possible. Et il faut faire attention au taux de refresh du repère affiché en console pour parfois le réduire s’il est trop gourmand (il existe <a href="https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Console/Helper/ProgressBar.php#L70">un paramètre sur la classe ProgressBar</a> qui permet de gérer ça).</p>
<ul>
<li><strong>Rendre son traitement tolérant aux erreurs</strong>, toujours traiter les erreurs via un <a href="https://symfony.com/doc/current/logging.html">channel de log</a> ou via un rapport. Bien sûr, ce n&rsquo;est pas une raison pour ignorer ces erreurs, pensez à regarder ce rapport et à agir en conséquence.</li>
<li><strong>Ne pas regrouper les traitements</strong>. Une tâche qui traite beaucoup d&rsquo;éléments va prendre soit du temps, soit de la charge CPU ou elle peut juste planter. Si vous avez plusieurs actions à faire, séparez les en plusieurs commandes qui pourront être lancées indépendamment. J’ai déjà pu voir une commande qui gérait des statuts de paiements et qui faisait aussi la détection de fraude. Vous pouvez totalement séparer les deux actions en deux commandes et ainsi réduire la charge de vos commandes.</li>
<li><strong>Rendre votre commande relançable</strong> sans impact sur son exécution. En effet, il peut arriver d’avoir de très long processus qui bugguent au milieu, ou même que la commande soit trop longue pour être exécutée d’un coup. Il faudra donc pouvoir la lancer plusieurs fois. Pour cela, il existe plusieurs méthodes. La première serait d’utiliser un flag quand un élément est déjà traité, ce qui permettra de l’exclure au prochain passage. Une seconde méthode serait d’avoir une borne dans votre commande, par exemple <code>--continue-after-id</code> ou encore <code>--continue-after-date</code> - ici l’idée est d’avoir une borne à partir de laquelle on reprend le traitement des éléments.</li>
<li><strong>Attention à l&rsquo;utilisation des fonction de manipulation de tableaux</strong>. Par exemple, un <code>array_merge</code> au milieu d&rsquo;une boucle. Il vaut mieux regarder les alternatives pour faire ça en dehors de cette boucle. Vous pouvez aussi préparer votre donnée de base pour éviter d&rsquo;avoir à faire ce genre de manipulation. De façon plus générale, il vaut mieux éviter tout ce qui utilise de la mémoire dans une boucle, car plus la boucle est grande, plus l’impact est important pour votre serveur.</li>
<li><strong>Un rapport à la fin</strong> vous sera toujours utile. Par exemple, avoir le nombre d&rsquo;éléments traités, la durée de ce traitement, la quantité de mémoire utilisée, le nombre de requêtes SQL engendrées etc. Il est également intéressant d&rsquo;ajouter des indicateurs métiers : par exemple si votre commande vérifie des paiements pour trouver des fraudes potentielles, avoir le nombre de paiements détectés comme &ldquo;fraude&rdquo; ou &ldquo;validé&rdquo; peut être pertinent.</li>
<li><strong>Paralléliser sur plusieurs workers</strong> permet aussi de beaucoup accélérer le traitement des données. Attention cependant, cela rendra tout de suite votre tâche beaucoup plus complexe car il va falloir partager votre traitement de base en plusieurs tâches en parallèle de façon indépendante.</li>
</ul>
<h2 id="doctrine">Doctrine<a href="#doctrine" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Doctrine est souvent utilisé dans les applications Symfony et c&rsquo;est l&rsquo;une des sources de données les plus populaires. Je ne pouvais donc pas passer à côté d&rsquo;une section concernant Doctrine 😀.</p>
<ul>
<li><strong>Nettoyer l’EntityManager</strong> de Doctrine. De nombreuses requêtes vont faire que beaucoup d’objets seront instanciés et des fuites de mémoire vont apparaître à coup sûr. C’est pour cela qu’il est recommandé de nettoyer l’EntityManager à chaque tour de boucle ou dès que vous le pouvez grâce à la méthode <code>EntityManager::clear()</code>.</li>
<li><strong>Préférer utiliser <code>toIterable()</code></strong> sur les retours de requêtes avec beaucoup de résultats. En effet, lorsqu’on appelle <code>getResult()</code>, Doctrine va préparer toute la collection d&rsquo;un coup, et donc potentiellement avec beaucoup d&rsquo;éléments. En utilisant <code>toIterable()</code>, on aura les éléments hydratés un par un grâce à <a href="https://www.php.net/manual/fr/language.generators.syntax.php">un générateur</a>.</li>
<li><strong><a href="https://stackoverflow.com/questions/8269916/what-is-sliding-window-algorithm-examples#answer-64111403">Borner les requêtes</a></strong>. Il peut arriver que nos processus aient des éléments traités en erreur pour diverses raisons. C’est pour cela qu’il est recommandé de borner ses requêtes de façon temporelle (ne récupérer que les éléments des 3 derniers jours par exemple). Ainsi, lorsqu&rsquo;un élément est traité en erreur, il ne sera automatiquement plus traité au bout de 3 jours (puisqu&rsquo;il ne sera plus sélectionné). Cela permet de gagner du temps CPU sur vos serveurs une fois la borne dépassée. Une autre solution est de mettre un compteur de tentatives sur vos éléments et d’ignorer les éléments à partir d’un certain nombre de tentatives. Et encore une fois, n’oubliez pas de suivre et traiter ces erreurs !</li>
<li><strong>Réduire les combinatoires</strong>. Généralement, on préfère tout traiter dans une commande. Par exemple, si on a des paniers clients à supprimer, on fera une commande journalière pour supprimer les paniers expirés. Mais parfois, il vaut mieux traiter la tâche en plusieurs parties pour éviter de trop faire d’un coup. N’hésitez pas à diviser la tâche en plusieurs lots pour réduire leur taille ou leur temps de traitement. Par exemple, une première commande pour les paniers de la matinée et une seconde pour les paniers de l’après-midi.</li>
<li><strong>Utiliser des références plutôt qu’un <code>find()</code></strong>. Quand on doit récupérer une référence à une entité,  sans pour autant lui appliquer des traitements directement, il vaut mieux privilégier l’utilisation de <code>getReference(Entity::class, $id)</code>. Cela renverra un proxy de l’objet demandé sans faire de requête vers la base de données, comme le ferait un <code>find()</code>. Vous pourrez trouver plus d’informations sur la méthode <code>getReference</code> dans <a href="https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/advanced-configuration.html#reference-proxies">la documentation Doctrine</a>.</li>
<li><strong>Bien choisir le mode d’hydratation</strong> lors de l’utilisation de vos requêtes SQL. Par défaut, Doctrine met l’hydratation de vos requêtes à <code>object</code>. Il va alors transformer le résultat de la requête en un objet et garder en mémoire tous les objets ainsi créés. Cette hydratation d’objet va forcément prendre du temps. Ici, je vous recommande de passer par de l’hydratation <code>array</code> voire <code>scalar</code> (selon les besoins) pour réduire au maximum le temps de process de vos données par Doctrine.</li>
</ul>
<pre tabindex="0"><code>  object - 113Mib -  575ms
  simple - 106Mib -  457ms
   array - 066Mib -  333ms
  scalar - 066Mib -  291ms
</code></pre><ul>
<li><strong>Régler le cache Doctrine et sur votre base de données</strong>. Lorsqu’on fait de nombreuses fois la même requête avec possiblement des doublons, il peut être très utile de mettre en place du cache côté Doctrine. Pour cela il existe trois niveaux de cache dans Doctrine: d’abord le cache sur le mapping entre vos entités et les tables en base de données. Puis le Query cache, qui permet de mettre en cache la construction de la requête SQL depuis le <!-- raw HTML omitted -->DQL<!-- raw HTML omitted -->. Et le Result cache qui va permettre de conserver les résultats de votre requête pour les réutiliser si la requête venait à être exécutée une seconde fois. Les caches sur les metadata (donc le mapping) et la query sont activés par défaut dans Symfony.
Aussi, il existe parfois, selon le moteur utilisé, un cache côté base de données qui permet de garder les résultats des requêtes pour ensuite répondre plus vite. Pour MySQL il est désactivé par défaut depuis 5.6 et ⚠️ il a été <a href="https://dev.mysql.com/blog-archive/mysql-8-0-retiring-support-for-the-query-cache/">retiré depuis MySQL 8.0</a>.</li>
</ul>
<h2 id="fichier">Fichier<a href="#fichier" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Il est parfois nécessaire de traiter de la donnée provenant d’un fichier, ou bien d’écrire cette donnée dans un fichier.</p>
<ul>
<li><strong>Générer le contenu du fichier via un fichier temporaire</strong>. Quand on doit créer de gros fichiers, le fait de les générer puis de les écrire sur le disque peut utiliser beaucoup de mémoire. C’est pour ça qu’il est recommandé de passer par un générateur et d’écrire dans un fichier temporaire (via l&rsquo;utilisation de <code>tmpfile()</code>) ligne par ligne (au plus petit élément possible) puis d’envoyer le fichier temporaire. Cela évite d’avoir beaucoup de RAM occupée par la variable qui va stocker ces données et permet aussi de gérer plus facilement les soucis de données. Vous pouvez aussi générer votre propre fichier et utiliser des <code>file_put_contents()</code> pour écrire dedans, mais pour ma part, je suis plus habitué à <code>fwrite</code> donc la fonction <code>tmpfile()</code> est plus adaptée.</li>
<li><strong>Génération de fichiers en asynchrone</strong>. Lorsqu&rsquo;on génère des fichiers, la tâche nous semble toujours rapide au début. Puis au fur et à mesure de l&rsquo;évolution de notre code, le volume de ces fichiers augmente, les rendant de plus en plus lents à générer. C&rsquo;est pour cela que je recommande de générer vos fichiers via un worker asynchrone. De la même manière, si vous travaillez sur une requête HTTP pour une API, vous pouvez envoyer la génération du fichier en tâche de fond. Après, vous pouvez soit mettre en place une interface pour récupérer vos fichiers, soit passer par un envoi de lien dans un email ou autre moyen de notifier la disponibilité du fichier généré.</li>
</ul>
<h2 id="monitoring">Monitoring<a href="#monitoring" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Et pour finir, on aime traiter de la donnée, mais il faut aussi la surveiller !</p>
<ul>
<li>Lorsque vous faites du traitement de lots de données, pensez à <strong>surveiller les informations système de vos machines</strong> ! RAM, CPU, espace disque… on ne sait jamais ce qui peut poser souci, donc autant avoir un œil sur tout ce qui pourrait bloquer ou ralentir votre traitement. On peut imaginer de suivre la RAM utilisée ou disponible en combinaison d’une barre de progression. Aussi, je ne peux que vous recommander quelques outils comme <a href="https://www.netdata.cloud/">Netdata</a> ou l’utilisation de sondes de monitoring comme <a href="https://www.datadoghq.com/">Datadog</a>, <a href="https://github.com/prometheus/node_exporter">Prometheus</a> et bien d’autres.</li>
<li>Faites attention au <strong>garbage collector de PHP</strong>. En effet, par défaut, le garbage collector de PHP passera automatiquement quand il détecte que 10,000 objets sont instanciés pour nettoyer tous les objets qui ne sont plus utilisés. Lorsque vous avez un processus sur beaucoup de données, il peut être intéressant de désactiver ce comportement du garbage collector pour le faire vous-même à la main. Plus d’informations sur <a href="https://www.php.net/manual/en/features.gc.collecting-cycles.php">la documentation PHP</a>.</li>
<li>Et bien sûr <strong>ajoutez des métriques métiers</strong> ! Par exemple, si l&rsquo;on doit passer des commandes d&rsquo;un statut &ldquo;en attente&rdquo; à &ldquo;validé&rdquo;, il peut être judicieux de mesurer le nombre de commandes traitées, le nombre de commandes validées ou encore le nombre d&rsquo;erreurs. La moindre information vous aidera à comprendre votre tâche et suivre le traitement de ces données.</li>
</ul>
<p>Vous pourrez aussi trouver des astuces pour monitorer votre code PHP simplement dans <a href="/blog/comment-monitorer-rapidement-du-code-php">un de nos précédents articles de blog</a>.</p>
<h2 id="pour-finir">Pour finir<a href="#pour-finir" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>J&rsquo;espère que ce billet vous aura permis d&rsquo;apprendre des choses, n&rsquo;hésitez pas à y revenir lorsque vous aurez à traiter des gros lots de données. Un peu comme une checklist pour voir ce que nous vous recommandons de faire dans ce cas là. Mais n’oubliez pas qu’il existe des outils de profiling qui vous feront remonter les métriques CPU, RAM, requêtes SQL… et qui vous permettront d’ajuster vos process avec plus de précision ! Je pense faire évoluer cette liste au fur et à mesure et pourquoi pas ajouter vos recommandations si vous en avez. 😀</p>
]]></content>
		</item>
		
		<item>
			<title>🎤 State of Elasticsearch and PHP in 2023</title>
			<link>/posts/2023-03-08-state-of-elasticsearch-php/</link>
			<pubDate>Wed, 08 Mar 2023 00:00:00 +0000</pubDate><guid>/posts/2023-03-08-state-of-elasticsearch-php/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<h2 id="abstract">Abstract<a href="#abstract" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Elasticsearch, on en parle partout mais on ne sait jamais par où commencer. Dans ce talk, je vous présente, avec les outils de 2023, comment mettre en place ce moteur de recherche dans un applicatif web, les optimisations à ne pas oublier et pleins d&rsquo;autres conseils pour améliorer votre prise en main et reprendre confiance en Elasticsearch!</p>
<h2 id="links">Links<a href="#links" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p><a href="https://twitter.com/ElasticFR/status/1633447473065721858">https://twitter.com/ElasticFR/status/1633447473065721858</a>
<a href="https://www.elasticon.com/event/e4d4b4ed-77a7-48d2-b711-74fea8341273/summary">https://www.elasticon.com/event/e4d4b4ed-77a7-48d2-b711-74fea8341273/summary</a></p>
]]></content>
		</item>
		
		<item>
			<title>✍️ Use Bayesian Averages to Improve Rating Sorting in your Elasticsearch Index</title>
			<link>/posts/2022-06-08-bayesian-average-improve-rating-sorting-elasticsearch/</link>
			<pubDate>Wed, 08 Jun 2022 00:00:00 +0000</pubDate><guid>/posts/2022-06-08-bayesian-average-improve-rating-sorting-elasticsearch/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p>Disclaimer: This post is a copy of my <a href="https://jolicode.com/blog/use-bayesian-averages-to-improve-rating-sorting-in-your-elasticsearch-index">JoliCode blog post</a>.</p>
<p><em>EDIT</em>: There were two typos in this article. In the formula we were wrong about the position of one of the arguments which made it wrong, we invite you to check again the formula so you can fix it too. And in the Elasticsearch painless script, a subtlety of the Java language will make a division of a double variable by a long variable outputting a long rounded (badly indeed) so you have to be sure all your variables are doubles. If you never read the article, it is now correct and you don&rsquo;t have to worry about it!</p>
<p>Our modern life is dominated by ratings, whether we go shopping, to the restaurant and even when looking for a spa. But how do we make sure these ratings are accurate?
How could you, with an Elasticsearch cluster, sort by these ratings?</p>
<p>In this post we will work with a list of <code>Book</code>. When using the average rating sorting, we will get strange results: a <code>Book</code> with only one vote and a 10 average rating on top of the listing followed by another one with 100 votes and an average rating of 9,2 - this doesn’t make any sense.</p>
<p>This is why you would want to use some complex formula to take everything into account. In this post we’ll present to you what the Bayesian average is and how you can use it with a database and Elasticsearch to improve your sorting.</p>
<h2 id="bayesian-average-what">Bayesian average, what?<a href="#bayesian-average-what" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<blockquote>
<p>A <strong>Bayesian average</strong> is a method of estimating the mean of a population using outside information, especially a pre-existing belief, which is factored into the calculation. This is a central feature of Bayesian interpretation. This is useful when the available data set is small.</p>
</blockquote>
<p>From <a href="https://en.wikipedia.org/wiki/Bayesian_average">https://en.wikipedia.org/wiki/Bayesian_average</a>.</p>
<p>I’m feeling the same as you are now: that quote didn’t help me understand and I was even more intrigued when reading this; what helped me was reading a practical usage of this method.</p>
<p>In our case, we have a <code>Book</code> resource that can be rated from 1 to 10. So all the users of our website can rate that <code>Book</code> and when the users are searching in the catalog, we want the default sorting to be based on this rating.</p>
<p>That&rsquo;s why we want to weigh this average by the number of voters of all the <code>Book</code>. Here is a more comprehensive formula:</p>
<p><img src="/media/original/2022/average_formula.png" alt="Bayesian average formula"></p>
<ul>
<li>“b” annotation refer to book related values;</li>
<li>“ab” annotation refer to “all book” related values;</li>
<li>“v” indicate the count of votes, so “Bv” will be the count of votes for a specific book, and “ABv” will be the count of votes for all the books;</li>
<li>“c” indicate the count of related object;</li>
<li>“a” will correspond to the average of the related object.</li>
</ul>
<p>We did <a href="https://docs.google.com/spreadsheets/d/1wz2gXRsEhU_u2l30NPFF3RHXgvBv9BR7L0BAH4veEUQ/edit?usp=sharing">a sheet</a> to see how that formula will behave based on several factors. So you can understand how the formula will distribute your elements based on their ratings.</p>
<h2 id="implementation">Implementation<a href="#implementation" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Now let’s apply this to our application! Here, we will use a Symfony application which allows us to bypass a lot of bootstrap and go directly to the interesting part. We have a database that contains users, books and ratings. Also, We have a <code>book</code> index in an Elasticsearch cluster that reflects our database books but normalized, with a extra field as following in our mapping:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">mappings</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dynamic</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># other properties ...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">bayesianAverage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">float</span><span class="w">
</span></span></span></code></pre></div><p><code>bayesianAverage</code> will be used to sort the <code>book</code> index later on. We will need to fill that value. During index creation, we transform the Book entity into an Elasticsearch document, and we compute the Bayesian average.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">BookTransformer</span> <span class="k">implements</span> <span class="nx">EntityTransformer</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">function</span> <span class="nf">createModel</span><span class="p">(</span><span class="nx">Book</span> <span class="nv">$book</span><span class="p">,</span> <span class="k">array</span> <span class="nv">$normalized</span><span class="p">)</span><span class="o">:</span> <span class="nx">BookModel</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$model</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BookModel</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// some assignations to fill my model
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nv">$distribution</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">ratingsDistribution</span><span class="o">-&gt;</span><span class="na">get</span><span class="p">(</span><span class="nv">$book</span><span class="o">-&gt;</span><span class="na">getId</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">RatingModel</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set1</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span> <span class="c1">// Count of &#34;1&#34; ratings for this book
</span></span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set2</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">2</span><span class="p">]);</span> <span class="c1">// Count of &#34;2&#34; ratings for this book...
</span></span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set3</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">3</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set4</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">4</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set5</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">5</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set6</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">6</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set7</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">7</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set8</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">8</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set9</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">9</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">set10</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">[</span><span class="mi">10</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ratings</span><span class="o">-&gt;</span><span class="na">setCount</span><span class="p">(</span><span class="nx">array_sum</span><span class="p">(</span><span class="nv">$distribution</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$model</span><span class="o">-&gt;</span><span class="na">setRatings</span><span class="p">(</span><span class="nv">$ratings</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nv">$model</span><span class="o">-&gt;</span><span class="na">setBayesianAverage</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">bayesianAverage</span><span class="o">-&gt;</span><span class="na">compute</span><span class="p">(</span><span class="nv">$ratings</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nv">$model</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Here we have a <code>RatingsDistribution</code> service<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> we use to compile all votes for a given <code>Book</code> within an array of integers with all possible ratings! That way we can assign our <code>Book</code> model with the number of voters for each rating and the total count of voters. These values will be used later on to calculate the Bayesian average from inside Elasticsearch.</p>
<p>And we are also seeing a <code>BayesianAverage</code> service we used to do calculations. Here is what it looks like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">BayesianAverage</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">allVotesCount</span><span class="p">()</span><span class="o">:</span> <span class="nx">int</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$result</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">connection</span><span class="o">-&gt;</span><span class="na">fetchAssociative</span><span class="p">(</span><span class="s1">&#39;SELECT COUNT(id) as count FROM rating;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="nx">int</span><span class="p">)</span> <span class="nv">$result</span><span class="p">[</span><span class="s1">&#39;count&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">allAverageRating</span><span class="p">()</span><span class="o">:</span> <span class="nx">float</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$result</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">connection</span><span class="o">-&gt;</span><span class="na">fetchAssociative</span><span class="p">(</span><span class="s1">&#39;SELECT AVG(r.rating) as average FROM rating r;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="nx">float</span><span class="p">)</span> <span class="nv">$result</span><span class="p">[</span><span class="s1">&#39;average&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">allCount</span><span class="p">()</span><span class="o">:</span> <span class="nx">int</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$result</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">connection</span><span class="o">-&gt;</span><span class="na">fetchAssociative</span><span class="p">(</span><span class="s1">&#39;SELECT COUNT(b.id) as count FROM book b;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="nx">int</span><span class="p">)</span> <span class="nv">$result</span><span class="p">[</span><span class="s1">&#39;count&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="k">function</span> <span class="nf">compute</span><span class="p">(</span><span class="nx">RatingModel</span> <span class="nv">$bookRatings</span><span class="p">)</span><span class="o">:</span> <span class="nx">float</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$bookCount</span> <span class="o">=</span> <span class="mi">0</span> <span class="o">===</span> <span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">getCount</span><span class="p">()</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">getCount</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$inter</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">allCount</span><span class="p">()</span> <span class="o">/</span> <span class="p">(</span><span class="nv">$bookCount</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">allVotesCount</span><span class="p">()</span> <span class="o">/</span> <span class="nv">$bookCount</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$avg</span> <span class="o">=</span> <span class="p">((</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get1Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get2Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get3Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">3</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get4Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">4</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get5Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">5</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get6Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">6</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get7Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">7</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get8Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">8</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get9Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">9</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nv">$bookRatings</span><span class="o">-&gt;</span><span class="na">get10Count</span><span class="p">()</span> <span class="o">*</span> <span class="mi">10</span><span class="p">))</span> <span class="o">/</span> <span class="nv">$bookCount</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nv">$inter</span> <span class="o">*</span> <span class="nv">$avg</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="nv">$inter</span><span class="p">)</span> <span class="o">*</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">allAverageRating</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>For our implementation we will cache global variables with a 1 hour time to live:</p>
<ul>
<li>all <code>Book</code>votes count in a <code>bayesian_all_votes</code> key;</li>
<li>all <code>Book</code> average rating in a <code>bayesian_all_average_rating</code> key;</li>
<li><code>Book</code> count in a <code>bayesian_all_count</code> key.</li>
</ul>
<p>Then we have a <code>calculate</code> method which makes all required calculations to get our Bayesian average for a given <code>Book</code> ratings distribution. Now, we are able to fill our index on creation with a correct Bayesian average on all our <code>Book</code> models!</p>
<h2 id="handling-new-votes">Handling new votes<a href="#handling-new-votes" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>But it works only on indexation, what happens if a user gives a new vote? All values we have calculated will be wrong and have to be refreshed!</p>
<p>First thing we will solve is updating the related model in Elasticsearch when a user is submitting a new vote on a <code>Book</code>. When we have a new vote, we will update the Elasticsearch model with a stored painless script<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> called <code>bayesian-update</code> in our Elasticsearch cluster. It will update the document values and update the <code>bayesianAverage</code> field, here is what the script will look like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// only for updates</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="na">addedRating</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="n">params</span><span class="p">.</span><span class="na">addedRating</span><span class="p">.</span><span class="na">toString</span><span class="p">()</span><span class="o">]++</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="err">&#39;</span><span class="n">count</span><span class="err">&#39;</span><span class="o">]++</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="na">removedRating</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="n">params</span><span class="p">.</span><span class="na">removedRating</span><span class="p">.</span><span class="na">toString</span><span class="p">()</span><span class="o">]--</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="err">&#39;</span><span class="n">count</span><span class="err">&#39;</span><span class="o">]--</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// classic Bayesian average calculations</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;1&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;2&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s3</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;3&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s4</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;4&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s5</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;5&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s6</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;6&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s7</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;7&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s8</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;8&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s9</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="sc">&#39;9&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">long</span><span class="w"> </span><span class="n">s10</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="err">&#39;</span><span class="n">10</span><span class="err">&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">double</span><span class="w"> </span><span class="n">count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="p">.</span><span class="na">ratings</span><span class="o">[</span><span class="err">&#39;</span><span class="n">count</span><span class="err">&#39;</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">double</span><span class="w"> </span><span class="n">inter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">params</span><span class="p">.</span><span class="na">allCount</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="p">(</span><span class="n">count</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="na">allVotesCount</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">count</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kt">double</span><span class="w"> </span><span class="n">avg</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">((</span><span class="n">s1</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">1</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s2</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">2</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s3</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">3</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s4</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">4</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s5</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">5</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s6</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">6</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s7</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">7</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s8</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">8</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s9</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">9</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">s10</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">10</span><span class="p">))</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">ctx</span><span class="p">.</span><span class="na">_source</span><span class="o">[</span><span class="err">&#39;</span><span class="n">bayesianAverage</span><span class="err">&#39;</span><span class="o">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">inter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">avg</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="n">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">inter</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">params</span><span class="p">.</span><span class="na">allAverageRating</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>This script use the following parameters:</p>
<ul>
<li><code>allCount</code> corresponds to the method with the same name in our <code>BayesianAverage</code> service, it will contains the number of <code>Book</code> we have in our database;</li>
<li><code>allVotesCount</code> also corresponds to the method with the same name in our <code>BayesianAverage</code> service and it will contain the number of votes for all the <code>Book</code>;</li>
<li><code>allAverageRating</code> is also mirrored to the <code>BayesianAverage</code> service and contains the global average on all <code>Book</code> votes;</li>
<li>then <code>addedRating</code> corresponds to the rating that was added on that <code>Book</code>;</li>
<li>lastly, <code>removedRating</code> is here because in my application, a user can update his rating to another value so this field will contain the value that was removed if a user is updating his vote.</li>
</ul>
<p>Now we can run our script each time there is a new vote by using this request onto the Elasticsearch cluster:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">POST</span> <span class="nx">book</span><span class="o">/</span><span class="nx">doc</span><span class="o">/</span><span class="mi">67433</span><span class="o">/</span><span class="nx">_update</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;script&#34;</span> <span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;id&#34;</span><span class="o">:</span> <span class="s2">&#34;bayesian-update&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;params&#34;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;addedRating&#34;</span><span class="o">:</span> <span class="mi">7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;removedRating&#34;</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;allCount&#34;</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;allVotesCount&#34;</span><span class="o">:</span> <span class="mi">30160</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;allAverageRating&#34;</span><span class="o">:</span> <span class="mf">6.51</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>That request will trigger an execution of the <code>bayesian-update</code> script on our model and update it automatically, without having to send the complete <code>Book</code> model again.</p>
<h2 id="keep-the-bayesian-average-up-to-date">Keep the Bayesian average up-to-date<a href="#keep-the-bayesian-average-up-to-date" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Now that new votes are managed, a big problem remains: Bayesian average is based on an average for a specific book, <strong>and</strong> the average for <strong>all books</strong>, so how can we be sure that our value is still relevant in time when more <code>Book</code> and votes are added?</p>
<p>In our case, we have a lot of <code>Book</code> and thanks to that, the difference in the Bayesian average result will be too small to be significant when new <code>Book</code> or new votes are added. That is why we chose to make a daily update on all our <code>Book</code> for the <code>bayesianAverage</code> field. This time frame should be based on your website traffic and if you need a really precise sorting or if some small variations are allowed. The more documents you have, the less frequently you will need to update since a new document won’t impact the global average that much.</p>
<p>For this update I created a new script called <code>bayesian-refresh</code> which is very similar to the first script but without the rating update part (so both <code>addedRating</code> and <code>removedRating</code> are not required parameters anymore).</p>
<p>To trigger this script, we use the bulk update API in Elasticsearch to run the script on all documents by selecting them by their ids as following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">POST</span> <span class="nx">book</span><span class="o">/</span><span class="nx">_bulk</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;update&#34;</span><span class="o">:</span> <span class="p">{</span><span class="s2">&#34;_id&#34;</span> <span class="o">:</span> <span class="mi">67433</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;script&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;id&#34;</span><span class="o">:</span> <span class="s2">&#34;bayesian-refresh&#34;</span><span class="p">,</span> <span class="s2">&#34;params&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;allCount&#34;</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s2">&#34;allVotesCount&#34;</span><span class="o">:</span> <span class="mi">30160</span><span class="p">,</span> <span class="s2">&#34;allAverageRating&#34;</span><span class="o">:</span> <span class="mf">6.51</span><span class="p">}}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;update&#34;</span><span class="o">:</span> <span class="p">{</span><span class="s2">&#34;_id&#34;</span> <span class="o">:</span> <span class="mi">67434</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;script&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;id&#34;</span><span class="o">:</span> <span class="s2">&#34;bayesian-refresh&#34;</span><span class="p">,</span> <span class="s2">&#34;params&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;allCount&#34;</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s2">&#34;allVotesCount&#34;</span><span class="o">:</span> <span class="mi">30160</span><span class="p">,</span> <span class="s2">&#34;allAverageRating&#34;</span><span class="o">:</span> <span class="mf">6.51</span><span class="p">}}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;update&#34;</span><span class="o">:</span> <span class="p">{</span><span class="s2">&#34;_id&#34;</span> <span class="o">:</span> <span class="mi">67435</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;script&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;id&#34;</span><span class="o">:</span> <span class="s2">&#34;bayesian-refresh&#34;</span><span class="p">,</span> <span class="s2">&#34;params&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;allCount&#34;</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s2">&#34;allVotesCount&#34;</span><span class="o">:</span> <span class="mi">30160</span><span class="p">,</span> <span class="s2">&#34;allAverageRating&#34;</span><span class="o">:</span> <span class="mf">6.51</span><span class="p">}}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;update&#34;</span><span class="o">:</span> <span class="p">{</span><span class="s2">&#34;_id&#34;</span> <span class="o">:</span> <span class="mi">67436</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;script&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;id&#34;</span><span class="o">:</span> <span class="s2">&#34;bayesian-refresh&#34;</span><span class="p">,</span> <span class="s2">&#34;params&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;allCount&#34;</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s2">&#34;allVotesCount&#34;</span><span class="o">:</span> <span class="mi">30160</span><span class="p">,</span> <span class="s2">&#34;allAverageRating&#34;</span><span class="o">:</span> <span class="mf">6.51</span><span class="p">}}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;update&#34;</span><span class="o">:</span> <span class="p">{</span><span class="s2">&#34;_id&#34;</span> <span class="o">:</span> <span class="mi">67437</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;script&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;id&#34;</span><span class="o">:</span> <span class="s2">&#34;bayesian-refresh&#34;</span><span class="p">,</span> <span class="s2">&#34;params&#34;</span> <span class="o">:</span> <span class="p">{</span><span class="s2">&#34;allCount&#34;</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s2">&#34;allVotesCount&#34;</span><span class="o">:</span> <span class="mi">30160</span><span class="p">,</span> <span class="s2">&#34;allAverageRating&#34;</span><span class="o">:</span> <span class="mf">6.51</span><span class="p">}}}</span>
</span></span></code></pre></div><p>Sadly Elasticsearch doesn’t provide a way to trigger a script on all documents of an index so we’re forced to do it that way<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.</p>
<h2 id="conclusion">Conclusion<a href="#conclusion" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Thanks to this formula and Elasticsearch we are now able to sort our <code>Book</code> listing per ratings based on the whole population of <code>Book</code> which makes this sort even more relevant! If you need this, you should consider “freshness” of the votes, in our case it doesn’t matter but in some projects, you will want to put less weight on an older vote. But thanks to  this post you should be able to setup a basic sort based on your newly Bayesian average field 🎉.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">GET</span> <span class="nx">book</span><span class="o">/</span><span class="nx">_search</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;query&#34;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;match_all&#34;</span><span class="o">:</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s2">&#34;sort&#34;</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="s2">&#34;bayesianAverage&#34;</span><span class="o">:</span> <span class="s2">&#34;desc&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This post was greatly inspired by: <a href="https://www.evanmiller.org/bayesian-average-ratings.html">https://www.evanmiller.org/bayesian-average-ratings.html</a> If you want more mathematical details you should take a look ! 😉 Also our formula won’t match that one because we simplified some concepts and removed others, like time, since we don’t need these in our case.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>An extract of that <code>RatingsDistribution</code> class if you are interested about it: <a href="https://gist.github.com/Korbeil/9e00073c29a26f55160abddcc9888757">https://gist.github.com/Korbeil/9e00073c29a26f55160abddcc9888757</a>. Keep in mind that our <code>Rating</code> entity is composed of 3 fields: a <code>Book</code> relation, an <code>User</code> relation and a <code>rating</code> (integer from 1 to 10).&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Painless is a scripting language provided by Elasticsearch to create custom behaviors within Elasticsearch <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-painless.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-painless.html</a>.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>We could use the Reindex API instead of the Bulk update, but it requires to create a new index, will all the complexity it adds when you have real-time updates happening live.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content>
		</item>
		
		<item>
			<title>🎤 A tale about ingestion</title>
			<link>/posts/2022-02-11-a-tale-about-ingestion/</link>
			<pubDate>Fri, 11 Feb 2022 00:00:00 +0000</pubDate><guid>/posts/2022-02-11-a-tale-about-ingestion/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<h2 id="abstract">Abstract<a href="#abstract" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Avant même d&rsquo;effectuer une recherche dans Elasticsearch, vous devez l&rsquo;alimenter avec vos données. Dans cet présentation, je passerai en revue tous les moyens que nous avons utilisés pour envoyer des millions de documents à nos index, des plus mauvais aux plus optimisés.</p>
<p>Mais même avec les optimisations, l&rsquo;ingestion peut prendre du temps. Pendant ce temps, notre index en ligne pourrait avoir des opérations d&rsquo;écriture que nous devons également répliquer sur notre nouvel index ! Je vous expliquerai donc aussi comment nous avons propagé ces changements aux deux index pour que tout aille bien une fois le nouvel index en ligne.</p>
<h2 id="video">Video<a href="#video" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Embed are blocked for this talk, but you can see the video on Elastic website: <a href="https://community-conference.elastic.co/session/305686">https://community-conference.elastic.co/session/305686</a></p>
]]></content>
		</item>
		
		<item>
			<title>✍️ Rate limit your Symfony APIs!</title>
			<link>/posts/2021-11-22-rate-limit-symfony-api/</link>
			<pubDate>Mon, 22 Nov 2021 00:00:00 +0000</pubDate><guid>/posts/2021-11-22-rate-limit-symfony-api/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p>Disclaimer: This post is a copy of my <a href="https://jolicode.com/blog/rate-limit-your-symfony-apis">JoliCode blog post</a>.</p>
<p>Sometimes, you need to put some custom rate limits on your APIs! In this article I&rsquo;ll show you how you can combine the <code>symfony/rate-limiter</code> component and some usual controllers.</p>
<h3 id="ratelimit-configuration">RateLimit configuration<a href="#ratelimit-configuration" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>The goal here is to have the following rate limit configuration works thanks to PHP8 attributes on any route you want:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">framework</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rate_limiter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">account_create</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">policy</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;fixed_window&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">limit</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;60 minutes&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">account_modify</span><span class="p">:</span><span class="w"> </span><span class="c"># account activate, change profile</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">policy</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;fixed_window&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">limit</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;60 minutes&#39;</span><span class="w">
</span></span></span></code></pre></div><p>This article isn&rsquo;t about the component itself so I&rsquo;ll recommend you to read the <a href="https://symfony.com/doc/current/rate_limiter.html">Symfony&rsquo;s RateLimiter documentation</a> if you want to understand how it works and how to create rules.</p>
<h3 id="attribute">Attribute<a href="#attribute" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>First of all, we need an attribute that we will use to declare routes that need to be rate limited. We will require a configuration key to identify which rate limit configuration we should take:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[Attribute(Attribute::TARGET_METHOD)]
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RateLimiting</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="k">public</span> <span class="nx">string</span> <span class="nv">$configuration</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="controller">Controller<a href="#controller" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Now let&rsquo;s use our attribute on some controller:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">#[RateLimiting(&#39;account_create&#39;)]
</span></span></span><span class="line"><span class="cl"><span class="c1">#[Route(&#39;/create&#39;, methods: [&#39;POST&#39;])]
</span></span></span><span class="line"><span class="cl"><span class="k">public</span> <span class="k">function</span> <span class="nf">createAccount</span><span class="p">()</span><span class="o">:</span> <span class="nx">JsonResponse</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// your controller logic ...
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>And that&rsquo;s all you need to do to declare a route as rate limited 👌</p>
<h3 id="compilerpass">CompilerPass<a href="#compilerpass" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>But before it works we need to make Symfony understand these attributes. So we need a CompilerPass to store all routes that have our attribute to avoid reflection at runtime:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RateLimitingPass</span> <span class="k">implements</span> <span class="nx">CompilerPassInterface</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">process</span><span class="p">(</span><span class="nx">ContainerBuilder</span> <span class="nv">$container</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$container</span><span class="o">-&gt;</span><span class="na">hasDefinition</span><span class="p">(</span><span class="nx">ApplyRateLimitingListener</span><span class="o">::</span><span class="na">class</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">throw</span> <span class="k">new</span> <span class="nx">\LogicException</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;Can not configure non-existent service %s&#39;</span><span class="p">,</span> <span class="nx">ApplyRateLimitingListener</span><span class="o">::</span><span class="na">class</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$taggedServices</span> <span class="o">=</span> <span class="nv">$container</span><span class="o">-&gt;</span><span class="na">findTaggedServiceIds</span><span class="p">(</span><span class="s1">&#39;controller.service_arguments&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="sd">/** @var Definition[] $serviceDefinitions */</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$serviceDefinitions</span> <span class="o">=</span> <span class="nx">array_map</span><span class="p">(</span><span class="nx">fn</span> <span class="p">(</span><span class="nx">string</span> <span class="nv">$id</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nv">$container</span><span class="o">-&gt;</span><span class="na">getDefinition</span><span class="p">(</span><span class="nv">$id</span><span class="p">),</span> <span class="nx">array_keys</span><span class="p">(</span><span class="nv">$taggedServices</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$rateLimiterClassMap</span> <span class="o">=</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$serviceDefinitions</span> <span class="k">as</span> <span class="nv">$serviceDefinition</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$controllerClass</span> <span class="o">=</span> <span class="nv">$serviceDefinition</span><span class="o">-&gt;</span><span class="na">getClass</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$reflClass</span> <span class="o">=</span> <span class="nv">$container</span><span class="o">-&gt;</span><span class="na">getReflectionClass</span><span class="p">(</span><span class="nv">$controllerClass</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">foreach</span> <span class="p">(</span><span class="nv">$reflClass</span><span class="o">-&gt;</span><span class="na">getMethods</span><span class="p">(</span><span class="nx">\ReflectionMethod</span><span class="o">::</span><span class="na">IS_PUBLIC</span> <span class="o">|</span> <span class="o">~</span><span class="nx">\ReflectionMethod</span><span class="o">::</span><span class="na">IS_STATIC</span><span class="p">)</span> <span class="k">as</span> <span class="nv">$reflMethod</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$attributes</span> <span class="o">=</span> <span class="nv">$reflMethod</span><span class="o">-&gt;</span><span class="na">getAttributes</span><span class="p">(</span><span class="nx">RateLimiting</span><span class="o">::</span><span class="na">class</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">\count</span><span class="p">(</span><span class="nv">$attributes</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="p">[</span><span class="nv">$attribute</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$attributes</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">          <span class="nv">$serviceKey</span> <span class="o">=</span> <span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;limiter.%s&#39;</span><span class="p">,</span> <span class="nv">$attribute</span><span class="o">-&gt;</span><span class="na">newInstance</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">configuration</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">          <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$container</span><span class="o">-&gt;</span><span class="na">hasDefinition</span><span class="p">(</span><span class="nv">$serviceKey</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">throw</span> <span class="k">new</span> <span class="nx">\RuntimeException</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="nx">‘Service</span> <span class="o">%</span><span class="nx">s</span> <span class="k">not</span> <span class="nx">found’</span><span class="p">,</span> <span class="nv">$serviceKey</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">          <span class="nv">$classMapKey</span> <span class="o">=</span> <span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;%s::%s&#39;</span><span class="p">,</span> <span class="nv">$serviceDefinition</span><span class="o">-&gt;</span><span class="na">getClass</span><span class="p">(),</span> <span class="nv">$reflMethod</span><span class="o">-&gt;</span><span class="na">getName</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">          <span class="nv">$rateLimiterClassMap</span><span class="p">[</span><span class="nv">$classMapKey</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$container</span><span class="o">-&gt;</span><span class="na">getDefinition</span><span class="p">(</span><span class="nv">$serviceKey</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$container</span><span class="o">-&gt;</span><span class="na">getDefinition</span><span class="p">(</span><span class="nx">ApplyRateLimitingListener</span><span class="o">::</span><span class="na">class</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">setArgument</span><span class="p">(</span><span class="s1">&#39;$rateLimiterClassMap&#39;</span><span class="p">,</span> <span class="nv">$rateLimiterClassMap</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Here we get all controllers and we check on each method if they have our attribute and then we link the route to the corresponding rate limit service and add it in our cache.</p>
<h3 id="listener">Listener<a href="#listener" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Now that Symfony understands our attribute and cache it, we need an event listener to hook on the <code>kernel.controller</code> event and check if our rate limit is fine or not.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ApplyRateLimitingListener</span> <span class="k">implements</span> <span class="nx">EventSubscriberInterface</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="nx">TokenStorageInterface</span> <span class="nv">$tokenStorage</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="sd">/** @var RateLimiterFactory[] */</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">array</span> <span class="nv">$rateLimiterClassMap</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="nx">bool</span> <span class="nv">$isRateLimiterEnabled</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="nx">RequestStack</span> <span class="nv">$requestStack</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="nx">RoleHierarchyInterface</span> <span class="nv">$roleHierarchy</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">onKernelController</span><span class="p">(</span><span class="nx">KernelEvent</span> <span class="nv">$event</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">isRateLimiterEnabled</span> <span class="o">||</span> <span class="o">!</span><span class="nv">$event</span><span class="o">-&gt;</span><span class="na">isMainRequest</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$request</span> <span class="o">=</span> <span class="nv">$event</span><span class="o">-&gt;</span><span class="na">getRequest</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="sd">/** @var string $controllerClass */</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$controllerClass</span> <span class="o">=</span> <span class="nv">$request</span><span class="o">-&gt;</span><span class="na">attributes</span><span class="o">-&gt;</span><span class="na">get</span><span class="p">(</span><span class="s1">&#39;_controller&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$rateLimiter</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">rateLimiterClassMap</span><span class="p">[</span><span class="nv">$controllerClass</span><span class="p">]</span> <span class="o">??</span> <span class="k">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="k">null</span> <span class="o">===</span> <span class="nv">$rateLimiter</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span><span class="p">;</span> <span class="c1">// no rate limit service was assigned for this controller
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$token</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">tokenStorage</span><span class="o">-&gt;</span><span class="na">getToken</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$token</span> <span class="nx">instanceof</span> <span class="nx">TokenInterface</span> <span class="o">&amp;&amp;</span> <span class="nx">in_array</span><span class="p">(</span><span class="s1">&#39;ROLE_GLOBAL_MODERATOR&#39;</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">roleHierarchy</span><span class="o">-&gt;</span><span class="na">getReachableRoleNames</span><span class="p">((</span><span class="nv">$token</span><span class="o">-&gt;</span><span class="na">getRoleNames</span><span class="p">()))))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span><span class="p">;</span> <span class="c1">// we ignore rate limit for site moderator &amp; upper roles
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">ensureRateLimiting</span><span class="p">(</span><span class="nv">$request</span><span class="p">,</span> <span class="nv">$rateLimiter</span><span class="p">,</span> <span class="nv">$request</span><span class="o">-&gt;</span><span class="na">getClientIp</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="k">function</span> <span class="nf">ensureRateLimiting</span><span class="p">(</span><span class="nx">Request</span> <span class="nv">$request</span><span class="p">,</span> <span class="nx">RateLimiterFactory</span> <span class="nv">$rateLimiter</span><span class="p">,</span> <span class="nx">string</span> <span class="nv">$clientIp</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$limit</span> <span class="o">=</span> <span class="nv">$rateLimiter</span><span class="o">-&gt;</span><span class="na">create</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;rate_limit_ip_%s&#39;</span><span class="p">,</span> <span class="nv">$clientIp</span><span class="p">))</span><span class="o">-&gt;</span><span class="na">consume</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$request</span><span class="o">-&gt;</span><span class="na">attributes</span><span class="o">-&gt;</span><span class="na">set</span><span class="p">(</span><span class="s1">&#39;rate_limit&#39;</span><span class="p">,</span> <span class="nv">$limit</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$limit</span><span class="o">-&gt;</span><span class="na">ensureAccepted</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$user</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">tokenStorage</span><span class="o">-&gt;</span><span class="na">getToken</span><span class="p">()</span><span class="o">?-&gt;</span><span class="na">getUser</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$user</span> <span class="nx">instanceof</span> <span class="nx">User</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$limit</span> <span class="o">=</span> <span class="nv">$rateLimiter</span><span class="o">-&gt;</span><span class="na">create</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;rate_limit_user_%s&#39;</span><span class="p">,</span> <span class="nv">$user</span><span class="o">-&gt;</span><span class="na">getId</span><span class="p">()))</span><span class="o">-&gt;</span><span class="na">consume</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$request</span><span class="o">-&gt;</span><span class="na">attributes</span><span class="o">-&gt;</span><span class="na">set</span><span class="p">(</span><span class="s1">&#39;rate_limit&#39;</span><span class="p">,</span> <span class="nv">$limit</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$limit</span><span class="o">-&gt;</span><span class="na">ensureAccepted</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">static</span> <span class="k">function</span> <span class="nf">getSubscribedEvents</span><span class="p">()</span><span class="o">:</span> <span class="k">array</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="nx">KernelEvents</span><span class="o">::</span><span class="na">CONTROLLER</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="s1">&#39;onKernelController&#39;</span><span class="p">,</span> <span class="mi">1024</span><span class="p">]];</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In this example, I chose to ignore rate limits for our global moderator roles. For all other users I check the rate limit on two levels: IP then User if they are logged. That way we can avoid any user spamming from different IPs. These are business rules I use but you can custom it the way you want.</p>
<p>Also you can see that we share the rate limit service before each check: if there is a rate limit issue then an exception will be thrown (thanks to the <code>ensureAccepted</code> method) and the second check won&rsquo;t happen so we will have the correct rate limit service shared.</p>
<h3 id="headers">Headers<a href="#headers" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Finally, to use that shared rate limit service, we can generate some headers to indicate how the rate limit went and other metrics:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">final</span> <span class="k">class</span> <span class="nc">RateLimitingResponseHeadersListener</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">public</span> <span class="k">function</span> <span class="nf">onKernelResponse</span><span class="p">(</span><span class="nx">ResponseEvent</span> <span class="nv">$event</span><span class="p">)</span><span class="o">:</span> <span class="nx">void</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">((</span><span class="nv">$rateLimit</span> <span class="o">=</span> <span class="nv">$event</span><span class="o">-&gt;</span><span class="na">getRequest</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">attributes</span><span class="o">-&gt;</span><span class="na">get</span><span class="p">(</span><span class="s1">&#39;rate_limit&#39;</span><span class="p">))</span> <span class="nx">instanceof</span> <span class="nx">RateLimit</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nv">$event</span><span class="o">-&gt;</span><span class="na">getResponse</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">headers</span><span class="o">-&gt;</span><span class="na">add</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;RateLimit-Remaining&#39;</span> <span class="o">=&gt;</span> <span class="nv">$rateLimit</span><span class="o">-&gt;</span><span class="na">getRemainingTokens</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;RateLimit-Reset&#39;</span> <span class="o">=&gt;</span> <span class="nx">time</span><span class="p">()</span> <span class="o">-</span> <span class="nv">$rateLimit</span><span class="o">-&gt;</span><span class="na">getRetryAfter</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">getTimestamp</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;RateLimit-Limit&#39;</span> <span class="o">=&gt;</span> <span class="nv">$rateLimit</span><span class="o">-&gt;</span><span class="na">getLimit</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">      <span class="p">]);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>I took the headers names from the <a href="https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html">RateLimit headers RFC</a>, it&rsquo;s still a draft but theses headers are already widely used.</p>
<p>And here we are - with only a few lines of code, you can add a rate limit to any route by adding your new <code>RateLimiting</code> attribute!</p>
]]></content>
		</item>
		
		<item>
			<title>🎤 You need an AutoMapper, but you don&#39;t know it yet!</title>
			<link>/posts/2021-06-17-you-need-an-automapper/</link>
			<pubDate>Thu, 17 Jun 2021 00:00:00 +0000</pubDate><guid>/posts/2021-06-17-you-need-an-automapper/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<h2 id="abstract">Abstract<a href="#abstract" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>It is quite common for a developer to have to map data. In the long run, this kind of code is boring to write. Why not generate it automatically? This is the promise that AutoMapper tries to solve: generating the code needed to map one data to another (array, class, etc.)</p>
<p>Through this talk, we will first see the Symfony Serializer and how it works.</p>
<p>Then, we will discover how AutoMapper takes advantage of the Serializer ecosystem while revolutionizing the Normalizer concept by adding code generation, thus drastically boosting performance!</p>
<h2 id="video">Video<a href="#video" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Available here: <a href="https://live.symfony.com/account/replay/video/593">https://live.symfony.com/account/replay/video/593</a></p>
<p>Also, slides are available there: <a href="https://speakerdeck.com/korbeil/you-need-an-automapper-but-you-dont-know-it-yet">https://speakerdeck.com/korbeil/you-need-an-automapper-but-you-dont-know-it-yet</a></p>
]]></content>
		</item>
		
	</channel>
</rss>
