<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thérage Kévin</title>
    <description>The latest articles on DEV Community by Thérage Kévin (@ktherage).</description>
    <link>https://dev.to/ktherage</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1030336%2F0185bf29-7ff1-4e46-860c-c496c40f338d.png</url>
      <title>DEV Community: Thérage Kévin</title>
      <link>https://dev.to/ktherage</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ktherage"/>
    <language>en</language>
    <item>
      <title>Construire un assistant de code review RAG avec PHP, Ollama et Qdrant</title>
      <dc:creator>Thérage Kévin</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:48:53 +0000</pubDate>
      <link>https://dev.to/ktherage/construire-un-assistant-de-code-review-rag-avec-php-ollama-et-qdrant-4lob</link>
      <guid>https://dev.to/ktherage/construire-un-assistant-de-code-review-rag-avec-php-ollama-et-qdrant-4lob</guid>
      <description>&lt;p&gt;Les &lt;a href="https://fr.wikipedia.org/wiki/Grand_mod%C3%A8le_de_langage" rel="noopener noreferrer"&gt;LLM&lt;/a&gt; sont excellents pour produire des code reviews qui sonnent juste. Mais « sonner juste » n'est pas la même chose qu'être utiles. Une review qui vous dit de « corrige le Code Style » est correcte mais inutile — chaque projet applique Code Style qui peut différer.&lt;/p&gt;

&lt;p&gt;Symfony a plus de &lt;strong&gt;20 ans de code reviews publiques&lt;/strong&gt; sur &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Chaque PR mergée contient des commentaires de &lt;a href="https://github.com/nicolas-grekas" rel="noopener noreferrer"&gt;nicolas-grekas&lt;/a&gt;, &lt;a href="https://github.com/stof" rel="noopener noreferrer"&gt;stof&lt;/a&gt;, &lt;a href="https://github.com/dunglas" rel="noopener noreferrer"&gt;dunglas&lt;/a&gt;, &lt;a href="https://github.com/xabbuh" rel="noopener noreferrer"&gt;xabbuh&lt;/a&gt;, et des dizaines d'autres reviewers de la core team et de contributeurs. C'est une mine d'or de patterns de review spécifiques au domaine : quels arguments convainquent, quels patterns sont rejetés, ce que la communauté considère comme du bon code Symfony.&lt;/p&gt;

&lt;p&gt;Le problème ? Personne n'avait construit de moteur de recherche pour l'exploiter. Alors je l'ai fais 🤣.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le lore derrière cette idée folle
&lt;/h2&gt;

&lt;p&gt;Cette histoire prends sa source au &lt;a href="https://live.symfony.com/" rel="noopener noreferrer"&gt;Symfony Live de Paris&lt;/a&gt;. Comme vous l'imaginez, celui de cette année était très orienté IA. J'ai vu bon nombre de Talks en parlé et j'ai voulu jouer un peu avec cette nouveautée mais jusque là, je n'avais pas de cas concrét.&lt;/p&gt;

&lt;p&gt;J'y ai assisté au un Talk de &lt;a href="https://github.com/lyrixx" rel="noopener noreferrer"&gt;Grégoire Pineau&lt;/a&gt; ou il expliquais comment avec &lt;a href="https://ai.symfony.com/" rel="noopener noreferrer"&gt;Symfony AI&lt;/a&gt;, &lt;a href="https://clickhouse.com/fr" rel="noopener noreferrer"&gt;Clickhouse&lt;/a&gt; et &lt;a href="https://redirection.io/" rel="noopener noreferrer"&gt;redirection.io&lt;/a&gt; il avait mené à bien la migration d'un site E-Commerce en réduissant la perte de trafic.&lt;/p&gt;

&lt;p&gt;Plus tard est arrivé le &lt;a href="https://symfony.com/blog/new-in-symfony-8-1-http-less-symfony-applications" rel="noopener noreferrer"&gt;Console Bundle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;J'avais déjà demandé à un LLM de relire les modifications que j'avais faites &lt;em&gt;(sur des projets perso bien sûr)&lt;/em&gt; et, comme vous vous en doutez, j'ai obtenu des conseils du genre &lt;code&gt;pensez à utiliser l'injection de dépendances&lt;/code&gt;, &lt;code&gt;peut-être extraire cette logique dans un service&lt;/code&gt; ou &lt;code&gt;pense a vérifier le code style&lt;/code&gt;. Ces retours sont techniquements corrects, mais surtout universellements applicables et complètements génériques. Bref, rien qui ne puisse être corrigé avec de bons outils et un peu de rigeur.&lt;/p&gt;

&lt;p&gt;Sortant du Symfony Live, m'est venu une idée un peu dingue.&lt;br&gt;
Et si je pouvais demander au même LLM : &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;« Review ce code comme le ferait stof »&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;J'aurais des retours ultra pointu et un code qui en ressortira grandi.&lt;/p&gt;

&lt;p&gt;Ou&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;« Review ce code comme le ferait n'importe quel contributeur Symfony »&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;J'aurais alors le point de vu de l'ensemble de la communauté sur le code que je viens de créé. &lt;/p&gt;

&lt;p&gt;Ou bien même&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;« Review ce code comme le ferait n'importe quel membre de la core team Symfony »&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;J'aurais un panel d'experts à ma disposition pour m'expliquer ce qui ne vas pas dans ce que j'ai fait.&lt;/p&gt;

&lt;p&gt;C'est ce que fait &lt;strong&gt;Symfony Reviewer MCP&lt;/strong&gt; : un &lt;a href="https://fr.wikipedia.org/wiki/G%C3%A9n%C3%A9ration_%C3%A0_enrichissement_contextuel" rel="noopener noreferrer"&gt;moteur de recherche sémantique (RAG)&lt;/a&gt; sur l'ensemble des code reviews historiques de Symfony accessibles via l'API &lt;a href="https://docs.github.com/en/rest" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, exposé via le &lt;a href="https://fr.wikipedia.org/wiki/Model_Context_Protocol" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; le tout avec &lt;a href="https://ai.symfony.com/" rel="noopener noreferrer"&gt;Symfony AI&lt;/a&gt; et dans une application &lt;a href="https://symfony.com/blog/new-in-symfony-8-1-http-less-symfony-applications" rel="noopener noreferrer"&gt;Symfony HTTP-Less&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Je l'ai construit en &lt;a href="https://www.php.net/releases/8.5/" rel="noopener noreferrer"&gt;PHP 8.5&lt;/a&gt; avec &lt;a href="https://symfony.com/" rel="noopener noreferrer"&gt;Symfony 8.1&lt;/a&gt;, utilisant &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; en local pour la vectorisation avec un model issu de &lt;a href="https://huggingface.co/" rel="noopener noreferrer"&gt;huggingface.co&lt;/a&gt; (embeddinggemma-300m, 768 dimensions) et &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; comme base de donnée vectorielle.&lt;/p&gt;

&lt;p&gt;Aucun GPU requis si on accepte la contrepartie, l'indexation complète des reviews historiques s'est exécutée sur le CPU de ma machine &lt;em&gt;pendant plusieurs jours&lt;/em&gt; 😅. Pour un projet ponctuel, ce compromis m'a semblé largement acceptable et "cost-efficient".&lt;/p&gt;

&lt;p&gt;Comment ça fonctionne ?&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture globale
&lt;/h2&gt;

&lt;p&gt;L'architecture globale se découpe en deux gros blocs :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;La génération du RAG :&lt;/strong&gt; 

&lt;ol&gt;
&lt;li&gt;Avec la récupération des données et leurs mise en cache&lt;/li&gt;
&lt;li&gt;Avec la génération du dataset et l'indexation dans &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Le serveur MCP&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  La génération du RAG
&lt;/h3&gt;

&lt;p&gt;Voici le pipeline de génération RAG complet :&lt;/p&gt;

&lt;p&gt;Ne vous inquiétez pas si ce schéma paraît dense, je vais parcourir chaque étape du pipeline dans le reste de l'article, depuis la récupération des reviews GitHub jusqu'à la recherche sémantique.&lt;/p&gt;

&lt;pre&gt;flowchart TD
    A["API GitHub (symfony/symfony)"] --&amp;gt; B["PullsFetcher (PR mergées uniquement)"]
    B --&amp;gt; C["ReviewsFetcher (commentaires + réponses)"]
    C --&amp;gt; D["DatasetGenerator (var/dataset/pull-{id}.txt)"]
    D --&amp;gt; E["Builder::build"]
    E --&amp;gt; F["Ollama (embeddinggemma-300m)"]
    F --&amp;gt; G["Qdrant (collection: reviews)"]
&lt;/pre&gt;
&lt;h4&gt;
  
  
  Récupération des données
&lt;/h4&gt;

&lt;p&gt;Avant que toute cette mécanique de décorateurs HTTP ait de l'importance, il faut d'abord parcourir &lt;a href="https://docs.github.com/en/rest" rel="noopener noreferrer"&gt;l'API GitHub&lt;/a&gt; et décider ce qui mérite d'être gardé.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PullsFetcher&lt;/code&gt; pagine &lt;code&gt;GET /repos/symfony/symfony/pulls?state=all&amp;amp;per_page=100&lt;/code&gt;, en ne gardant que les PR dont &lt;code&gt;merged_at&lt;/code&gt; n'est pas nul — inutile d'entraîner le système sur des idées rejetées. Plutôt que de paginer aveuglément jusqu'à tomber sur une page vide, il envoie d'abord une unique requête &lt;code&gt;HEAD&lt;/code&gt; et lit le nombre total de pages directement dans le &lt;a href="https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api" rel="noopener noreferrer"&gt;header &lt;code&gt;Link&lt;/code&gt;&lt;/a&gt;. Cet appel &lt;code&gt;HEAD&lt;/code&gt; est d'ailleurs exactement la raison d'être de &lt;code&gt;BLACKLISTED_PATTERN&lt;/code&gt; : c'est une requête de découverte, pas quelque chose qui mérite d'être caché 365 jours.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReviewsFetcher&lt;/code&gt; parcourt ensuite &lt;a href="https://docs.github.com/en/rest/pulls/comments" rel="noopener noreferrer"&gt;&lt;code&gt;GET /repos/symfony/symfony/pulls/{id}/comments&lt;/code&gt;&lt;/a&gt; pour chaque PR et reconstruit le véritable arbre de conversation — les commentaires parents avec leurs réponses attachées. Le piège : l'API GitHub ne garantit pas l'ordre des commentaires. Si une réponse arrive avant son parent, &lt;code&gt;ReviewsFetcher&lt;/code&gt; la met de côté dans un pool temporaire (&lt;code&gt;$repliesTempPool&lt;/code&gt;) au lieu de la perdre, et la rattache dès que le parent est trouvé. Un petit détail de tenue de registre, mais sans lui, n'importe quel thread où trois personnes se disputent sur tabs vs espaces dans le désordre perdrait silencieusement des réponses.&lt;/p&gt;
&lt;h5&gt;
  
  
  La chaîne de décorateurs HTTP : Logging &amp;amp; Caching
&lt;/h5&gt;
&lt;h6&gt;
  
  
  L'architecture
&lt;/h6&gt;

&lt;p&gt;Les deux fetchers passent par la même petite &lt;a href="https://symfony.com/doc/current/http_client.html#decorating-the-client" rel="noopener noreferrer"&gt;chaîne de décorateurs HTTP&lt;/a&gt; :&lt;/p&gt;

&lt;pre&gt;flowchart TD
    A["GithubHttpClient (scoping + auth Bearer)"] --&amp;gt; B["CachedHttpClient (cache filesystem, TTL 365j)"]
    B --&amp;gt; C["LoggedHttpClient (logging structuré)"]
    C --&amp;gt; D["HttpClient::create() (Symfony natif)"]
&lt;/pre&gt;

&lt;p&gt;Chaque décorateur ajoute une responsabilité :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedHttpClient&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HttpClientInterface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ResetInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;HttpClientInterface&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;FilesystemAdapter&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$blacklistedPatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kt"&gt;ResponseInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;preg_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;blacklistedPatterns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;\sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#^%s$#'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$httpCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$httpCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isHit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CachedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;h6&gt;
  
  
  Le problème de sérialisation en chemin
&lt;/h6&gt;

&lt;p&gt;Cette chaîne contient un piège que j'ai déjà documenté dans mon article précédent — &lt;a href="https://symfony.com/doc/current/http_client.html#information-related-to-the-response" rel="noopener noreferrer"&gt;&lt;code&gt;HttpClient::getInfo()&lt;/code&gt;&lt;/a&gt; de Symfony contient une clé &lt;code&gt;pause_handler&lt;/code&gt; avec une &lt;code&gt;Closure&lt;/code&gt;, impossible à sérialiser. La classe &lt;code&gt;CachedResponse&lt;/code&gt; gère cela en filtrant les valeurs non sérialisables :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedResponse&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ResponseInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$statusCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var array&amp;lt;string, list&amp;lt;string&amp;gt;&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var array&amp;lt;string|int, mixed&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$toArray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var array&amp;lt;string|int, mixed&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ResponseInterface&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;toArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="cd"&gt;/** @var array&amp;lt;string|int, mixed&amp;gt; $info */&lt;/span&gt;
        &lt;span class="nv"&gt;$info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInfo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;\Closure&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sans ce filtre, &lt;a href="https://symfony.com/doc/current/components/cache.html" rel="noopener noreferrer"&gt;&lt;code&gt;FilesystemAdapter&lt;/code&gt;&lt;/a&gt; échoue silencieusement — l'exception de sérialisation est attrapée par &lt;code&gt;DefaultMarshaller&lt;/code&gt; avec &lt;code&gt;throwOnSerializationFailure&lt;/code&gt; à &lt;code&gt;false&lt;/code&gt;, et la clé de cache est discrètement ignorée.&lt;/p&gt;

&lt;h4&gt;
  
  
  Génération du dataset
&lt;/h4&gt;

&lt;p&gt;La commande &lt;code&gt;BuildCommand&lt;/code&gt; orchestre le pipeline de vectorisation :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;AsCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"build a RAG over Symfony's official Github repository's code review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'This command is a pre-requisites for the MCP server'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BuildCommand&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;string&lt;/span&gt; &lt;span class="no"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mcp:build'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;DatasetGenerator&lt;/span&gt; &lt;span class="nv"&gt;$datasetGenerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;#[Option(description: 'Skip dataset generation and uses dataset cache', name: 'skip-generation', shortcut: 'G')]&lt;/span&gt;
        &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$skipGeneration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;#[Option(description: 'Skip build of dataset cache', name: 'skip-build', shortcut: 'B')]&lt;/span&gt;
        &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$skipBuild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$skipGeneration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$skipBuild&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$throwable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$throwable&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FAILURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Le fichier de base
&lt;/h5&gt;

&lt;p&gt;Une fois les données récupérées, &lt;code&gt;DatasetGenerator&lt;/code&gt; transforme chaque PR et ses reviews en fichier texte structuré :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;PULL_REQUEST&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;54321&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nicolas-grekas&lt;/span&gt;
    &lt;span class="na"&gt;author_association&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MEMBER&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;HttpKernel&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="s"&gt;Fix edge case in exception handling&lt;/span&gt;

&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;REVIEWS&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;REVIEW_1234&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;replyTo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stof&lt;/span&gt;
        &lt;span class="na"&gt;reviewer_association&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MEMBER&lt;/span&gt;
        &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/Component/HttpKernel/Event/ExceptionEvent.php&lt;/span&gt;
        &lt;span class="na"&gt;diff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="err"&gt;@@&lt;/span&gt; &lt;span class="s"&gt;-88,7 +88,7 @@&lt;/span&gt;
             &lt;span class="s"&gt;public function getThrowable()&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;?\Throwable&lt;/span&gt;
             &lt;span class="s"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;We should keep the original exception here,&lt;/span&gt;
            &lt;span class="s"&gt;the wrapper is only for internal use.&lt;/span&gt;

        &lt;span class="na"&gt;reactions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;+1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
            &lt;span class="na"&gt;-1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;laugh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;hooray&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;confused&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;heart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;rocket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;eyes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ces fichiers vivent dans &lt;code&gt;var/dataset/pull-{id}.txt&lt;/code&gt; et servent de vérité terrain pour la vectorisation.&lt;/p&gt;

&lt;h5&gt;
  
  
  Intégration Qdrant
&lt;/h5&gt;

&lt;p&gt;La base vectorielle est câblée dans le conteneur comme &lt;code&gt;StoreInterface&lt;/code&gt;, via le &lt;code&gt;StoreFactory&lt;/code&gt; de &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoreInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Store&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autowire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoreFactory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'$collectionName'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'reviews'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'$endpoint'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'QDRANT_DSN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'$httpClient'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LoggedHttpClient&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'$embeddingsDimension'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'$embeddingsDistance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Cosine'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VectorizerInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Vectorizer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autowire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'$model'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux détails valent le détour. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Le &lt;code&gt;Vectorizer&lt;/code&gt; utilise exactement le même modèle &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; qu'au build — un modèle d'embedding &lt;code&gt;hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0&lt;/code&gt; produisant des vecteurs à 768 dimensions.
Cette partie est extrêment importante si vous ne voulez pas vous retrouver à comparer des pommes de terre avec des choux lors de la recherche via le MCP. En effet, un vecteur généré avec un modèle précis ne peux pas être comparé avec un vecteur généré avec un autre modèle.&lt;/li&gt;
&lt;li&gt;La base réutilise le décorateur &lt;code&gt;LoggedHttpClient&lt;/code&gt;, donc chaque aller-retour vers Qdrant bénéficie d'un logging structuré par-dessus le client HTTP natif de Symfony.&lt;/li&gt;
&lt;/ol&gt;

&lt;h5&gt;
  
  
  Vectorisation et stockage
&lt;/h5&gt;

&lt;p&gt;C'est là que les choses se gâtent, cette partie à elle seule m'a pris des jours.&lt;/p&gt;

&lt;h6&gt;
  
  
  Qu'est qu'il se passe à la vectorisation ?
&lt;/h6&gt;

&lt;p&gt;Le fichier issu de &lt;code&gt;var/dataset/pull-{id}.txt&lt;/code&gt; sont lus puis envoyés a &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; pour demander à un modèle d'embedding, dans mon cas &lt;code&gt;hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0&lt;/code&gt;, qui va en générer un vecteur de 768 dimensions &lt;em&gt;(chiffre qui dépends du modèle d'embedding)&lt;/em&gt; avant d'être renvoyé à symfony-ai par Ollama pour enfin être sauvegarder dans un espace vectoriel dans &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;&lt;/p&gt;

&lt;pre&gt;sequenceDiagram
    participant AI as Symfony AI (Builder)
    participant Ollama
    participant Model as embeddinggemma-300m
    participant Qdrant

    AI-&amp;gt;&amp;gt;Ollama: vectorize(contenu du fichier dataset)
    Ollama-&amp;gt;&amp;gt;Model: inférence du modèle
    Model--&amp;gt;&amp;gt;Ollama: 768 valeurs flottantes
    Ollama--&amp;gt;&amp;gt;AI: Vector (768 dimensions)
    AI-&amp;gt;&amp;gt;Qdrant: add(VectorDocument)
    Qdrant--&amp;gt;&amp;gt;AI: confirmation
&lt;/pre&gt;

&lt;p&gt;Exemple de vecteur :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ ollama run hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0 &lt;span class="s1"&gt;'Hello world !'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;0.058340553,0.017256556,-0.0023928124,0.062416226,-0.019779362,-0.069838926,0.003351966,0.029903421,0.01617497,0.009088458,-0.024585545,-0.07013172,0.0077750348,0.03643439,-0.02245716,0.02035499,0.005985676,0.008291158,0.013118213,-0.074038,0.014411658,0.011837681,0.027627029,-0.008276582,0.059961967,0.018847544,0.040701613,0.020481525,0.004880007,-0.026033,0.028065553,-0.015691148,-0.06859542,-0.04025022,-0.0046045044,-0.033632968,0.01929922,0.01854895,-0.000545488,-0.37636346,0.05929959,0.0069747632,-0.026434837,0.018491298,-0.00945082,0.0009028575,0.024641853,-0.053274404,-0.043102805,0.0016308841,-0.04315744,-0.0024387056,-0.007468653,-0.03491168,-0.00315068,-0.023274362,-0.0016503683,-0.027008653,-0.0016653507,0.029124975,0.015810343,-0.020706663,0.03261976,-0.015973076,-0.0104695875,0.0027727042,0.03187403,0.25363848,0.016416604,0.027735965,0.009674886,-0.031506274,-0.01176536,-0.060261074,-0.0031871377,-0.01684568,0.029581062,-0.05600014,-0.02623269,0.04637347,-0.04044786,0.0036846441,0.008154481,0.0190515,-0.023374602,-0.011516434,0.01098124,0.008980432,-0.02016589,-0.020812696,0.046533223,0.009594602,-0.033848874,-0.046110246,0.027529772,0.029260637,-0.04092383,0.00048074988,0.01185442,0.0059498977,0.02239185,-0.0014466406,-0.00841961,-0.011190161,0.05701471,-0.015211558,-0.052781906,0.014623615,-0.0012096912,0.029300302,0.0044483966,0.028927691,-0.032093737,0.048145276,0.034030333,0.03062545,0.015509856,-0.008765529,-0.027545273,0.010858431,0.021899927,-0.008150199,0.0034826857,-0.03487983,-0.039671477,0.009751623,-0.019133179,-0.0030401512,-0.010503486,-0.017615957,0.0365356,0.001852526,-0.013896455,0.04652015,-0.049408674,0.0120107075,0.0065035336,0.0004583914,0.0074103116,-0.028435387,-0.0110742645,-0.0012466233,0.0014801751,-0.015979966,-0.028333941,0.0053472416,0.014286039,0.00054261123,0.049599133,-0.026553018,-0.021315206,0.043478087,0.032634746,0.0031313808,-0.0007238099,-0.0033655402,0.02283678,0.012112167,-0.019739753,-0.0080446005,0.018196804,0.0334649,-0.04168719,-0.0046553654,-0.007058912,-0.0104055125,-0.033779215,0.0015363622,0.051331602,0.009603074,0.0061764405,-0.0047951997,0.0055726245,-0.013255206,-0.0184209,0.025034897,0.057538535,-0.022678796,0.029314326,-0.028992262,-0.024944754,0.005440558,-0.03586656,0.00470333,0.0039566993,-0.03774347,0.007882632,-0.019097507,0.019113457,-0.024320446,-0.012655296,-0.04030833,0.0036063064,0.017062565,-0.047738973,0.03617525,0.009754858,0.01101775,-0.03881582,-0.05497514,0.018249176,-0.02227283,-0.064608485,-0.009161445,-0.010031034,-0.0095641855,0.01916363,0.01516687,0.016545013,-0.002466866,0.05094877,-0.012659827,-0.014525608,-0.023728082,0.043452837,-0.0182468,0.0171756,-0.027970072,-0.07089399,0.030868053,-0.017713076,-0.012257217,-0.010346674,0.026055504,0.0060443725,0.017072264,0.008725935,0.013845085,-0.016069857,-0.015575777,-0.024176376,0.011916211,0.023472574,0.020805132,0.023326341,-0.032767452,-0.054396667,0.013436974,0.004714595,-0.033193223,0.018227011,0.021449534,0.035102192,0.014087173,-0.012364751,-0.02923529,-0.014678346,0.020951372,-0.046659384,-0.0001321898,-0.04854372,-0.008653948,-0.02752997,-0.039902873,0.059013035,-0.023333075,-0.002939849,-0.02412675,-0.004704963,0.005708739,0.0078358585,0.015467018,-0.017394196,-0.024916349,0.0033860407,-0.005256748,-0.019880958,0.02133062,-0.01909998,-0.03368627,-0.030686421,-0.046723425,-0.009353089,0.00718895,0.03141207,-0.005678604,0.010026497,0.017099433,0.09632587,-0.049855407,0.040112875,0.03521583,0.029755611,0.016151456,-0.011640585,-0.012128548,-0.028238969,0.015068383,-0.033082347,0.01045796,0.02537935,0.035929427,-0.066318564,0.0031127778,0.0012862016,-0.0036205864,0.025082638,-0.053492177,-0.0033758928,0.011705148,-0.0033429412,0.04687452,-0.008285868,0.0009990487,-0.032646406,0.009088849,-0.0041933167,-0.046134073,0.0067272885,0.009922917,0.01473402,-0.008017513,-0.042351346,-0.026370844,-0.013365593,-0.05842642,0.0053622974,0.07431096,-0.0013185574,-0.009227675,-0.023330051,-0.027747931,-0.009491263,0.021478329,-0.0059168683,-0.021412537,0.020145044,-0.039623715,-0.0428058,0.025570398,0.03506635,-0.037845273,0.05345302,-0.0574125,-0.00028089757,0.009052639,-0.019611377,0.04223033,0.014607936,0.04443048,0.0076912907,0.007895783,-0.0042047133,-0.0071978727,-0.005284037,0.019756729,0.006555163,0.0008758057,-0.017007992,0.050635543,0.0092563275,0.02716696,0.021407066,0.14517316,-0.02369811,0.0027539528,0.03910237,0.008360229,-0.021910692,0.011394674,0.011142392,-0.0015445971,0.0025348184,0.010536188,0.020002112,-0.025976151,0.02049012,-0.02106826,-0.032985732,0.019664701,0.021935249,0.0066386443,0.017932259,0.01572093,-0.010654519,0.023060553,-0.014906989,0.006019814,0.010056607,0.058398962,-0.033474576,0.0011755938,0.009262606,-0.01099747,-0.0015662273,-0.009874551,0.008189235,0.025591813,-0.018472403,0.04004355,-0.011285104,-0.014111953,-0.0063381894,0.0005300517,-0.023887232,-0.04470202,-0.0028616937,0.014985186,-0.03294004,-0.008383296,-0.043611214,-0.008217752,0.040103037,0.014903076,-0.0021273512,0.042627018,0.0010886613,0.41363978,0.03303489,-0.026918324,-0.051881365,-0.009240305,-0.017186431,-0.064830735,0.019849097,0.033434503,-0.0071105417,0.008766244,-0.017940182,-0.03069124,0.025611496,-0.0054379674,-0.018304668,0.035152443,0.017566781,-0.03807328,0.016257798,0.023560232,0.0043043825,0.08201138,-0.012402507,0.019028682,-0.018637981,0.0073655653,0.0015441499,-0.013527856,-0.0059385803,-0.022364056,-0.02425886,0.016911663,-0.0011785801,0.0053356686,0.016516488,-0.01820115,0.0032703253,-0.012067703,0.020428859,-0.004661816,0.0019089471,0.035107043,-0.04653369,0.032357465,0.037315182,-0.018799154,0.022463702,-0.020647656,0.021579335,-0.0511682,-0.016783282,-0.03466541,-0.0018399828,0.0013000914,-0.010348332,0.0012786519,-0.04518444,0.03560613,0.002523491,-0.005807527,0.010765285,-0.023149451,0.00044260465,0.0029642652,0.0010614877,-0.008083944,-0.022398435,-0.020968962,-0.014627337,0.008090492,0.005585011,-0.03377691,-0.011848554,0.0072662574,-0.03521151,-0.032203663,0.008255807,-0.040369254,0.030274471,-0.011215091,-0.008181156,0.053159524,-0.020998636,-0.002394821,0.0028064605,0.0074562496,0.007893967,-0.007300692,0.0015053308,-0.010529569,-0.0060634767,-0.024756493,-0.03676517,0.011349936,-0.015407753,-0.009043473,0.034528915,0.017980041,-0.021671167,-0.033637524,-0.049074188,-0.010759755,-0.016900545,0.054496538,0.09080891,-0.012992101,0.02599233,-0.0011818404,0.038375963,-0.0099124005,0.010196206,0.013038416,0.0007229094,0.058817368,-0.0010059974,0.031990774,0.05380181,0.024521016,0.002847628,0.07304043,0.0017232046,-0.031101514,-0.0050771832,0.024083985,0.005650839,0.013745816,0.037950784,-0.013184445,-0.030096699,0.0072532697,0.0069977636,0.012731625,-0.03473301,0.020486254,-0.028183239,-0.043638907,0.05255927,0.040101644,0.020626077,-0.00009298259,0.03147282,0.005550194,-0.0030457666,-0.015949357,-0.019966332,0.004474357,0.0073816148,-0.0966872,-0.0014123727,0.015428014,-0.00072764594,0.02582003,0.023952637,-0.013374169,-0.024338745,0.021395741,0.012303337,0.024213506,0.013632532,-0.016449485,-0.03322197,0.0039774035,0.00541838,0.0003197519,-0.031282444,0.021476608,0.006979306,0.024495661,0.008296023,-0.036932785,-0.031137321,-0.00706068,0.024338977,0.0073112054,0.06343152,0.010950803,-0.04011533,0.0023558561,0.005737587,-0.013831569,0.025473805,-0.017996674,0.030670065,-0.021311458,-0.014061837,-0.028316947,-0.016967898,0.05437623,-0.05517407,-0.011666794,-0.064273596,0.00039994833,-0.0016417564,0.00369619,-0.004408155,-0.033399895,0.010705014,0.022728024,-0.006053165,0.0031930788,-0.010684994,-0.05090471,-0.03378601,-0.016370287,0.00020012762,-0.022603909,0.036075003,0.030441662,0.03643664,0.01663764,0.010343481,-0.00867144,-0.015162774,-0.0014251935,0.03770172,-0.013012902,0.035615146,0.00044963427,0.012939211,-0.008898151,0.04329554,0.006962741,0.047645073,-0.058727764,0.0069460804,0.027805299,-0.0022572207,0.03155984,0.007940954,0.025537886,0.026445614,-0.01529072,0.01621024,0.0069643836,-0.013095124,-0.0015153071,-0.013846497,-0.0054590567,0.10567172,0.024595099,-0.021427441,-0.017892607,0.029084895,-0.044227537,-0.020952923,0.0037034317,-0.053684484,-0.026559578,0.0031811807,-0.0022820174,-0.07499727,-0.06748456,-0.031104647,-0.037120227,-0.0070385304,0.03623152,-0.06010997,-0.0040761833,-0.023788461,0.007862985,-0.0080264,0.0294231,-0.06763409,-0.027284352,0.02720548,0.012118604,-0.044641722,0.025212545,-0.0050499276,0.010612783,-0.0048592645,0.011480939,-0.038084067,0.050459232,-0.021653391,-0.016860519,-0.022343304,0.011578441,0.029444221,0.005036281,0.052007847,-0.00015478901,0.010757338,-0.008266853,-0.06438106,0.0038179888,0.010810128,0.014887702,0.041086577,0.09645378,-0.03697137,0.018508574,0.021505969,0.043335702,-0.015699612,0.006846198,0.059496786,0.042588223,-0.020572873,0.0042599905,0.0061774435,-0.0048375144,0.023622885,-0.013585687,0.012436588,-0.0031023244,-0.009659066,0.007284619,0.004994468,-0.03997744,0.025999822,-0.004322784,0.0021907475,0.027662035,0.013451684,0.0061220867,-0.053517137,0.011526667,-0.016668048,0.03389963,0.0024166985,-0.0087329345,0.0005349021,0.09292805,-0.024875147,0.01887172,-0.015814196,0.01548722,-0.0023607062,0.00095323403,0.040603366,-0.018233076,0.00050581084,-0.028851299,-0.05746257,-0.022262363,0.06411007,-0.015622854,0.02921695,0.032788914,-0.0042617223,-0.0026337528,0.029876161,0.026273958,-0.048925456,-0.014354729,0.0069950293,-0.046119038,-0.0015572214,-0.01648194,-0.018696893,-0.036625963,-0.0049027205,-0.002610518,-0.046027448,0.02675758,-0.033888668,0.0046032025,-0.017866787,-0.016139796]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Pourquoi une approche séquentielle ?
&lt;/h6&gt;

&lt;p&gt;Comme expliqué en introduction, sans GPU dédié, c'est mon CPU — plus précisément l'iGPU intégré à mon CPU — qui doit faire le travail de vectorisation.&lt;br&gt;
Même si l'iGPU partage la RAM avec le CPU et bien que 32 Go soient disponibles &lt;em&gt;(modulo l'utilisation de mon système, de programme en cours, ...)&lt;/em&gt;, la vitesse de la RAM n'a rien à voir avec la mémoire des cartes graphiques, bien plus rapide et dédiée.&lt;br&gt;
De plus, le CPU ne peut traiter que quelques opérations en parallèle, là où le GPU en exécute des milliers simultanément ce qui fait que les calculs matriciels deviennent très lents et mobilisent ma machine à 100%.&lt;/p&gt;

&lt;p&gt;Résultat, une seul vectorisation n'est possible à la fois.&lt;br&gt;
Et donc, oui, un batch upsert serait plus rapide, mais le but était de construire un pipeline fonctionnel avec uniquement des ressources locales.&lt;/p&gt;

&lt;p&gt;Ce n'est pas une limite de PHP ou de Qdrant, seulement un compromis pragmatique lié au matériel disponible.&lt;/p&gt;
&lt;h6&gt;
  
  
  Atomicité par renommage de fichiers
&lt;/h6&gt;

&lt;p&gt;C'est la décision de design la plus intéressante. Au lieu d'une table en base de données pour suivre les fichiers traités, le &lt;code&gt;Builder&lt;/code&gt; utilise des &lt;code&gt;rename()&lt;/code&gt; atomiques :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pull-{id}.txt               → prêt à traiter
processing_pull-{id}.txt    → en cours de vectorisation
ragged_pull-{id}.txt        → vectorisé avec succès
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ManagedStoreInterface&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;recoverOrphanedProcessingFiles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;scandir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'..'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RAGGED_PREFIX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PROCESSING_PREFIX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// un autre processus l'a pris&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;vectorizer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;vectorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;\count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$vector&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Wrong dimensions'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VectorDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^0-9]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/ragged_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// rollback&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crash-safe par conception : si le script meurt en plein milieu, &lt;code&gt;recoverOrphanedProcessingFiles()&lt;/code&gt; remet en file les orphelins &lt;code&gt;processing_*&lt;/code&gt; au prochain lancement. Pas de locks, pas de base de données, pas de race conditions.&lt;/p&gt;

&lt;p&gt;Oui, le code montre que, malgrés ce que j'ai dis plus haut :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Résultat, une seul vectorisation n'est possible à la fois.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Oui, j'ai quand même essayé 🤣.&lt;/p&gt;

&lt;h4&gt;
  
  
  Utilisation
&lt;/h4&gt;

&lt;p&gt;La CLI expose deux commandes :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pipeline complet : récupération → dataset → vectorisation&lt;/span&gt;
php bin/console mcp:build

&lt;span class="c"&gt;# Re-vectoriser sans re-récupérer&lt;/span&gt;
php bin/console mcp:build &lt;span class="nt"&gt;--skip-generation&lt;/span&gt;

&lt;span class="c"&gt;# Re-récupérer sans re-vectoriser&lt;/span&gt;
php bin/console mcp:build &lt;span class="nt"&gt;--skip-build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Le serveur MCP
&lt;/h3&gt;

&lt;p&gt;Pour que le LLM puisse avoir accès a ces review fraichement indéxés et effectuer ses recherches lui même, il faut lui donner les accès.&lt;br&gt;
Tout ça s'effectue via le protocole MCP suivant ce pipeline d'appel simplifié :&lt;/p&gt;

&lt;pre&gt;flowchart TD
    H["Appel Tool MCP (review_as_group/person)"] --&amp;gt; I["Retriever (recherche sémantique)"]
    I --&amp;gt; G["Qdrant (collection: reviews)"]
    I --&amp;gt; J["Client LLM (Claude Desktop)"]
&lt;/pre&gt;

&lt;p&gt;Le pipeline de génération (récupération → dataset → vectorisation) et le pipeline de service (recherche → réponse) partagent un seul point commun : la collection &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Le serveur MCP expose deux &lt;strong&gt;tools&lt;/strong&gt; et quatre &lt;strong&gt;prompts&lt;/strong&gt; :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Rôle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recherche par groupe d'affiliation (MEMBER, CONTRIBUTOR, NONE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_person&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recherche par reviewer spécifique (nicolas-grekas, stof, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Rôle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message formaté utilisant &lt;code&gt;review_as_group&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_person&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message formaté utilisant &lt;code&gt;review_as_person&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_stofed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Force le reviewer à « stof » — le plus prolifique reviewer du Core Symfony&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hq_review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-review : interroge 14 reviewers et synthétise un rapport markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Recherche : les Tools MCP
&lt;/h4&gt;

&lt;p&gt;Quand un utilisateur envoie une requête via un tool MCP, voici ce qui se passe :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Le tool construit une requête combinant reviewer, chemin de fichier et diff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RetrieverInterface::retrieve()&lt;/code&gt; vectorise la requête via Ollama&lt;/li&gt;
&lt;li&gt;Une recherche par similarité cosinus s'exécute sur Qdrant&lt;/li&gt;
&lt;li&gt;Les &lt;code&gt;VectorDocument&lt;/code&gt; correspondants sont retournés&lt;/li&gt;
&lt;li&gt;Leur &lt;code&gt;metadata['content']&lt;/code&gt; est extrait et assemblé en contexte&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;sequenceDiagram
    participant Client as Claude Desktop
    participant MCP as Serveur MCP (stdio)
    participant Tool as review_as_person
    participant Retriever
    participant Ollama
    participant Qdrant

    Client-&amp;gt;&amp;gt;MCP: call_tool(review_as_person)
    MCP-&amp;gt;&amp;gt;Tool: __invoke(pseudonym, file, diff, limit)
    Tool-&amp;gt;&amp;gt;Retriever: retrieve(query, ['limit' =&amp;gt; limit])
    Retriever-&amp;gt;&amp;gt;Ollama: vectorize(query)
    Ollama--&amp;gt;&amp;gt;Retriever: vecteur de la requête
    Retriever-&amp;gt;&amp;gt;Qdrant: recherche par similarité cosinus
    Qdrant--&amp;gt;&amp;gt;Retriever: documents les plus proches
    Retriever--&amp;gt;&amp;gt;Tool: VectorDocument[]
    Tool--&amp;gt;&amp;gt;MCP: reviews trouvées (texte)
    MCP--&amp;gt;&amp;gt;Client: résultat du tool
&lt;/pre&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;McpTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Tool retrieving a `limit` amount of reviews from `pseudonym` github user based on a given git a complete `file` path and `diff`. Results are separated by `\n\n---\n\n`.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Review matching file diff as github user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReviewAsPersonMatchingFileDiffTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;string&lt;/span&gt; &lt;span class="no"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'review_as_person'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$pseudonym&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$diff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TXT
        reviewer: $pseudonym
        file: $file
        diff:
        {$diff}
        TXT;&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$retrieved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

            &lt;span class="nv"&gt;$return&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$retrieved&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$document&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMetadata&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;\is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'Error retrieving reviews: '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;\count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'No reviews found.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Le Prompt HQ Review
&lt;/h4&gt;

&lt;p&gt;Le prompt &lt;code&gt;hq_review&lt;/code&gt; est la fonctionnalité vedette. Il interroge 14 reviewers Symfony de premier plan (&lt;a href="https://github.com/GromNaN" rel="noopener noreferrer"&gt;GromNaN&lt;/a&gt;, &lt;a href="https://github.com/dunglas" rel="noopener noreferrer"&gt;dunglas&lt;/a&gt;, &lt;a href="https://github.com/welcoMattic" rel="noopener noreferrer"&gt;welcoMattic&lt;/a&gt;, &lt;a href="https://github.com/nicolas-grekas" rel="noopener noreferrer"&gt;nicolas-grekas&lt;/a&gt;, &lt;a href="https://github.com/chalasr" rel="noopener noreferrer"&gt;chalasr&lt;/a&gt;, &lt;a href="https://github.com/stof" rel="noopener noreferrer"&gt;stof&lt;/a&gt;, &lt;a href="https://github.com/yceruto" rel="noopener noreferrer"&gt;yceruto&lt;/a&gt;, &lt;a href="https://github.com/mtarld" rel="noopener noreferrer"&gt;mtarld&lt;/a&gt;, &lt;a href="https://github.com/OskarStark" rel="noopener noreferrer"&gt;OskarStark&lt;/a&gt;, &lt;a href="https://github.com/xabbuh" rel="noopener noreferrer"&gt;xabbuh&lt;/a&gt;, &lt;a href="https://github.com/lyrixx" rel="noopener noreferrer"&gt;lyrixx&lt;/a&gt;, &lt;a href="https://github.com/kbond" rel="noopener noreferrer"&gt;kbond&lt;/a&gt;, &lt;a href="https://github.com/jderusse" rel="noopener noreferrer"&gt;jderusse&lt;/a&gt;, &lt;a href="https://github.com/alexandre-daubois" rel="noopener noreferrer"&gt;alexandre-daubois&lt;/a&gt;), collecte leurs feedbacks historiques sur le même fichier/diff, et demande au LLM de synthétiser un rapport markdown avec des retours pondérés par reviewer.&lt;/p&gt;

&lt;p&gt;Le résultat est une code review qui ressemble à un mini-symposium des mainteneurs du Core Symfony — sans nécessiter leur temps.&lt;/p&gt;
&lt;h4&gt;
  
  
  Utilisation
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; symfony-reviewer-mcp-cli /path/to/Dockerfile
docker run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;QDRANT_DSN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://host.docker.internal:6333 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OLLAMA_DSN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://host.docker.internal:11434 symfony-reviewer-mcp-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Puis configurer Claude Desktop (ou tout client MCP) en ajoutant le serveur à &lt;code&gt;claude_desktop_config.json&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony-reviewer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"--add-host=host.docker.internal:host-gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"QDRANT_DSN=http://host.docker.internal:6333"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OLLAMA_DSN=http://host.docker.internal:11434"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"symfony-reviewer-mcp-cli"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Les variables d'env
&lt;/h2&gt;

&lt;p&gt;Toute la configuration passe par les variables &lt;code&gt;.env&lt;/code&gt; :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Rôle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GITHUB_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;Token d'accès personnel GitHub&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QDRANT_DSN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL du service Qdrant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OLLAMA_DSN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL du service Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLACKLISTED_PATTERN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tableau JSON de patterns URL à exclure du cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APP_VERSION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version affichée dans les métadonnées MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Leçons apprises
&lt;/h2&gt;

&lt;p&gt;Ce projet m'a permis de comprendre comment fonctionne le protocole MCP, ce qu'est un base de données vectoriel et comment l'utiliser. L'architecture présentée ici est relativement classique pour un pipeline RAG mais il a été réalisé avec Symfony et peu servir de socle de base pour d'autres expérimentations ou usages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ce qui a bien fonctionné
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atomicité par renommage de fichiers&lt;/strong&gt; : Ce pattern est élégant, crash-safe et ne nécessite aucune infrastructure. Chaque développeur PHP comprend &lt;code&gt;rename()&lt;/code&gt;. Pas de locks Redis, pas de migrations de base de données.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline incrémental&lt;/strong&gt; : Relancer &lt;code&gt;mcp:build&lt;/code&gt; avec des fichiers existants est un no-op. L'itération est rapide — on peut ajuster la vectorisation et ne traiter que les nouveaux fichiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonctionnalités PHP 8.x&lt;/strong&gt; : La promotion de constructeur, les propriétés readonly, l'opérateur pipe (&lt;code&gt;|&amp;gt;&lt;/code&gt;), et les commandes invokables rendent le code bien plus propre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; en local&lt;/strong&gt; : embeddinggemma-300m tourne sur CPU sans problème. 768 dimensions, c'est assez modeste pour des requêtes rapides mais assez riche pour la recherche sémantique sur des code reviews.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ce qui doit être amélioré
&lt;/h3&gt;

&lt;p&gt;Si j'avais eu une machine plus efficace avec un GPU dédié ou une RAM unifiée (👋 les propriétaires de Mac), j'aurai peut-être pu changer ce qui suit :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interaction Qdrant naïve&lt;/strong&gt; : Les documents sont ajoutés un par un. Un batch upsert serait nettement plus rapide pour les gros volumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pas de mise à jour incrémentale du RAG&lt;/strong&gt; : Le pipeline est add-only. Il n'y a pas de mécanisme "builtin" &lt;em&gt;(c'est possible via le dashboard de qdrant)&lt;/em&gt; pour purger ou mettre à jour les vecteurs existants quand des commentaires de PR sont édités sur GitHub. Ce qui en soit est un vrai/faux problème puisqu'on retrouve rarement de nouveaux commentaire sur des Pull-Requests déjà mergées.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ajoutez vos propres conventions&lt;/strong&gt; : J'ai ajouté les reviews de Symfony, mais vous pouvez vous aussi modifier et adapter le code pour qu'il s'appuie sur un corpus de données supplémentaire comme les revues de vos collaborateurs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ce que je ferais différemment
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vectorisation par lots&lt;/strong&gt; : Grouper les documents et vectoriser en lots pour un meilleur débit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Récupération asynchrone&lt;/strong&gt; : La phase de récupération des données est séquentielle par PR. Des requêtes concurrentes réduiraient significativement le temps de construction initial&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook GitHub&lt;/strong&gt; : Au lieu de reconstruire périodiquement, écouter les événements de PR mergées et mettre à jour le dataset de manière incrémentale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Évaluation des modèles d'embeddings&lt;/strong&gt; : 768 dimensions fonctionne bien, mais je devrais comparer différents modèles : des modèles plus petits (comme &lt;a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;all-MiniLM-L6-v2&lt;/a&gt;, 384 dimensions) pour améliorer les performances, ou des modèles beaucoup plus gros afin d'évaluer le gain potentiel en qualité de recherche.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contexte élargi : commentaires de PR et diff complet&lt;/strong&gt; : le dataset actuel ne retient que les review comments attachés à un diff précis. Il ignore les commentaires généraux de la PR (issue comments, description) et surtout le diff complet de la PR — un reviewer ne juge jamais une ligne isolée, il la juge dans le contexte du changement entier. Injecter les deux donnerait au modèle bien plus de matière pour comprendre pourquoi une review a été formulée ainsi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploiter le champ &lt;code&gt;metadata&lt;/code&gt; pour le JSON brut de GitHub&lt;/strong&gt; : &lt;a href="https://github.com/symfony/ai-store" rel="noopener noreferrer"&gt;&lt;code&gt;symfony/ai-store&lt;/code&gt;&lt;/a&gt; attache à chaque &lt;code&gt;VectorDocument&lt;/code&gt; un objet &lt;code&gt;Metadata&lt;/code&gt; (&lt;code&gt;Symfony\AI\Store\Document\Metadata&lt;/code&gt;) qui voyage jusqu'au store choisi. Chez Qdrant, ce &lt;code&gt;Metadata&lt;/code&gt; correspond exactement à la notion de &lt;em&gt;payload&lt;/em&gt; : un objet JSON arbitraire attaché à chaque point, indexable et filtrable nativement — par exemple filtrer par &lt;code&gt;reviewer_association&lt;/code&gt;, par date, ou par nombre de réactions, sans re-parser le texte du dataset. Aujourd'hui, seul &lt;code&gt;content&lt;/code&gt; (le texte assemblé du fichier dataset) y est stocké ; j'y aurais ajouté la réponse JSON brute de l'API GitHub (PR + review + reactions), pour garder une trace exploitable indépendante du format texte généré.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploiter les réactions du JSON GitHub&lt;/strong&gt;: Les réactions se prêteraient d'ailleurs à mieux qu'un simple filtre : elles pourraient pondérer le score de recherche lui-même, pas juste être renvoyées dans le &lt;code&gt;content&lt;/code&gt;. Depuis la version 1.14, Qdrant propose une &lt;a href="https://qdrant.tech/documentation/search/hybrid-queries/" rel="noopener noreferrer"&gt;&lt;em&gt;Formula Query&lt;/em&gt;&lt;/a&gt; qui permet de composer un score final à partir du score de similarité initial et de champs du payload, dans une même formule de reclassement. Une review avec dix &lt;code&gt;+1&lt;/code&gt; remonterait alors devant une review isolée à zéro réaction, à similarité vectorielle égale — une façon de faire remonter les avis que la communauté a elle-même validés, Une fonctionnalitée qui meriterai d'être explorée.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Est-ce que ça fonctionne ?
&lt;/h2&gt;

&lt;p&gt;La grande question : « est-ce que les reviews produites sont réellement meilleures ? ».&lt;/p&gt;

&lt;p&gt;Répondre Oui serait en parti faux. En effet, aujourd'hui, mon évaluation reste essentiellement basée sur un sentiment générale plutôt que sur de vraies données tangibles et quantifiables.&lt;br&gt;
Les réponses générées &lt;strong&gt;me&lt;/strong&gt; paraissent beaucoup plus dans le ton d'une review qui aurait été effectuée sur le dépôt Github de Symfony, et donc plus proches des ses conventions, que les réponses obtenues avec un LLM sans contexte.&lt;br&gt;
Enfin, comme un LLM et par nature "probabiliste", je ne crois pas qu'il soit pertinant de dire que sur la base de mes propres observations et mon sentiment personnel seul le fonctionnement d'un outil est établi.&lt;/p&gt;

&lt;p&gt;Voici un exemple de revue effectuée sur ce projet pour vous donner un idée :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;### 1. fabpot&lt;/span&gt;
&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*Files&lt;/span&gt;&lt;span class="s"&gt;:** `src/HTTP/CachedHttpClient.php`, `src/HTTP/CachedResponse.php`&lt;/span&gt;

&lt;span class="na"&gt;&amp;gt; Thanks for the PR! A few things before we can merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; * Why is the cache key based on the URL only? For `POST` requests the body matters, otherwise all `/api/embed` calls will return the same result. This is a real bug, not an optimization issue.&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; * Please add a test covering the "non-buffered" response case — `getContent()` then `toArray()` will fail on `EventSourceHttpClient` responses.&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; * Do we need a `CHANGELOG` entry for the `CachedHttpClient`? I think we can keep it internal.&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; Otherwise the approach is clean. Once the cache key is fixed, we can squash the commits.&lt;/span&gt;

&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;

&lt;span class="gu"&gt;### 2. nicolas-grekas&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedResponse.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Reading the response body twice is going to blow up the moment the response is not buffered. `toArray()` internally calls `getContent()` — so snapshot the content once and `json_decode` it, don't call both.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the decorated chain is wrong: `service(LoggedHttpClient::class)` no longer resolves to the logger once you decorate it with `CachedHttpClient`. Qdrant and Ollama are being routed through the GitHub cache without intent. Decorate a dedicated alias, e.g. `cached.github.http_client`, and keep `HttpClientInterface` as the plain chain.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; One more: `getContent(false)` is being snapshotted eagerly in the constructor — that defeats lazy streaming for large GitHub responses. Buffer lazily.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 3. stof&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Service wiring nit: decoration replaces the decorated id, so `$httpClient =&amp;gt; service(LoggedHttpClient::class)` gives you the `CachedHttpClient`, not the logging client. That's a scope leak — the Qdrant store is now coupled to a cache tuned for the GitHub API (see `BLACKLISTED_PATTERN`).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; I'd define a dedicated `logged.http_client` service for the store/platform and only decorate `cached.github.http_client` for the GitHub fetchers. Also check the `-&amp;gt;decorate(..., priority: 1)` priorities — with equal priorities the order of application is by declaration order, which is fragile to read.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; And the `json:BLACKLISTED_PATTERN` env var — document its format in the README.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 4. weaverryan&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/MCP/Tools/ReviewAsPersonMatchingFileDiffTool.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Hey! Love the ergonomics of this tool — the `review_as_person` name makes the intent super clear. Great job composing the query with the pseudonym, file, and diff.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; One DX thought: when no reviews are found we return `No reviews found.` — that's good. But maybe give the caller a hint that they can reduce `limit` or widen the diff? Small thing, ignore if you want.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the metadata `content` check with the warning log is nice defensive coding. Keep it up! 🎉&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 5. derrabus&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Two things:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 1. `$key = md5($method.$url)` — please include the serialized options/body. Hash collisions here are silent correctness bugs, not just perf issues.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 2. The `preg_match` on a compiled regex built via pipe chains is clever but hard to read. Since the blacklist is a list of exact strings, why not use `in_array` or a simple `str_starts_with` on the pattern list? Keep it simple.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the code is `final readonly` — good. But it implements `ResetInterface`; make sure the decorated inner `reset()` is reachable in `withOptions()` clones (it is, since you forward to the scoped client — just double-check the cache/lifecycle after cloning).&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 6. xabbuh&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedResponse.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; I have concerns about the snapshot in the constructor:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Calling `$response-&amp;gt;getContent()` eagerly downloads and stores the whole payload. For the GitHub fetchers this is fine, but a general-purpose cache should stream lazily.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * More importantly, `getContent()` followed by `toArray()` breaks for responses that disabled buffering (the Ollama `EventSourceHttpClient` forces `buffer =&amp;gt; false`). This is an exploitable/observable crash — at minimum it should throw a clear `TransportException` or read once.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * `getInfo()` filtering out closures is a nice touch, but the returned array is shallow — nested closures could still leak. Use a recursive filter or `json_encode/decode` the info array.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 7. Tobion&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The pipeline operator chains in `request()` are over-engineered for building a regex. `implode('|', array_map(preg_quote(...), $this-&amp;gt;blacklistedPatterns))` is enough. As written, an empty blacklist produces the pattern `#^$#` which would match an empty call string — harmless, but misleading.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; More importantly: cache invalidation. There is none — responses are cached for a year (`defaultLifetime`). GitHub data changes; the fetchers need a way to bust the cache (e.g. include a version/tag in the key or a TTL per-URL). Otherwise reviews fetched once are served stale forever.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 8. mpdude&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/RAG/Builder.php`&lt;/span&gt; (via &lt;span class="sb"&gt;`Store`&lt;/span&gt;)
&lt;span class="gt"&gt;
&amp;gt; The Qdrant indexing loop swallows exceptions and logs `Index failed` — but the build then *continues* (I saw `Indexing document continues`). If a document fails vectorization, subsequent documents are still sent. That means the collection is only partially populated, and `review_as_person` will silently return "No reviews found" or partial results.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Please make the build fail-fast or at least surface a summary count at the end ("indexed X / failed Y") so operators know the dataset is incomplete. Right now nothing tells us that only ~1106 of 8691 documents made it in.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 9. WouterJ&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The container config reads really well — the decoration chain is easy to follow. Nice use of `env('json:BLACKLISTED_PATTERN')` and `StoreFactory::create(...)`.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Minor: the `logged.http_client` vs `cached.github.http_client` distinction is muddied because both decorators use `priority: 1` and decorate each other's ids. I'd give them explicit service aliases (`github.logged.http_client`, etc.) so the intent is obvious. Also the unused `'stream_handler'` monolog handler and the commented-out `http` transport block could be cleaned up before merge.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 10. alexislefebvre&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`tests/HTTP/CachedHttpClientTest.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Nice test coverage — you test blacklist skipping, persistence across instances, `withOptions` cloning, and `reset`. 👍&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Missing cases I'd love to see:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 1. A `POST` request with a body — assert that different bodies don't collide in the cache (this would catch the `md5(method.url)` bug).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 2. A non-buffered/streaming response (`MockResponse` with `buffer =&amp;gt; false` is hard to fake; but at least an SSE-like response) going through `CachedResponse` without throwing.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 3. The cache should not be hit for `POST`/`PUT` (or should include the body in the key) — please encode that expectation in a test.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 11. Nyholm&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/LoggedHttpClient.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; As the http-client component maintainer: don't re-implement caching. Symfony's `HttpClient` supports a `cache` option natively via the `http_cache` from the contracts, and it handles cache keys, headers, `Vary`, and ETags properly. Rolling your own `md5(method.url)` cache key is a regression waiting to happen (it already broke on POST bodies).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; If you keep the custom decorator, at least delegate to `CacheItemPoolInterface` semantics and include the request payload + relevant headers in the key. And please make `stream()` forward correctly — it does, but note that cached responses can never stream, which may surprise callers.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also: `FileSystemAdapter` on a single Docker container is fine, but for multi-instance deploys you'll want a shared pool (Redis). Worth a comment.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 12. jderusse&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/RAG/Builder.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The elephant in the room: the per-document vectorization loop is serial. 8691 documents, ~10s each — that's ~24h to build the RAG, and with the cache bug most embeddings were identical (same URL → same key). That's why retrieval feels broken.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Fixes I'd push for:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Parallelize vectorization with Symfony's `AsyncResponse` / `stream()` over batches.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Include the body in the cache key (obviously).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Index the docs that failed (`Index failed` ×636) with retry/backoff.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also `Builder` should checkpoint progress so a crash doesn't restart from zero.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 13. chalasr&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/MCP/Tools/ReviewAsPersonMatchingFileDiffTool.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/Command/ServeCommand.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Tool ergonomics are good — `limit` is explicit, errors are caught and surfaced. But: the error path returns `Error retrieving reviews: {message}` as a *successful* tool result. For an MCP server, real failures should be proper exceptions/tool errors, not strings, otherwise the client can't distinguish "no data" from "server broken".&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the query string embeds the raw diff with no size guard — a huge diff will blow the embedding context window. Truncate or chunk the diff.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; And the serve command: make sure `APP_DEBUG` is off in prod and there's a graceful shutdown on SIGTERM.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 14. yceruto&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The routing of the HTTP decorators deserves attention: `LoggedHttpClient::class` is decorated by `CachedHttpClient`, so every consumer referencing it — including the Qdrant store — ends up behind the GitHub cache. That coupling is accidental.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; I'd restructure like this:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; ```&lt;/span&gt;

&lt;span class="gt"&gt;
&amp;gt; HttpClientInterface          # plain&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;  └─ logged.http_client       # logging only (for Qdrant/Ollama)&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;  └─ cached.github.http_client # cache + github token (for GitHub fetchers)&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Two separate chains, no cross-decorating. Then the cache key issue (URL-only, no body) also only affects GitHub GETs, which is safe.&lt;/p&gt;

&lt;p&gt;After that, the 14 &lt;code&gt;review_as_person&lt;/code&gt; calls will stop returning the &lt;code&gt;buffering is disabled&lt;/code&gt; error and start returning real reviews.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


## Pour conclure

&amp;lt;div style="width:100%;height:0;padding-bottom:56%;position:relative;"&amp;gt;
    &amp;lt;iframe src="https://giphy.com/embed/NRiRXQTwbijNba2l2l" width="100%" height="100%" style="position:absolute" frameBorder="0" class="giphy-embed" allowFullScreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;a href="https://giphy.com/gifs/The-Animal-Crackers-Movie-baking-try-it-NRiRXQTwbijNba2l2l"&amp;gt;via GIPHY&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;

Essayez-le vous même, le [projet](https://github.com/ktherage/symfony-review-mcp) est conçu pour être autonome et indépendant. Les données sont publiquements accessibles, vous pouvez créé une instance Qdrant et Ollama facilement avec [Docker](https://www.docker.com/).

Pour l'installer :



```bash
git clone https://github.com/ktherage/symfony-review-mcp
cd symfony-review-mcp
docker compose run --rm cli composer install
docker compose run --rm cli bin/console mcp:build
docker compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La chose la plus surprenante que j'ai apprise en construisant ce projet : PHP est un langage parfaitement viable pour les pipelines RAG. Les composants HttpClient, Cache et Console de Symfony, combinés avec les packages &lt;a href="https://github.com/symfony/ai" rel="noopener noreferrer"&gt;&lt;code&gt;symfony/ai-*&lt;/code&gt;&lt;/a&gt;, gèrent tout, de la décoration HTTP aux opérations sur bases vectorielles. &lt;/p&gt;

&lt;p&gt;Vous n'avez pas besoin de Python pour faire de la recherche sémantique.&lt;/p&gt;

&lt;p&gt;Parfois, le meilleur outil pour le travail est celui que vous maîtrisez déjà.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>symfony</category>
      <category>php</category>
      <category>rag</category>
    </item>
    <item>
      <title>Building a RAG-Powered Code Review Assistant with PHP, Ollama, and Qdrant</title>
      <dc:creator>Thérage Kévin</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:12:11 +0000</pubDate>
      <link>https://dev.to/ktherage/building-a-rag-powered-code-review-assistant-with-php-ollama-and-qdrant-13g4</link>
      <guid>https://dev.to/ktherage/building-a-rag-powered-code-review-assistant-with-php-ollama-and-qdrant-13g4</guid>
      <description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Large_language_model" rel="noopener noreferrer"&gt;LLMs&lt;/a&gt; are great at producing code reviews that sound right. But "sounding right" isn't the same as being useful. A review that tells you to "fix the Code Style" is correct but useless — every project applies a Code Style that may differ.&lt;/p&gt;

&lt;p&gt;Symfony has more than &lt;strong&gt;20 years of public code reviews&lt;/strong&gt; on &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Every merged PR contains comments from &lt;a href="https://github.com/nicolas-grekas" rel="noopener noreferrer"&gt;nicolas-grekas&lt;/a&gt;, &lt;a href="https://github.com/stof" rel="noopener noreferrer"&gt;stof&lt;/a&gt;, &lt;a href="https://github.com/dunglas" rel="noopener noreferrer"&gt;dunglas&lt;/a&gt;, &lt;a href="https://github.com/xabbuh" rel="noopener noreferrer"&gt;xabbuh&lt;/a&gt;, and dozens of other reviewers from the core team and contributors. It's a goldmine of domain-specific review patterns: which arguments convince, which patterns get rejected, what the community considers good Symfony code.&lt;/p&gt;

&lt;p&gt;The problem? No one had built a search engine to exploit it. So I did it 🤣.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lore behind this crazy idea
&lt;/h2&gt;

&lt;p&gt;This story starts at the &lt;a href="https://live.symfony.com/" rel="noopener noreferrer"&gt;Symfony Live in Paris&lt;/a&gt;. As you can imagine, this year's edition was very AI-focused. I saw quite a few talks about it and wanted to play around with this novelty, but until then I didn't have a concrete use case.&lt;/p&gt;

&lt;p&gt;I attended a talk by &lt;a href="https://github.com/lyrixx" rel="noopener noreferrer"&gt;Grégoire Pineau&lt;/a&gt; where he explained how, with &lt;a href="https://ai.symfony.com/" rel="noopener noreferrer"&gt;Symfony AI&lt;/a&gt;, &lt;a href="https://clickhouse.com/" rel="noopener noreferrer"&gt;Clickhouse&lt;/a&gt; and &lt;a href="https://redirection.io/" rel="noopener noreferrer"&gt;redirection.io&lt;/a&gt;, he had successfully migrated an e-commerce site while reducing traffic loss.&lt;/p&gt;

&lt;p&gt;Later, the &lt;a href="https://symfony.com/blog/new-in-symfony-8-1-http-less-symfony-applications" rel="noopener noreferrer"&gt;Console Bundle&lt;/a&gt; arrived.&lt;/p&gt;

&lt;p&gt;I had already asked an LLM to review the changes I had made &lt;em&gt;(on personal projects of course)&lt;/em&gt; and, as you might guess, I got advice like &lt;code&gt;consider using dependency injection&lt;/code&gt;, &lt;code&gt;maybe extract this logic into a service&lt;/code&gt; or &lt;code&gt;remember to check the code style&lt;/code&gt;. These pieces of feedback are technically correct, but above all universally applicable and completely generic. In short, nothing that can't be fixed with good tools and a little rigor.&lt;/p&gt;

&lt;p&gt;Coming out of Symfony Live, a somewhat crazy idea came to me.&lt;br&gt;
What if I could ask the same LLM: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Review this code the way stof would"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd get ultra-sharp feedback and code that would come out stronger.&lt;/p&gt;

&lt;p&gt;Or&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Review this code the way any Symfony contributor would"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I'd get the whole community's point of view on the code I just created. &lt;/p&gt;

&lt;p&gt;Or even&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Review this code the way any member of the Symfony core team would"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd have a panel of experts at my disposal to explain what's wrong with what I did.&lt;/p&gt;

&lt;p&gt;That's what &lt;strong&gt;Symfony Reviewer MCP&lt;/strong&gt; does: a &lt;a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" rel="noopener noreferrer"&gt;semantic search engine (RAG)&lt;/a&gt; over all of Symfony's historical code reviews, accessible via the &lt;a href="https://docs.github.com/en/rest" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; API, exposed through the &lt;a href="https://en.wikipedia.org/wiki/Model_Context_Protocol" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt;, all with &lt;a href="https://ai.symfony.com/" rel="noopener noreferrer"&gt;Symfony AI&lt;/a&gt; and in a &lt;a href="https://symfony.com/blog/new-in-symfony-8-1-http-less-symfony-applications" rel="noopener noreferrer"&gt;Symfony HTTP-less&lt;/a&gt; application.&lt;/p&gt;

&lt;p&gt;I built it in &lt;a href="https://www.php.net/releases/8.5/" rel="noopener noreferrer"&gt;PHP 8.5&lt;/a&gt; with &lt;a href="https://symfony.com/" rel="noopener noreferrer"&gt;Symfony 8.1&lt;/a&gt;, using &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; locally for vectorization with a model from &lt;a href="https://huggingface.co/" rel="noopener noreferrer"&gt;huggingface.co&lt;/a&gt; (embeddinggemma-300m, 768 dimensions) and &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; as the vector database.&lt;/p&gt;

&lt;p&gt;No GPU required — if you accept the trade-off: indexing all historical reviews ran on my machine's CPU &lt;em&gt;for several days&lt;/em&gt; 😅. For a one-off project, I found this trade-off largely acceptable and cost-efficient.&lt;/p&gt;

&lt;p&gt;How does it work?&lt;/p&gt;
&lt;h2&gt;
  
  
  Global architecture
&lt;/h2&gt;

&lt;p&gt;The global architecture is split into two big blocks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RAG generation:&lt;/strong&gt; 

&lt;ol&gt;
&lt;li&gt;Data fetching and caching&lt;/li&gt;
&lt;li&gt;Dataset generation and indexing into &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The MCP server&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  RAG generation
&lt;/h3&gt;

&lt;p&gt;Here is the complete RAG generation pipeline:&lt;/p&gt;

&lt;p&gt;Don't worry if this diagram looks dense — I'll walk through every step of the pipeline in the rest of the article, from fetching GitHub reviews to semantic search.&lt;/p&gt;

&lt;pre&gt;flowchart TD
    A["GitHub API (symfony/symfony)"] --&amp;gt; B["PullsFetcher (merged PRs only)"]
    B --&amp;gt; C["ReviewsFetcher (comments + replies)"]
    C --&amp;gt; D["DatasetGenerator (var/dataset/pull-{id}.txt)"]
    D --&amp;gt; E["Builder::build"]
    E --&amp;gt; F["Ollama (embeddinggemma-300m)"]
    F --&amp;gt; G["Qdrant (collection: reviews)"]
&lt;/pre&gt;
&lt;h4&gt;
  
  
  Fetching the data
&lt;/h4&gt;

&lt;p&gt;Before all this HTTP decorator machinery matters, you first have to walk the &lt;a href="https://docs.github.com/en/rest" rel="noopener noreferrer"&gt;GitHub API&lt;/a&gt; and decide what's worth keeping.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PullsFetcher&lt;/code&gt; pages through &lt;code&gt;GET /repos/symfony/symfony/pulls?state=all&amp;amp;per_page=100&lt;/code&gt;, keeping only PRs whose &lt;code&gt;merged_at&lt;/code&gt; isn't null — no point training the system on rejected ideas. Rather than blindly paging until hitting an empty page, it first sends a single &lt;code&gt;HEAD&lt;/code&gt; request and reads the total page count directly from the &lt;a href="https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api" rel="noopener noreferrer"&gt;Link header&lt;/a&gt;. That &lt;code&gt;HEAD&lt;/code&gt; call is exactly why &lt;code&gt;BLACKLISTED_PATTERN&lt;/code&gt; exists: it's a discovery request, not something worth caching for 365 days.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReviewsFetcher&lt;/code&gt; then walks &lt;a href="https://docs.github.com/en/rest/pulls/comments" rel="noopener noreferrer"&gt;&lt;code&gt;GET /repos/symfony/symfony/pulls/{id}/comments&lt;/code&gt;&lt;/a&gt; for each PR and rebuilds the actual conversation tree — parent comments with their replies attached. The catch: GitHub's API doesn't guarantee comment ordering. If a reply arrives before its parent, &lt;code&gt;ReviewsFetcher&lt;/code&gt; parks it in a temporary pool (&lt;code&gt;$repliesTempPool&lt;/code&gt;) instead of dropping it, and reattaches it as soon as the parent is found. A small piece of bookkeeping, but without it, any thread where three people argue about tabs vs. spaces out of order would silently lose replies.&lt;/p&gt;
&lt;h5&gt;
  
  
  The HTTP decorator chain: Logging &amp;amp; Caching
&lt;/h5&gt;
&lt;h6&gt;
  
  
  The architecture
&lt;/h6&gt;

&lt;p&gt;Both fetchers go through the same small &lt;a href="https://symfony.com/doc/current/http_client.html#decorating-the-client" rel="noopener noreferrer"&gt;HTTP decorator chain&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;flowchart TD
    A["GithubHttpClient (scoping + Bearer auth)"] --&amp;gt; B["CachedHttpClient (filesystem cache, 365d TTL)"]
    B --&amp;gt; C["LoggedHttpClient (structured logging)"]
    C --&amp;gt; D["HttpClient::create() (Symfony native)"]
&lt;/pre&gt;

&lt;p&gt;Each decorator adds one responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedHttpClient&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HttpClientInterface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ResetInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;HttpClientInterface&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;FilesystemAdapter&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$blacklistedPatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kt"&gt;ResponseInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;preg_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;blacklistedPatterns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;\sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#^%s$#'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$httpCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$httpCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isHit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CachedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheItem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;h6&gt;
  
  
  The serialization problem along the way
&lt;/h6&gt;

&lt;p&gt;This chain contains a trap I already documented in my previous article — Symfony's &lt;a href="https://symfony.com/doc/current/http_client.html#information-related-to-the-response" rel="noopener noreferrer"&gt;&lt;code&gt;HttpClient::getInfo()&lt;/code&gt;&lt;/a&gt; contains a &lt;code&gt;pause_handler&lt;/code&gt; key with a &lt;code&gt;Closure&lt;/code&gt;, impossible to serialize. The &lt;code&gt;CachedResponse&lt;/code&gt; class handles this by filtering out non-serializable values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedResponse&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ResponseInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$statusCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var array&amp;lt;string, list&amp;lt;string&amp;gt;&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var array&amp;lt;string|int, mixed&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$toArray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var array&amp;lt;string|int, mixed&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ResponseInterface&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;toArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="cd"&gt;/** @var array&amp;lt;string|int, mixed&amp;gt; $info */&lt;/span&gt;
        &lt;span class="nv"&gt;$info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInfo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;\Closure&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this filter, &lt;a href="https://symfony.com/doc/current/components/cache.html" rel="noopener noreferrer"&gt;&lt;code&gt;FilesystemAdapter&lt;/code&gt;&lt;/a&gt; silently fails — the serialization exception is caught by &lt;code&gt;DefaultMarshaller&lt;/code&gt; with &lt;code&gt;throwOnSerializationFailure&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt;, and the cache key is quietly ignored.&lt;/p&gt;

&lt;h4&gt;
  
  
  Dataset generation
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;BuildCommand&lt;/code&gt; class orchestrates the vectorization pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;AsCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"build a RAG over Symfony's official Github repository's code review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'This command is a pre-requisites for the MCP server'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BuildCommand&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;string&lt;/span&gt; &lt;span class="no"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mcp:build'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;DatasetGenerator&lt;/span&gt; &lt;span class="nv"&gt;$datasetGenerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;#[Option(description: 'Skip dataset generation and uses dataset cache', name: 'skip-generation', shortcut: 'G')]&lt;/span&gt;
        &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$skipGeneration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;#[Option(description: 'Skip build of dataset cache', name: 'skip-build', shortcut: 'B')]&lt;/span&gt;
        &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$skipBuild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$skipGeneration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$skipBuild&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$throwable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$throwable&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FAILURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  The base file
&lt;/h5&gt;

&lt;p&gt;Once the data is fetched, &lt;code&gt;DatasetGenerator&lt;/code&gt; transforms each PR and its reviews into a structured text file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;PULL_REQUEST&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;54321&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nicolas-grekas&lt;/span&gt;
    &lt;span class="na"&gt;author_association&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MEMBER&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;HttpKernel&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="s"&gt;Fix edge case in exception handling&lt;/span&gt;

&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;REVIEWS&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;REVIEW_1234&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;replyTo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stof&lt;/span&gt;
        &lt;span class="na"&gt;reviewer_association&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MEMBER&lt;/span&gt;
        &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/Component/HttpKernel/Event/ExceptionEvent.php&lt;/span&gt;
        &lt;span class="na"&gt;diff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="err"&gt;@@&lt;/span&gt; &lt;span class="s"&gt;-88,7 +88,7 @@&lt;/span&gt;
             &lt;span class="s"&gt;public function getThrowable()&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;?\Throwable&lt;/span&gt;
             &lt;span class="s"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;We should keep the original exception here,&lt;/span&gt;
            &lt;span class="s"&gt;the wrapper is only for internal use.&lt;/span&gt;

        &lt;span class="na"&gt;reactions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;+1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
            &lt;span class="na"&gt;-1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;laugh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;hooray&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;confused&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;heart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;rocket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="na"&gt;eyes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These files live in &lt;code&gt;var/dataset/pull-{id}.txt&lt;/code&gt; and serve as the ground truth for vectorization.&lt;/p&gt;

&lt;h5&gt;
  
  
  Qdrant integration
&lt;/h5&gt;

&lt;p&gt;The vector store is wired into the container as &lt;code&gt;StoreInterface&lt;/code&gt;, via Qdrant's &lt;code&gt;StoreFactory&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoreInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Store&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autowire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoreFactory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'$collectionName'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'reviews'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'$endpoint'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'QDRANT_DSN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'$httpClient'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LoggedHttpClient&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'$embeddingsDimension'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'$embeddingsDistance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Cosine'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VectorizerInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Vectorizer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;autowire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'$model'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are worth mentioning. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;Vectorizer&lt;/code&gt; uses exactly the same &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; model as at build time — an embedding model &lt;code&gt;hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0&lt;/code&gt; producing 768-dimension vectors.
This part is extremely important if you don't want to end up comparing apples with oranges during MCP search. Indeed, a vector generated with a specific model can't be compared with a vector generated with another model.&lt;/li&gt;
&lt;li&gt;The store reuses the &lt;code&gt;LoggedHttpClient&lt;/code&gt; decorator, so every Qdrant round-trip benefits from structured logging on top of Symfony's native HTTP client.&lt;/li&gt;
&lt;/ol&gt;

&lt;h5&gt;
  
  
  Vectorization &amp;amp; storage
&lt;/h5&gt;

&lt;p&gt;This is where things get hairy — this part alone took me days.&lt;/p&gt;

&lt;h6&gt;
  
  
  What happens during vectorization?
&lt;/h6&gt;

&lt;p&gt;The file from &lt;code&gt;var/dataset/pull-{id}.txt&lt;/code&gt; is read, then sent to &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; to ask an embedding model — in my case &lt;code&gt;hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0&lt;/code&gt; — which generates a 768-dimension vector &lt;em&gt;(a number that depends on the embedding model)&lt;/em&gt; before it's sent back to symfony-ai by Ollama, to finally be saved into a vector space in &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;sequenceDiagram
    participant AI as Symfony AI (Builder)
    participant Ollama
    participant Model as embeddinggemma-300m
    participant Qdrant

    AI-&amp;gt;&amp;gt;Ollama: vectorize(dataset file content)
    Ollama-&amp;gt;&amp;gt;Model: model inference
    Model--&amp;gt;&amp;gt;Ollama: 768 float values
    Ollama--&amp;gt;&amp;gt;AI: Vector (768 dimensions)
    AI-&amp;gt;&amp;gt;Qdrant: add(VectorDocument)
    Qdrant--&amp;gt;&amp;gt;AI: confirmation
&lt;/pre&gt;

&lt;p&gt;Example vector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ ollama run hf.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF:Q8_0 &lt;span class="s1"&gt;'Hello world !'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;0.058340553,0.017256556,-0.0023928124,0.062416226,-0.019779362,-0.069838926,0.003351966,0.029903421,0.01617497,0.009088458,-0.024585545,-0.07013172,0.0077750348,0.03643439,-0.02245716,0.02035499,0.005985676,0.008291158,0.013118213,-0.074038,0.014411658,0.011837681,0.027627029,-0.008276582,0.059961967,0.018847544,0.040701613,0.020481525,0.004880007,-0.026033,0.028065553,-0.015691148,-0.06859542,-0.04025022,-0.0046045044,-0.033632968,0.01929922,0.01854895,-0.000545488,-0.37636346,0.05929959,0.0069747632,-0.026434837,0.018491298,-0.00945082,0.0009028575,0.024641853,-0.053274404,-0.043102805,0.0016308841,-0.04315744,-0.0024387056,-0.007468653,-0.03491168,-0.00315068,-0.023274362,-0.0016503683,-0.027008653,-0.0016653507,0.029124975,0.015810343,-0.020706663,0.03261976,-0.015973076,-0.0104695875,0.0027727042,0.03187403,0.25363848,0.016416604,0.027735965,0.009674886,-0.031506274,-0.01176536,-0.060261074,-0.0031871377,-0.01684568,0.029581062,-0.05600014,-0.02623269,0.04637347,-0.04044786,0.0036846441,0.008154481,0.0190515,-0.023374602,-0.011516434,0.01098124,0.008980432,-0.02016589,-0.020812696,0.046533223,0.009594602,-0.033848874,-0.046110246,0.027529772,0.029260637,-0.04092383,0.00048074988,0.01185442,0.0059498977,0.02239185,-0.0014466406,-0.00841961,-0.011190161,0.05701471,-0.015211558,-0.052781906,0.014623615,-0.0012096912,0.029300302,0.0044483966,0.028927691,-0.032093737,0.048145276,0.034030333,0.03062545,0.015509856,-0.008765529,-0.027545273,0.010858431,0.021899927,-0.008150199,0.0034826857,-0.03487983,-0.039671477,0.009751623,-0.019133179,-0.0030401512,-0.010503486,-0.017615957,0.0365356,0.001852526,-0.013896455,0.04652015,-0.049408674,0.0120107075,0.0065035336,0.0004583914,0.0074103116,-0.028435387,-0.0110742645,-0.0012466233,0.0014801751,-0.015979966,-0.028333941,0.0053472416,0.014286039,0.00054261123,0.049599133,-0.026553018,-0.021315206,0.043478087,0.032634746,0.0031313808,-0.0007238099,-0.0033655402,0.02283678,0.012112167,-0.019739753,-0.0080446005,0.018196804,0... &lt;span class="o"&gt;(&lt;/span&gt;line truncated to 2000 chars&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Why a sequential approach?
&lt;/h6&gt;

&lt;p&gt;As explained in the intro, without a dedicated GPU, it's my CPU — more precisely the iGPU integrated into my CPU — that has to do the vectorization work.&lt;br&gt;
Even though the iGPU shares RAM with the CPU and even though 32 GB are available &lt;em&gt;(depending on my system usage, running programs, ...)&lt;/em&gt;, RAM speed has nothing to do with graphics card memory, which is far faster and dedicated.&lt;br&gt;
Moreover, the CPU can only process a few operations in parallel, where the GPU executes thousands simultaneously, which makes matrix computations very slow and saturates my machine at 100%.&lt;/p&gt;

&lt;p&gt;As a result, only one vectorization at a time is possible.&lt;br&gt;
And so yes, a batch upsert would be faster, but the goal was to build a working pipeline using only local resources.&lt;/p&gt;

&lt;p&gt;It's not a limitation of PHP or Qdrant, just a pragmatic trade-off tied to the available hardware.&lt;/p&gt;
&lt;h6&gt;
  
  
  File-rename atomicity
&lt;/h6&gt;

&lt;p&gt;This is the most interesting design decision. Instead of a database table to track processed files, the &lt;code&gt;Builder&lt;/code&gt; uses atomic &lt;code&gt;rename()&lt;/code&gt; calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pull-{id}.txt               → ready to process
processing_pull-{id}.txt    → currently vectorizing
ragged_pull-{id}.txt        → vectorized successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ManagedStoreInterface&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;recoverOrphanedProcessingFiles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;scandir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'..'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RAGGED_PREFIX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PROCESSING_PREFIX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// another process claimed it&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;vectorizer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;vectorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;\count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$vector&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Wrong dimensions'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VectorDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^0-9]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/ragged_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/processing_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;datasetDirectory&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// rollback&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crash-safe by design: if the script dies mid-way, &lt;code&gt;recoverOrphanedProcessingFiles()&lt;/code&gt; re-queues orphaned &lt;code&gt;processing_*&lt;/code&gt; files on the next run. No locks, no database, no race conditions.&lt;/p&gt;

&lt;p&gt;Yes, the code shows that, despite what I said above:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As a result, only one vectorization at a time is possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, I still tried 🤣.&lt;/p&gt;

&lt;h4&gt;
  
  
  Usage
&lt;/h4&gt;

&lt;p&gt;The CLI exposes two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full pipeline: fetch → dataset → vectorize&lt;/span&gt;
php bin/console mcp:build

&lt;span class="c"&gt;# Re-vectorize without re-fetching&lt;/span&gt;
php bin/console mcp:build &lt;span class="nt"&gt;--skip-generation&lt;/span&gt;

&lt;span class="c"&gt;# Re-fetch without re-vectorizing&lt;/span&gt;
php bin/console mcp:build &lt;span class="nt"&gt;--skip-build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The MCP server
&lt;/h3&gt;

&lt;p&gt;For the LLM to have access to these freshly indexed reviews and run its searches by itself, you have to give it access.&lt;br&gt;
Everything happens through the MCP protocol following this simplified call pipeline:&lt;/p&gt;

&lt;pre&gt;flowchart TD
    H["MCP Tool Call (review_as_group/person)"] --&amp;gt; I["Retriever (semantic search)"]
    I --&amp;gt; G["Qdrant (collection: reviews)"]
    I --&amp;gt; J["LLM Client (Claude Desktop)"]
&lt;/pre&gt;

&lt;p&gt;The generation pipeline (fetch → dataset → vectorize) and the serving pipeline (retrieve → respond) share a single point in common: the &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; collection. &lt;/p&gt;

&lt;p&gt;The MCP server exposes two &lt;strong&gt;tools&lt;/strong&gt; and four &lt;strong&gt;prompts&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search by affiliation group (MEMBER, CONTRIBUTOR, NONE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_person&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search by a specific reviewer (nicolas-grekas, stof, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Formatted message using &lt;code&gt;review_as_group&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review_as_person&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Formatted message using &lt;code&gt;review_as_person&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_stofed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forces the reviewer to "stof" — the most prolific reviewer of Symfony Core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hq_review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-review: queries 14 reviewers and synthesizes a markdown report&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h4&gt;
  
  
  Retrieval: MCP Tools
&lt;/h4&gt;

&lt;p&gt;When a user sends a query through an MCP tool, here is what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The tool builds a query combining reviewer, file path, and diff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RetrieverInterface::retrieve()&lt;/code&gt; vectorizes the query via Ollama&lt;/li&gt;
&lt;li&gt;A cosine similarity search runs on Qdrant&lt;/li&gt;
&lt;li&gt;The matching &lt;code&gt;VectorDocument&lt;/code&gt; objects are returned&lt;/li&gt;
&lt;li&gt;Their &lt;code&gt;metadata['content']&lt;/code&gt; is extracted and assembled into context&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;sequenceDiagram
    participant Client as Claude Desktop
    participant MCP as MCP Server (stdio)
    participant Tool as review_as_person
    participant Retriever
    participant Ollama
    participant Qdrant

    Client-&amp;gt;&amp;gt;MCP: call_tool(review_as_person)
    MCP-&amp;gt;&amp;gt;Tool: __invoke(pseudonym, file, diff, limit)
    Tool-&amp;gt;&amp;gt;Retriever: retrieve(query, ['limit' =&amp;gt; limit])
    Retriever-&amp;gt;&amp;gt;Ollama: vectorize(query)
    Ollama--&amp;gt;&amp;gt;Retriever: query vector
    Retriever-&amp;gt;&amp;gt;Qdrant: cosine similarity search
    Qdrant--&amp;gt;&amp;gt;Retriever: closest documents
    Retriever--&amp;gt;&amp;gt;Tool: VectorDocument[]
    Tool--&amp;gt;&amp;gt;MCP: found reviews (text)
    MCP--&amp;gt;&amp;gt;Client: tool result
&lt;/pre&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;McpTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Tool retrieving a `limit` amount of reviews from `pseudonym` github user based on a given git a complete `file` path and `diff`. Results are separated by `\n\n---\n\n`.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Review matching file diff as github user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReviewAsPersonMatchingFileDiffTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;string&lt;/span&gt; &lt;span class="no"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'review_as_person'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$pseudonym&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$diff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;TXT
        reviewer: $pseudonym
        file: $file
        diff:
        {$diff}
        TXT;&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$retrieved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'limit'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$limit&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

            &lt;span class="nv"&gt;$return&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$retrieved&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$document&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMetadata&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;\is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'Error retrieving reviews: '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;\count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'No reviews found.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  The HQ Review prompt
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;hq_review&lt;/code&gt; prompt is the showcase feature. It queries 14 top-tier Symfony reviewers (&lt;a href="https://github.com/GromNaN" rel="noopener noreferrer"&gt;GromNaN&lt;/a&gt;, &lt;a href="https://github.com/dunglas" rel="noopener noreferrer"&gt;dunglas&lt;/a&gt;, &lt;a href="https://github.com/welcoMattic" rel="noopener noreferrer"&gt;welcoMattic&lt;/a&gt;, &lt;a href="https://github.com/nicolas-grekas" rel="noopener noreferrer"&gt;nicolas-grekas&lt;/a&gt;, &lt;a href="https://github.com/chalasr" rel="noopener noreferrer"&gt;chalasr&lt;/a&gt;, &lt;a href="https://github.com/stof" rel="noopener noreferrer"&gt;stof&lt;/a&gt;, &lt;a href="https://github.com/yceruto" rel="noopener noreferrer"&gt;yceruto&lt;/a&gt;, &lt;a href="https://github.com/mtarld" rel="noopener noreferrer"&gt;mtarld&lt;/a&gt;, &lt;a href="https://github.com/OskarStark" rel="noopener noreferrer"&gt;OskarStark&lt;/a&gt;, &lt;a href="https://github.com/xabbuh" rel="noopener noreferrer"&gt;xabbuh&lt;/a&gt;, &lt;a href="https://github.com/lyrixx" rel="noopener noreferrer"&gt;lyrixx&lt;/a&gt;, &lt;a href="https://github.com/kbond" rel="noopener noreferrer"&gt;kbond&lt;/a&gt;, &lt;a href="https://github.com/jderusse" rel="noopener noreferrer"&gt;jderusse&lt;/a&gt;, &lt;a href="https://github.com/alexandre-daubois" rel="noopener noreferrer"&gt;alexandre-daubois&lt;/a&gt;), collects their historical feedback on the same file/diff, and asks the LLM to synthesize a markdown report with feedback weighted per reviewer.&lt;/p&gt;

&lt;p&gt;The result is a code review that reads like a mini-symposium of Symfony Core maintainers — without requiring their time.&lt;/p&gt;
&lt;h4&gt;
  
  
  Usage
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; symfony-reviewer-mcp-cli /path/to/Dockerfile
docker run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;QDRANT_DSN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://host.docker.internal:6333 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OLLAMA_DSN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://host.docker.internal:11434 symfony-reviewer-mcp-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then configure Claude Desktop (or any MCP client) by adding the server to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symfony-reviewer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"--add-host=host.docker.internal:host-gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"QDRANT_DSN=http://host.docker.internal:6333"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OLLAMA_DSN=http://host.docker.internal:11434"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"symfony-reviewer-mcp-cli"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Environment variables
&lt;/h2&gt;

&lt;p&gt;All the configuration goes through &lt;code&gt;.env&lt;/code&gt; variables:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GITHUB_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;GitHub personal access token&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QDRANT_DSN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Qdrant service URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OLLAMA_DSN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama service URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLACKLISTED_PATTERN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON array of URL patterns to exclude from the cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APP_VERSION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version displayed in the MCP metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;This project taught me how the MCP protocol works, what a vector database is and how to use it. The architecture presented here is fairly standard for a RAG pipeline, but it was built with Symfony and can serve as a base for other experiments or use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  What worked well
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File-rename atomicity&lt;/strong&gt;: This pattern is elegant, crash-safe, and requires no infrastructure. Every PHP developer understands &lt;code&gt;rename()&lt;/code&gt;. No Redis locks, no database migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental pipeline&lt;/strong&gt;: Re-running &lt;code&gt;mcp:build&lt;/code&gt; with existing files is a no-op. Iteration is fast — you can tweak the vectorization and only process new files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP 8.x features&lt;/strong&gt;: Constructor promotion, readonly properties, the pipe operator (&lt;code&gt;|&amp;gt;&lt;/code&gt;), and invokable commands make the code significantly cleaner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; locally&lt;/strong&gt;: embeddinggemma-300m runs on CPU without issues. 768 dimensions is modest enough for fast queries but rich enough for semantic search over code reviews.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What needs improvement
&lt;/h3&gt;

&lt;p&gt;Had I had a more powerful machine with a dedicated GPU or unified memory (👋 Mac owners), I might have been able to change the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Naive Qdrant interaction&lt;/strong&gt;: Documents are added one at a time. A batch upsert would be significantly faster for large volumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No incremental RAG updates&lt;/strong&gt;: The pipeline is add-only. There's no "builtin" mechanism &lt;em&gt;(it's possible via the Qdrant dashboard)&lt;/em&gt; to purge or update existing vectors when PR comments are edited on GitHub. Which is a real/false problem in itself, since you rarely find new comments on already-merged pull requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add your own conventions&lt;/strong&gt;: I added Symfony's reviews, but you too can modify and adapt the code to rely on an additional data corpus, like your colleagues' reviews.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I'd do differently
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Batch vectorization&lt;/strong&gt;: Group documents and vectorize in batches for higher throughput&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async fetching&lt;/strong&gt;: The data fetching phase is sequential per PR. Concurrent requests would significantly cut the initial build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub webhook&lt;/strong&gt;: Instead of periodically rebuilding, listen for merged PR events and update the dataset incrementally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding model evaluation&lt;/strong&gt;: 768 dimensions works well, but I should compare different models: smaller ones (like &lt;a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;all-MiniLM-L6-v2&lt;/a&gt;, 384 dimensions) to improve performance, or much larger ones to assess the potential gain in search quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wider context: PR comments and full diff&lt;/strong&gt;: the current dataset only keeps review comments attached to a specific diff. It ignores general PR comments (issue comments, description) and above all the PR's full diff — a reviewer never judges an isolated line, they judge it in the context of the whole change. Injecting both would give the model far more material to understand why a review was worded the way it was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploit the &lt;code&gt;metadata&lt;/code&gt; field for GitHub's raw JSON&lt;/strong&gt;: &lt;a href="https://github.com/symfony/ai-store" rel="noopener noreferrer"&gt;&lt;code&gt;symfony/ai-store&lt;/code&gt;&lt;/a&gt; attaches a &lt;code&gt;Metadata&lt;/code&gt; object (&lt;code&gt;Symfony\AI\Store\Document\Metadata&lt;/code&gt;) to every &lt;code&gt;VectorDocument&lt;/code&gt;, which travels all the way to the chosen store. With Qdrant, this &lt;code&gt;Metadata&lt;/code&gt; maps exactly to the notion of &lt;em&gt;payload&lt;/em&gt;: an arbitrary JSON object attached to each point, natively indexable and filterable — for example filtering by &lt;code&gt;reviewer_association&lt;/code&gt;, by date, or by reaction count, without re-parsing the dataset text. Today, only &lt;code&gt;content&lt;/code&gt; (the assembled dataset file text) is stored there; I would have added GitHub's raw API response (PR + review + reactions), to keep an exploitable trace independent of the generated text format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploit the GitHub JSON reactions&lt;/strong&gt;: Reactions would actually lend themselves to more than a simple filter: they could weight the search score itself, not just be returned in the &lt;code&gt;content&lt;/code&gt;. Since version 1.14, Qdrant offers a &lt;a href="https://qdrant.tech/documentation/search/hybrid-queries/" rel="noopener noreferrer"&gt;&lt;em&gt;Formula Query&lt;/em&gt;&lt;/a&gt; that lets you compose a final score from the initial similarity score and payload fields, in a single re-ranking formula. A review with ten &lt;code&gt;+1&lt;/code&gt; would then rank above an isolated review with zero reactions, at equal vector similarity — a way to surface the opinions the community itself validated. A feature worth exploring.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Does it work?
&lt;/h2&gt;

&lt;p&gt;The big question: "are the reviews produced actually better?".&lt;/p&gt;

&lt;p&gt;Answering Yes would be partially wrong. Indeed, today my evaluation remains essentially based on a general feeling rather than on real, tangible, quantifiable data.&lt;br&gt;
The generated answers seem to me far more in the tone of a review that would have been made on Symfony's GitHub repository, and therefore closer to its conventions, than the answers obtained with a context-free LLM.&lt;br&gt;
Finally, since an LLM is "probabilistic" by nature, I don't believe it's relevant to assert, based on my own observations and personal feeling alone, that the functioning of a tool is established.&lt;/p&gt;

&lt;p&gt;Here is a example of a review done on that project to give you an idea :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;### 1. fabpot&lt;/span&gt;
&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*Files&lt;/span&gt;&lt;span class="s"&gt;:** `src/HTTP/CachedHttpClient.php`, `src/HTTP/CachedResponse.php`&lt;/span&gt;

&lt;span class="na"&gt;&amp;gt; Thanks for the PR! A few things before we can merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; * Why is the cache key based on the URL only? For `POST` requests the body matters, otherwise all `/api/embed` calls will return the same result. This is a real bug, not an optimization issue.&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; * Please add a test covering the "non-buffered" response case — `getContent()` then `toArray()` will fail on `EventSourceHttpClient` responses.&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; * Do we need a `CHANGELOG` entry for the `CachedHttpClient`? I think we can keep it internal.&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s"&gt; Otherwise the approach is clean. Once the cache key is fixed, we can squash the commits.&lt;/span&gt;

&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;

&lt;span class="gu"&gt;### 2. nicolas-grekas&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedResponse.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Reading the response body twice is going to blow up the moment the response is not buffered. `toArray()` internally calls `getContent()` — so snapshot the content once and `json_decode` it, don't call both.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the decorated chain is wrong: `service(LoggedHttpClient::class)` no longer resolves to the logger once you decorate it with `CachedHttpClient`. Qdrant and Ollama are being routed through the GitHub cache without intent. Decorate a dedicated alias, e.g. `cached.github.http_client`, and keep `HttpClientInterface` as the plain chain.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; One more: `getContent(false)` is being snapshotted eagerly in the constructor — that defeats lazy streaming for large GitHub responses. Buffer lazily.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 3. stof&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Service wiring nit: decoration replaces the decorated id, so `$httpClient =&amp;gt; service(LoggedHttpClient::class)` gives you the `CachedHttpClient`, not the logging client. That's a scope leak — the Qdrant store is now coupled to a cache tuned for the GitHub API (see `BLACKLISTED_PATTERN`).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; I'd define a dedicated `logged.http_client` service for the store/platform and only decorate `cached.github.http_client` for the GitHub fetchers. Also check the `-&amp;gt;decorate(..., priority: 1)` priorities — with equal priorities the order of application is by declaration order, which is fragile to read.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; And the `json:BLACKLISTED_PATTERN` env var — document its format in the README.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 4. weaverryan&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/MCP/Tools/ReviewAsPersonMatchingFileDiffTool.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Hey! Love the ergonomics of this tool — the `review_as_person` name makes the intent super clear. Great job composing the query with the pseudonym, file, and diff.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; One DX thought: when no reviews are found we return `No reviews found.` — that's good. But maybe give the caller a hint that they can reduce `limit` or widen the diff? Small thing, ignore if you want.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the metadata `content` check with the warning log is nice defensive coding. Keep it up! 🎉&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 5. derrabus&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Two things:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 1. `$key = md5($method.$url)` — please include the serialized options/body. Hash collisions here are silent correctness bugs, not just perf issues.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 2. The `preg_match` on a compiled regex built via pipe chains is clever but hard to read. Since the blacklist is a list of exact strings, why not use `in_array` or a simple `str_starts_with` on the pattern list? Keep it simple.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the code is `final readonly` — good. But it implements `ResetInterface`; make sure the decorated inner `reset()` is reachable in `withOptions()` clones (it is, since you forward to the scoped client — just double-check the cache/lifecycle after cloning).&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 6. xabbuh&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedResponse.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; I have concerns about the snapshot in the constructor:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Calling `$response-&amp;gt;getContent()` eagerly downloads and stores the whole payload. For the GitHub fetchers this is fine, but a general-purpose cache should stream lazily.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * More importantly, `getContent()` followed by `toArray()` breaks for responses that disabled buffering (the Ollama `EventSourceHttpClient` forces `buffer =&amp;gt; false`). This is an exploitable/observable crash — at minimum it should throw a clear `TransportException` or read once.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * `getInfo()` filtering out closures is a nice touch, but the returned array is shallow — nested closures could still leak. Use a recursive filter or `json_encode/decode` the info array.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 7. Tobion&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The pipeline operator chains in `request()` are over-engineered for building a regex. `implode('|', array_map(preg_quote(...), $this-&amp;gt;blacklistedPatterns))` is enough. As written, an empty blacklist produces the pattern `#^$#` which would match an empty call string — harmless, but misleading.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; More importantly: cache invalidation. There is none — responses are cached for a year (`defaultLifetime`). GitHub data changes; the fetchers need a way to bust the cache (e.g. include a version/tag in the key or a TTL per-URL). Otherwise reviews fetched once are served stale forever.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 8. mpdude&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/RAG/Builder.php`&lt;/span&gt; (via &lt;span class="sb"&gt;`Store`&lt;/span&gt;)
&lt;span class="gt"&gt;
&amp;gt; The Qdrant indexing loop swallows exceptions and logs `Index failed` — but the build then *continues* (I saw `Indexing document continues`). If a document fails vectorization, subsequent documents are still sent. That means the collection is only partially populated, and `review_as_person` will silently return "No reviews found" or partial results.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Please make the build fail-fast or at least surface a summary count at the end ("indexed X / failed Y") so operators know the dataset is incomplete. Right now nothing tells us that only ~1106 of 8691 documents made it in.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 9. WouterJ&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The container config reads really well — the decoration chain is easy to follow. Nice use of `env('json:BLACKLISTED_PATTERN')` and `StoreFactory::create(...)`.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Minor: the `logged.http_client` vs `cached.github.http_client` distinction is muddied because both decorators use `priority: 1` and decorate each other's ids. I'd give them explicit service aliases (`github.logged.http_client`, etc.) so the intent is obvious. Also the unused `'stream_handler'` monolog handler and the commented-out `http` transport block could be cleaned up before merge.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 10. alexislefebvre&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`tests/HTTP/CachedHttpClientTest.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Nice test coverage — you test blacklist skipping, persistence across instances, `withOptions` cloning, and `reset`. 👍&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Missing cases I'd love to see:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 1. A `POST` request with a body — assert that different bodies don't collide in the cache (this would catch the `md5(method.url)` bug).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 2. A non-buffered/streaming response (`MockResponse` with `buffer =&amp;gt; false` is hard to fake; but at least an SSE-like response) going through `CachedResponse` without throwing.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; 3. The cache should not be hit for `POST`/`PUT` (or should include the body in the key) — please encode that expectation in a test.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 11. Nyholm&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/HTTP/LoggedHttpClient.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; As the http-client component maintainer: don't re-implement caching. Symfony's `HttpClient` supports a `cache` option natively via the `http_cache` from the contracts, and it handles cache keys, headers, `Vary`, and ETags properly. Rolling your own `md5(method.url)` cache key is a regression waiting to happen (it already broke on POST bodies).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; If you keep the custom decorator, at least delegate to `CacheItemPoolInterface` semantics and include the request payload + relevant headers in the key. And please make `stream()` forward correctly — it does, but note that cached responses can never stream, which may surprise callers.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also: `FileSystemAdapter` on a single Docker container is fine, but for multi-instance deploys you'll want a shared pool (Redis). Worth a comment.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 12. jderusse&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/RAG/Builder.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The elephant in the room: the per-document vectorization loop is serial. 8691 documents, ~10s each — that's ~24h to build the RAG, and with the cache bug most embeddings were identical (same URL → same key). That's why retrieval feels broken.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Fixes I'd push for:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Parallelize vectorization with Symfony's `AsyncResponse` / `stream()` over batches.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Include the body in the cache key (obviously).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; * Index the docs that failed (`Index failed` ×636) with retry/backoff.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also `Builder` should checkpoint progress so a crash doesn't restart from zero.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 13. chalasr&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/MCP/Tools/ReviewAsPersonMatchingFileDiffTool.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/Command/ServeCommand.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Tool ergonomics are good — `limit` is explicit, errors are caught and surfaced. But: the error path returns `Error retrieving reviews: {message}` as a *successful* tool result. For an MCP server, real failures should be proper exceptions/tool errors, not strings, otherwise the client can't distinguish "no data" from "server broken".&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Also, the query string embeds the raw diff with no size guard — a huge diff will blow the embedding context window. Truncate or chunk the diff.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; And the serve command: make sure `APP_DEBUG` is off in prod and there's a graceful shutdown on SIGTERM.&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;### 14. yceruto&lt;/span&gt;
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; &lt;span class="sb"&gt;`src/Kernel.php`&lt;/span&gt;, &lt;span class="sb"&gt;`src/HTTP/CachedHttpClient.php`&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; The routing of the HTTP decorators deserves attention: `LoggedHttpClient::class` is decorated by `CachedHttpClient`, so every consumer referencing it — including the Qdrant store — ends up behind the GitHub cache. That coupling is accidental.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; I'd restructure like this:&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; ```&lt;/span&gt;

&lt;span class="gt"&gt;
&amp;gt; HttpClientInterface          # plain&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;  └─ logged.http_client       # logging only (for Qdrant/Ollama)&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;  └─ cached.github.http_client # cache + github token (for GitHub fetchers)&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Two separate chains, no cross-decorating. Then the cache key issue (URL-only, no body) also only affects GitHub GETs, which is safe.&lt;/p&gt;

&lt;p&gt;After that, the 14 &lt;code&gt;review_as_person&lt;/code&gt; calls will stop returning the &lt;code&gt;buffering is disabled&lt;/code&gt; error and start returning real reviews.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


## To conclude

&amp;lt;div style="width:100%;height:0;padding-bottom:56%;position:relative;"&amp;gt;
    &amp;lt;iframe src="https://giphy.com/embed/NRiRXQTwbijNba2l2l" width="100%" height="100%" style="position:absolute" frameBorder="0" class="giphy-embed" allowFullScreen&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;a href="https://giphy.com/gifs/The-Animal-Crackers-Movie-baking-try-it-NRiRXQTwbijNba2l2l"&amp;gt;via GIPHY&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;

Try it yourself — the [project](https://github.com/ktherage/symfony-review-mcp) is designed to be self-contained and independent. The data is publicly accessible, and you can spin up a Qdrant and Ollama instance easily with [Docker](https://www.docker.com/).

To install it:



```bash
git clone https://github.com/ktherage/symfony-review-mcp
cd symfony-review-mcp
docker compose run --rm cli composer install
docker compose run --rm cli bin/console mcp:build
docker compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most surprising thing I learned building this project: PHP is a perfectly viable language for RAG pipelines. Symfony's HttpClient, Cache, and Console components, combined with the &lt;a href="https://github.com/symfony/ai" rel="noopener noreferrer"&gt;&lt;code&gt;symfony/ai-*&lt;/code&gt;&lt;/a&gt; packages, handle everything from HTTP decoration to vector database operations. &lt;/p&gt;

&lt;p&gt;You don't need Python to do semantic search.&lt;/p&gt;

&lt;p&gt;Sometimes the best tool for the job is the one you already master.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>symfony</category>
      <category>php</category>
      <category>rag</category>
    </item>
    <item>
      <title>API Platform Con 2025 - Day 2</title>
      <dc:creator>Thérage Kévin</dc:creator>
      <pubDate>Wed, 24 Sep 2025 08:00:19 +0000</pubDate>
      <link>https://dev.to/sensiolabs/api-platform-con-2025-day-2-ebd</link>
      <guid>https://dev.to/sensiolabs/api-platform-con-2025-day-2-ebd</guid>
      <description>&lt;p&gt;I had the opportunity to attend the API Platform Con 2025 thanks to SensioLabs and here is what I learned through the talks I viewed.&lt;/p&gt;




&lt;h2&gt;
  
  
  How LLMs are changing the way we should build APIs (Fabien Potentier)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slides of this talk are available : &lt;a href="https://speakerdeck.com/fabpot/how-ai-agents-are-changing-the-way-we-should-build-apis" rel="noopener noreferrer"&gt;https://speakerdeck.com/fabpot/how-ai-agents-are-changing-the-way-we-should-build-apis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fabien Potentier shared insights about how Large Language Models are fundamentally changing the way we need to think about API design. As he mentioned, this is a world that changes so fast that some assertions might already be outdated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agents ?
&lt;/h3&gt;

&lt;p&gt;LLMs are evolving beyond simple text generation into autonomous agents. According to Anthropic's definition, an agent is an LLM using tools in a loop. These LLMs are self-directed - they can reason about things, they can plan, and have memory.&lt;/p&gt;

&lt;p&gt;An AI agent is kind of a mix between a machine and a human, combining the computational power of machines with human-like reasoning capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who can consume your app ?
&lt;/h3&gt;

&lt;p&gt;Back in the days, the consumers were clearly defined:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Human users only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CLI tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only for developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;API:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only for machines&lt;/li&gt;
&lt;li&gt;Semi-private (to decouple frontend) or public&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nowadays, APIs are mostly used to expose data, but AI agents have changed the game completely. They are able to interact with all three interfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Websites can be scraped by AI&lt;/strong&gt; - agents can navigate and extract information from web interfaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI tools can be used through MCP servers&lt;/strong&gt; - providing structured tool access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIs&lt;/strong&gt; - LLMs (e.g., in chatbots) are often wrappers on top of APIs. Furthermore, LLMs can also write API calls directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But all three have different expectations, and this creates new challenges.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge: APIs for Humans vs. Machines vs. AI Agents
&lt;/h3&gt;

&lt;p&gt;APIs are optimized for machines, but when something breaks, you need a human in the loop. However, AI agents are autonomous but, like humans, they need help and guidance.&lt;/p&gt;

&lt;p&gt;Take HTTP status codes as an example. They provide information about problems, but AI agents need more context.&lt;br&gt;
HTTP responses can provide context about errors, but responses provided by APIs might not be up-to-date or accurate, causing LLMs to get stuck.&lt;/p&gt;

&lt;p&gt;Here is a common workflow pattern followed by LLM : Thought → Action → Observation.&lt;br&gt;
Without guidance provided via prompts, it can loop over the same problem, encountering the same Observation after performing the same Action—potentially forever.&lt;br&gt;
LLMs will try to guess and self-correct, which is probably bad for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Costly&lt;/strong&gt; - more API calls and processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time loss&lt;/strong&gt; - inefficient problem resolution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource greedy&lt;/strong&gt; - GPU time and electricity are consumed without solving the problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; The fewer round trips you have with an LLM, the more "deterministic" it becomes, even though LLMs are inherently not deterministic.&lt;/p&gt;
&lt;h3&gt;
  
  
  Best Practices for LLM-Friendly APIs
&lt;/h3&gt;

&lt;p&gt;Everything that is valid for LLMs is also valid for humans.&lt;/p&gt;
&lt;h4&gt;
  
  
  Error Messages
&lt;/h4&gt;

&lt;p&gt;Be precise with your error messages: "Bad date format. Use 'YYYY-MM-DD'."&lt;br&gt;
Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer tokens consumed&lt;/li&gt;
&lt;li&gt;Smaller context window usage&lt;/li&gt;
&lt;li&gt;Faster resolution&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Consistent Naming
&lt;/h4&gt;

&lt;p&gt;Use the same naming pattern everywhere. For example, use &lt;code&gt;user_id&lt;/code&gt; consistently across all endpoints.&lt;br&gt;
Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable patterns&lt;/li&gt;
&lt;li&gt;LLMs like consistency&lt;/li&gt;
&lt;li&gt;Easier to understand and use&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Documentation
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Fix examples and remove outdated content&lt;/li&gt;
&lt;li&gt;Fewer problems and hallucinations&lt;/li&gt;
&lt;li&gt;Consider using &lt;code&gt;llms.txt&lt;/code&gt; files - documentation specifically formatted for LLMs in Markdown&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Performance Considerations
&lt;/h4&gt;

&lt;p&gt;AI agents are slow, so reducing the number of requests provides a significant performance boost.&lt;/p&gt;
&lt;h4&gt;
  
  
  Intent-First API Design
&lt;/h4&gt;

&lt;p&gt;Design your APIs to capture and preserve user intent rather than just exposing CRUD operations.&lt;/p&gt;
&lt;h3&gt;
  
  
  Testing Challenges
&lt;/h3&gt;

&lt;p&gt;Testing AI agents is super difficult because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLMs are not deterministic&lt;/li&gt;
&lt;li&gt;You need to set temperature to 0 for more consistent results&lt;/li&gt;
&lt;li&gt;Use concise prompts&lt;/li&gt;
&lt;li&gt;Ultimately, you need a human to judge the quality of actions performed by the LLM, making automated testing complex&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Technical Considerations
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Tokens vs. Text
&lt;/h4&gt;

&lt;p&gt;Understanding tokenization is crucial. Tools like &lt;a href="https://tiktokenizer.vercel.app" rel="noopener noreferrer"&gt;tiktokenizer.vercel.app&lt;/a&gt; help visualize how text is tokenized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language matters:&lt;/strong&gt; English costs less in tokens than French or Japanese for example&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique IDs are problematic:&lt;/strong&gt; UUIDs are bad for tokenizers, ULIDs are better&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shorter is not always better&lt;/strong&gt; in terms of token efficiency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date formats matter&lt;/strong&gt; for token consumption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON is not the best format&lt;/strong&gt; for LLMs - Markdown is better and uses fewer tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More tokens require more money and create larger context windows, which negatively impact AI agent response times and relevance.&lt;/p&gt;
&lt;h4&gt;
  
  
  Security and Credentials
&lt;/h4&gt;

&lt;p&gt;AI agents are bad at dealing with credentials. The solution is to use MCP (Model Context Protocol) servers that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle credentials securely&lt;/li&gt;
&lt;li&gt;Provide tools to AI agents&lt;/li&gt;
&lt;li&gt;Give limited scope permissions to MCP actions&lt;/li&gt;
&lt;li&gt;Act as a secure intermediary between the LLM and your APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Log Everything
&lt;/h3&gt;

&lt;p&gt;Given the complexity and unpredictability of AI agent interactions, comprehensive logging becomes essential for debugging and improving the system.&lt;/p&gt;
&lt;h3&gt;
  
  
  The New Experience: AX (AI Experience)
&lt;/h3&gt;

&lt;p&gt;Fabien introduced the concept of AX (AI Experience) alongside the familiar UX (User Experience) and DX (Developer Experience). This represents a new dimension of API design focused on how well your API works with AI agents.&lt;/p&gt;

&lt;p&gt;Key aspects of good AX include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Up-to-date documentation and examples (avoiding outdated examples that could mislead the LLM)&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;llms.txt&lt;/code&gt; files with all useful documentation for the LLM in Markdown format&lt;/li&gt;
&lt;li&gt;Clear, consistent error messages&lt;/li&gt;
&lt;li&gt;Intent-preserving API design&lt;/li&gt;
&lt;li&gt;Efficient token usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fascinating aspect is that many improvements for AX also benefit traditional DX, making APIs better for both human developers and AI agents.&lt;/p&gt;


&lt;h2&gt;
  
  
  Build a decoupled application with API Platform and Vue.js (Nathan de Pachtere)
&lt;/h2&gt;

&lt;p&gt;Nathan de Pachtere shared his experience building decoupled applications using API Platform for the backend and Vue.js for the frontend. His insights covered the differences between headless and decoupled approaches, practical implementation strategies, and the benefits of monorepo architecture.&lt;/p&gt;
&lt;h3&gt;
  
  
  Headless
&lt;/h3&gt;

&lt;p&gt;Headless architecture involves creating a business-focused API that anyone can use independently. Think of the GitHub API - it's designed as a standalone service that provides all the functionality needed to interact with GitHub's features, completely independent of any specific frontend implementation.&lt;/p&gt;

&lt;p&gt;The goal is to create business logic and provide an API that everyone can utilize for their own purposes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Decoupled
&lt;/h3&gt;

&lt;p&gt;Decoupled architecture is similar but more focused. You provide a frontend that relies specifically on your API, creating what's essentially a backend-for-frontend pattern. The API doesn't seem to be made for independent use outside of the specific application - it's tailored to serve the frontend's exact needs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Choose This Approach?
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Advantages
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separation of responsibilities&lt;/strong&gt; - Clear boundaries between frontend and backend concerns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team management&lt;/strong&gt; - Enables specialist teams to work independently on their expertise areas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capitalization&lt;/strong&gt; - Reusable components and logic across different projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future-proofing&lt;/strong&gt; - AI might be the interface used in the future, making an API-first approach valuable&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Disadvantages
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complexity&lt;/strong&gt; - More complex setup for existing projects that need to be refactored&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Headless Implementation
&lt;/h3&gt;
&lt;h4&gt;
  
  
  using API Platform
&lt;/h4&gt;

&lt;p&gt;The process follows a business-driven approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Represent the API based on business needs&lt;/strong&gt; - Focus on what the business actually does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translate into entities and workflows&lt;/strong&gt; - Convert business processes into technical implementations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write only the necessary code&lt;/strong&gt; - Keep it simple initially&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then optimize and refactor&lt;/strong&gt; - Improve performance and code quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate&lt;/strong&gt; - Continuously improve based on feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go beyond CRUD&lt;/strong&gt; - Implement meaningful business operations, not just basic data manipulation&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;
  
  
  Providing API Keys
&lt;/h4&gt;

&lt;p&gt;For machine-to-machine authentication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create a simple interface&lt;/strong&gt; for creating/deleting configurable keys with specific permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider external identity providers&lt;/strong&gt; like Keycloak or Zitadel for more advanced use cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Important principle:&lt;/strong&gt; Don't mix human users with machine users - they have different needs and security requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nathan emphasized making tests simple and easy to implement, integrating them naturally into the development workflow rather than treating them as an afterthought.&lt;/p&gt;
&lt;h4&gt;
  
  
  Deprecation Strategy
&lt;/h4&gt;

&lt;p&gt;When evolving your API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deprecate endpoints, resources, and properties&lt;/strong&gt; gradually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give consumers time to adapt&lt;/strong&gt; to changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communicate changes clearly&lt;/strong&gt; before removing functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach maintains backward compatibility while allowing the API to evolve.&lt;/p&gt;
&lt;h3&gt;
  
  
  Decoupled Implementation
&lt;/h3&gt;
&lt;h4&gt;
  
  
  using Vue.js
&lt;/h4&gt;

&lt;p&gt;Nathan chose Vue.js for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Independent and community-driven&lt;/strong&gt; - Not controlled by a single corporation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composition API (Vue 3)&lt;/strong&gt; - Promotes code reusability and better organization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excellent Developer Experience&lt;/strong&gt; - Great tooling and development workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top performance&lt;/strong&gt; - Fast and efficient (until the next framework comes along, as he joked)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  API Connection
&lt;/h4&gt;

&lt;p&gt;For connecting the Vue.js frontend to the API Platform backend:&lt;/p&gt;
&lt;h5&gt;
  
  
  Code Generation
&lt;/h5&gt;

&lt;p&gt;Use &lt;strong&gt;openapi-ts.dev&lt;/strong&gt; to generate TypeScript types and composables from your OpenAPI specification. This ensures type safety and reduces manual work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important principle:&lt;/strong&gt; Don't use the generated types directly as base objects in your frontend. Create your own models to maintain proper decoupling between frontend and backend representations.&lt;/p&gt;
&lt;h5&gt;
  
  
  HTTP Client and State Management
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tanstack Query&lt;/strong&gt; - For efficient data fetching and caching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript throughout&lt;/strong&gt; - Ensures type safety across the application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code for Vue.js development&lt;/strong&gt; - Better integration compared to JetBrains IDEs for Vue.js work&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;
  
  
  High-Level SDKs
&lt;/h5&gt;

&lt;p&gt;Provide high-level SDKs to facilitate API integration, making it easier for developers to work with your API.&lt;/p&gt;
&lt;h3&gt;
  
  
  Version Management
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Polyrepo vs Monorepo
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Monorepo doesn't mean monolith&lt;/strong&gt; - this is a crucial distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monorepo&lt;/strong&gt; = Multiple separate projects in a single repository&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monolith&lt;/strong&gt; = Single application handling everything&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Monorepo Benefits
&lt;/h4&gt;

&lt;p&gt;The goal is to simplify the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unified way of thinking about code&lt;/strong&gt; - Consistent patterns across projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; - Shared tooling and configurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilitates sharing&lt;/strong&gt; - Easy code and component reuse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More efficient teamwork&lt;/strong&gt; - Simplified collaboration and dependency management&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Tooling
&lt;/h4&gt;

&lt;p&gt;Nathan recommended &lt;strong&gt;moonrepo.dev&lt;/strong&gt; as an open-source tool for managing monorepos. You can find more information at &lt;strong&gt;monorepo.tools&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Real-World Example
&lt;/h4&gt;

&lt;p&gt;Nathan shared their experience with enormous benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code generalization&lt;/strong&gt; - Reusable patterns and components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Functionality sharing&lt;/strong&gt; - Common libraries across projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technology-based organization&lt;/strong&gt; - Projects use shared libraries organized by technology stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see a practical example of this approach in the &lt;a href="https://github.com/alpsify/lychen" rel="noopener noreferrer"&gt;Lychen project&lt;/a&gt; (&lt;a href="https://lychen.fr/" rel="noopener noreferrer"&gt;lychen.fr&lt;/a&gt;), which demonstrates a well-structured monorepo with clear separation between backend, frontend, and shared tools.&lt;/p&gt;

&lt;p&gt;The Lychen project shows how to organize a monorepo with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt; (API Platform/PHP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; (Vue.js/TypeScript)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared tooling&lt;/strong&gt; (Moonrepo, Docker, testing tools)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear technology boundaries&lt;/strong&gt; while maintaining efficient code sharing&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Jean-Beru presents: Fun with flags (Hubert Lenoir)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slides of this talk are available : &lt;a href="https://jean-beru.github.io/2025_09_apiplatformcon_fun_with_flags" rel="noopener noreferrer"&gt;https://jean-beru.github.io/2025_09_apiplatformcon_fun_with_flags&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Jean-Beru (Hubert Lenoir) presented the fascinating world of feature flags and their practical implementation. As Uncle Ben said in Spider-Man: "With great power comes great responsibility" - and feature flags are indeed a powerful tool that requires careful consideration.&lt;/p&gt;
&lt;h3&gt;
  
  
  What are Feature Flags?
&lt;/h3&gt;

&lt;p&gt;Feature flags (also known as feature flipping or feature toggles) are a software development technique that allows you to turn features on or off without deploying new code. They act as conditional statements in your code that determine whether a particular feature should be enabled or disabled for specific users, environments, or conditions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Types of Feature Flags
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Release Flags
&lt;/h4&gt;

&lt;p&gt;Mainly used to test new features in production environments safely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous development&lt;/strong&gt; - Even if a feature is not ready, you can continue developing and deploying (not ready = disabled)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe deployment&lt;/strong&gt; - Deploy code with features turned off, then enable them when ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual rollout&lt;/strong&gt; - Enable features for small groups before full release&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Experiment Flags
&lt;/h4&gt;

&lt;p&gt;Used to compare different versions of your application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A/B testing&lt;/strong&gt; - Compare different implementations or user experiences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Must be followed by metrics&lt;/strong&gt; - Track performance and user behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial enablement&lt;/strong&gt; - Enable for specific percentages (e.g., 20% of users)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach allows data-driven decisions about which features or implementations work best for your users.&lt;/p&gt;
&lt;h4&gt;
  
  
  Permission Flags
&lt;/h4&gt;

&lt;p&gt;Control access to features based on user permissions or subscription levels.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blocking access based on permissions&lt;/strong&gt; - For example, paid features only available to premium subscribers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based feature access&lt;/strong&gt; - Different features for different user types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription tiers&lt;/strong&gt; - Enable advanced features for higher-tier customers&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Operational Flags
&lt;/h4&gt;

&lt;p&gt;Security belt and kill switch functionality.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Allow disabling cumbersome features&lt;/strong&gt; - Quickly turn off resource-intensive features during high load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency response&lt;/strong&gt; - Disable problematic features without deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance management&lt;/strong&gt; - Control system load by toggling expensive operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more detailed information about feature flag patterns, Martin Fowler has an excellent article at &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/feature-toggles.html&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;There are many feature flag providers available in the market, but the implementation doesn't necessarily need to use Symfony's Security component.&lt;/p&gt;
&lt;h4&gt;
  
  
  Why Not Security Component?
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Restricted to current user context&lt;/strong&gt; - Limitations when flags need to work across different user contexts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication timing issues&lt;/strong&gt; - Authentication happens after routing, which can lead to unwanted forbidden error codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility needs&lt;/strong&gt; - Custom implementations can better integrate with existing providers like Unleash&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Requirements for a Good Implementation
&lt;/h4&gt;

&lt;p&gt;A solid feature flag system should provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt; - Easy to implement and use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrated debugging&lt;/strong&gt; - Clear visibility into which flags are active&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple sources&lt;/strong&gt; - Ability to switch between different flag providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Various provider support&lt;/strong&gt; - Work with different feature flag services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cacheable&lt;/strong&gt; - Performance optimization through caching mechanisms&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Symfony Integration
&lt;/h4&gt;

&lt;p&gt;There's a work-in-progress FeatureFlag component for Symfony (PR #53213). This component aims to provide native support for feature flags within the Symfony ecosystem.&lt;/p&gt;
&lt;h3&gt;
  
  
  With API Platform
&lt;/h3&gt;

&lt;p&gt;Feature flags can be easily tested via a separated bundle: &lt;a href="https://github.com/ajgarlag/feature-flag-bundle" rel="noopener noreferrer"&gt;ajgarlag/feature-flag-bundle&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Implementation Steps
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decoration of API Platform provider&lt;/strong&gt; - Use the decorator pattern to wrap existing providers with feature flag logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use FeatureFlag WIP component interface&lt;/strong&gt; - Integrate with the upcoming Symfony FeatureFlag component&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;
  
  
  Example with GitLab Provider
&lt;/h4&gt;

&lt;p&gt;GitLab provides a feature flag service that uses Unleash in the background. This integration allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manage flags through GitLab UI&lt;/strong&gt; - Familiar interface for teams already using GitLab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage Unleash capabilities&lt;/strong&gt; - Powerful feature flag engine under the hood&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate with CI/CD pipelines&lt;/strong&gt; - Automatic flag management as part of deployment process&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Profiler Integration
&lt;/h4&gt;

&lt;p&gt;The implementation includes Symfony Profiler integration, providing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debug information&lt;/strong&gt; - See which flags are active during development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance insights&lt;/strong&gt; - Monitor the impact of feature flag checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development workflow&lt;/strong&gt; - Easy testing and debugging of flag behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;p&gt;Implementing feature flags brings several significant benefits:&lt;/p&gt;
&lt;h4&gt;
  
  
  Deploy Continuously
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decouple deployment from release&lt;/strong&gt; - Deploy code safely with features disabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce deployment risk&lt;/strong&gt; - Lower chance of breaking production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster iteration cycles&lt;/strong&gt; - More frequent, smaller deployments&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Progressive Testing
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A/B testing capabilities&lt;/strong&gt; - Compare different approaches with real users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual rollouts&lt;/strong&gt; - Start with small user groups and expand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data-driven decisions&lt;/strong&gt; - Make choices based on actual usage metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Quick Turn Off
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No redeployment needed&lt;/strong&gt; - Instantly disable problematic features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency response&lt;/strong&gt; - Rapid reaction to production issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business continuity&lt;/strong&gt; - Keep core functionality working while fixing problems&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Separate Code from Feature Release
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Independent timelines&lt;/strong&gt; - Development and business release schedules can differ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing coordination&lt;/strong&gt; - Align feature releases with marketing campaigns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stakeholder management&lt;/strong&gt; - Give business teams control over when features go live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feature flags represent a powerful paradigm shift in how we think about software deployment and release management, enabling more flexible, safer, and data-driven development practices.&lt;/p&gt;


&lt;h2&gt;
  
  
  PIE : The next Big Thing (Alexandre Daubois)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Extensions ?
&lt;/h3&gt;

&lt;p&gt;Extensions are like composer packages, but written in C, C++, Rust, and now Go.&lt;br&gt;&lt;br&gt;
They live at a lower level, which makes them much faster than pure PHP code.&lt;/p&gt;

&lt;p&gt;Frameworks like &lt;strong&gt;Phalcon&lt;/strong&gt; are themselves shipped as extensions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Installing a third-party lib
&lt;/h3&gt;

&lt;p&gt;Traditionally, installing an extension is much more painful than a &lt;code&gt;composer install&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
It usually involves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloading the source code.&lt;/li&gt;
&lt;li&gt;Compiling it with &lt;code&gt;phpize&lt;/code&gt; and &lt;code&gt;make&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Adding a line to &lt;code&gt;php.ini&lt;/code&gt; to enable it.&lt;/li&gt;
&lt;li&gt;Restarting PHP-FPM or Apache to load it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This workflow makes extensions harder to distribute and standardize compared to Composer packages.&lt;/p&gt;
&lt;h3&gt;
  
  
  PECL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Clunky and outdated.&lt;/li&gt;
&lt;li&gt;Slow to install.&lt;/li&gt;
&lt;li&gt;Lacks proper security (no package signing).&lt;/li&gt;
&lt;li&gt;Not officially backed by PHP, and some in the community want to phase it out.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  docker-php-extension-installer
&lt;/h3&gt;

&lt;p&gt;A widely used community project that simplifies extension installation inside Docker images.&lt;br&gt;&lt;br&gt;
Instead of writing complex &lt;code&gt;apt-get&lt;/code&gt; + &lt;code&gt;phpize&lt;/code&gt; + &lt;code&gt;make&lt;/code&gt; commands, you just add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;install-php-extensions xdebug redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is great, but still not perfect—it remains Docker-specific and doesn’t integrate with Composer or Packagist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project to replace PECL
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;pie-design&lt;/strong&gt; repository defines the foundations of &lt;strong&gt;PIE&lt;/strong&gt;, a new way to install extensions as easily as PHP packages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Started in March 2024.&lt;/li&gt;
&lt;li&gt;Version 1 released in June 2025.&lt;/li&gt;
&lt;li&gt;PIE is distributed as a single &lt;code&gt;phar&lt;/code&gt; file: just download it and use it.&lt;/li&gt;
&lt;li&gt;All extension metadata is stored in Packagist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Command options
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pie install ext-xdebug&lt;/code&gt; → installs an extension and updates &lt;code&gt;php.ini&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pie uninstall ext-redis&lt;/code&gt; → removes an extension.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pie update&lt;/code&gt; → upgrades to the latest available version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pie search redis&lt;/code&gt; → searches for extensions in Packagist.&lt;/li&gt;
&lt;li&gt;Running &lt;code&gt;pie&lt;/code&gt; without arguments reads extensions from &lt;code&gt;composer.json&lt;/code&gt; and installs them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add repositories via Composer, VCS, or local paths.&lt;/li&gt;
&lt;li&gt;Automatic &lt;code&gt;php.ini&lt;/code&gt; update.&lt;/li&gt;
&lt;li&gt;Support for &lt;code&gt;GH_TOKEN&lt;/code&gt; to install from private repositories.&lt;/li&gt;
&lt;li&gt;OS compatibility restrictions.&lt;/li&gt;
&lt;li&gt;Symfony CLI integration: &lt;code&gt;symfony pie install&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The future of extensions
&lt;/h3&gt;

&lt;p&gt;PIE is the theoretical replacement for PECL.&lt;br&gt;&lt;br&gt;
An RFC vote was held, closing on &lt;strong&gt;September 20, 2025&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Almost everyone voted &lt;em&gt;yes&lt;/em&gt;, which means PIE is now the official successor to PECL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Make your devs happy by normalizing your API errors (Clément Herreman)
&lt;/h2&gt;

&lt;p&gt;Errors are not just bugs. They’re an opportunity to give users autonomy through clear feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an error?
&lt;/h3&gt;

&lt;p&gt;An error is any behavior—intentional or not—that prevents the user from completing their task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why normalize errors?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;To react properly to a precise issue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrying a token.&lt;/li&gt;
&lt;li&gt;Handling distributed system failures.&lt;/li&gt;
&lt;li&gt;Fixing configuration issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To present errors consistently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear and understandable messages for end-users.&lt;/li&gt;
&lt;li&gt;Precise identification to ease support.&lt;/li&gt;
&lt;li&gt;Keeping some details vague for security reasons.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How?
&lt;/h3&gt;

&lt;p&gt;Errors can be classified into three categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Errors that belong to your domain: you own them, so enrich them with context.&lt;/li&gt;
&lt;li&gt;Errors that don’t belong to your domain but still happen: wrap them with a code and enrich them.&lt;/li&gt;
&lt;li&gt;Rare/unexpected errors: keep the default JSON output.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  RFC 7807: Problem Details for HTTP APIs
&lt;/h4&gt;

&lt;p&gt;This RFC defines a standard JSON structure for errors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt;: unique machine-readable code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt;: short, human-readable summary.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;detail&lt;/code&gt;: contextual explanation of this particular error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;instance&lt;/code&gt;: URL to the error catalog.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;...&lt;/code&gt;: any custom fields you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Example HTTP response
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/problem+json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/errors/authentication_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authentication failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your token has expired. Please request a new one."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/login"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  API Platform
&lt;/h4&gt;

&lt;p&gt;API Platform provides a ready-to-use &lt;code&gt;ApiPlatform\Problem\Error&lt;/code&gt; class to implement RFC 7807.&lt;/p&gt;

&lt;h4&gt;
  
  
  Organizing errors
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Keep only business exceptions in the domain layer.&lt;/li&gt;
&lt;li&gt;Wrap infrastructure errors before sending them to the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Documenting errors
&lt;/h4&gt;

&lt;p&gt;Errors can be declared as attributes on operations, making them explicit in the API docs.&lt;/p&gt;

&lt;h4&gt;
  
  
  Improvements: RFC 9457
&lt;/h4&gt;

&lt;p&gt;RFC 9457 is essentially the same as RFC 7807, with some additions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A registry of errors via &lt;code&gt;schema.org&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A mechanism for returning multiple errors at once (though strongly discouraged).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As Clément highlighted: RFC 9457 doesn’t bring much practical value, and some of its suggestions are even discouraged in the spec.&lt;/p&gt;




&lt;h2&gt;
  
  
  Symfony and Dependency Injection: From past to future (Imen Ezzine)
&lt;/h2&gt;

&lt;p&gt;Dependency Injection (DI) is the “D” in SOLID, and it has been a cornerstone of Symfony’s design for nearly two decades.&lt;br&gt;&lt;br&gt;
This talk explored its history, evolution, and what’s next.&lt;/p&gt;

&lt;h3&gt;
  
  
  The early days
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;2007 – Symfony 1&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Services instantiated directly, often via &lt;code&gt;sfContext()&lt;/code&gt; (a singleton).&lt;/li&gt;
&lt;li&gt;Hard to test, rigid, tightly coupled.&lt;/li&gt;
&lt;li&gt;No real container.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Symfony 2 and the paradigm shift
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;2011 – Symfony 2&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduction of a central container.&lt;/li&gt;
&lt;li&gt;Services configured via YAML and parameters.&lt;/li&gt;
&lt;li&gt;Dependencies injected as constructor arguments.&lt;/li&gt;
&lt;li&gt;Autowiring introduced in &lt;strong&gt;Symfony 2.8&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2015 – API Platform v1&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavy reliance on autowiring (then experimental).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2016 – API Platform v2&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@ApiResource&lt;/code&gt; annotation magic powered by the DI component.&lt;/li&gt;
&lt;li&gt;Data persisters and providers had to be tagged manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2017 – Symfony 3.3 / API Platform 2.2&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Autowiring + autoconfigure.&lt;/li&gt;
&lt;li&gt;Manual tagging mostly eliminated (providers/persisters automatically wired).&lt;/li&gt;
&lt;li&gt;Symfony 3.4: services private by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Symfony 5 to Symfony 7
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;2021 – Symfony 5.3&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DI powered by attributes → much less YAML.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#[When]&lt;/code&gt; attribute for conditional services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Symfony 6.0 – 6.3&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New attributes for corner cases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#[Autowire]&lt;/code&gt; attribute for precise service injection.&lt;/li&gt;
&lt;li&gt;Support for env vars and parameters via attributes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#[AsAlias]&lt;/code&gt; to alias services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2022 – API Platform 3.0&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New state processors and providers replace older persister/provider pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2023 – Symfony 7&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#[AutoconfigureTag]&lt;/code&gt; → automatic tagging (used in API Platform filters).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TaggedIterator&lt;/code&gt; → inject multiple tagged services.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AutowireIterator&lt;/code&gt; → autowire all classes implementing an interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Symfony 7.1 – 7.3&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#[AutowireMethodOf]&lt;/code&gt; to autowire a single method.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#[WhenNot]&lt;/code&gt; for conditional services.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;when&lt;/code&gt; parameter in &lt;code&gt;#[AsAlias]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Takeaways
&lt;/h3&gt;

&lt;p&gt;Over 20 years, DI in Symfony evolved from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual instantiation →&lt;/li&gt;
&lt;li&gt;Manual configuration →&lt;/li&gt;
&lt;li&gt;Automatic configuration through &lt;strong&gt;attributes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This journey has made Symfony projects &lt;strong&gt;more testable, maintainable, and developer-friendly&lt;/strong&gt; while reducing boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Cover image by &lt;a href="https://ncls.tv/" rel="noopener noreferrer"&gt;Nicolas Detrez&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>techtalks</category>
      <category>api</category>
      <category>symfony</category>
    </item>
    <item>
      <title>API Platform Con 2025 - Day 1</title>
      <dc:creator>Thérage Kévin</dc:creator>
      <pubDate>Wed, 24 Sep 2025 07:52:30 +0000</pubDate>
      <link>https://dev.to/sensiolabs/api-platform-con-2025-day-1-1gpo</link>
      <guid>https://dev.to/sensiolabs/api-platform-con-2025-day-1-1gpo</guid>
      <description>&lt;p&gt;I had the opportunity to attend the API Platform Con 2025 thanks to SensioLabs and here is what I learned through the talks I viewed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enhance your API Platform APIs with Go thanks to FrankenPHP (Kévin Dunglas)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slides of this talk are available : &lt;a href="https://dunglas.dev/2025/09/the-best-of-both-worlds-go-powered-grpc-for-your-php-and-api-platform-apps/" rel="noopener noreferrer"&gt;https://dunglas.dev/2025/09/the-best-of-both-worlds-go-powered-grpc-for-your-php-and-api-platform-apps/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API Platform is celebrating its 10th anniversary this year, having been created on January 20, 2015. Originally a Symfony bundle, it is now usable with Laravel or even without any framework. With over 14,000 stars on GitHub and 921 code and documentation contributors, API Platform has become an essential tool for creating APIs.&lt;/p&gt;

&lt;p&gt;Kevin highlighted that it has also been the starting point for many related projects such as Mercure and FrankenPHP, and many Symfony components were first developed for API Platform.&lt;/p&gt;

&lt;p&gt;He also paid tribute to Ryan Weaver, a key contributor, and encouraged attendees to support his family through the &lt;a href="https://gofund.me/31ec53011" rel="noopener noreferrer"&gt;GoFundMe "In memory of Ryan Weaver: For his son Beckett"&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Model, Many API Architecture Types
&lt;/h3&gt;

&lt;p&gt;With API Platform, you can use the same DTO, the same code, and the same PHP class to generate different output formats with just a few configuration changes.&lt;/p&gt;

&lt;p&gt;Here are some of the supported formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hydra&lt;/li&gt;
&lt;li&gt;OpenAPI&lt;/li&gt;
&lt;li&gt;HAL&lt;/li&gt;
&lt;li&gt;JSON:API&lt;/li&gt;
&lt;li&gt;GraphQL&lt;/li&gt;
&lt;li&gt;Mercure (SSE support)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach eliminates code duplication across different API format requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why gRPC is missing in API Platform
&lt;/h3&gt;

&lt;p&gt;Currently, gRPC is not supported by API Platform. Here's why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gRPC does not follow REST principles.&lt;/li&gt;
&lt;li&gt;It is different from GraphQL.&lt;/li&gt;
&lt;li&gt;It uses Protobuf (a binary format) instead of JSON for the output format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the time, in classic gRPC architecture, PHP is not a candidate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7aa574oibcb89hyrtjmm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7aa574oibcb89hyrtjmm.jpg" title="Schema of Typical gRPC Architecture which does not include a PHP side gRPC server but a C++ server, android/java client and a ruby client" alt="Schema of Typical gRPC Architecture which does not include a PHP side gRPC server but a C++ server, android/java client and a ruby client" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, gRPC has several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast and efficient&lt;/li&gt;
&lt;li&gt;Strongly typed&lt;/li&gt;
&lt;li&gt;Language agnostic: a code generator allows generating data structures in many languages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use cases for gRPC include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microservices&lt;/li&gt;
&lt;li&gt;Internet of Things (IoT)&lt;/li&gt;
&lt;li&gt;Critical components where performance is essential&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How gRPC Works
&lt;/h3&gt;

&lt;p&gt;gRPC operates on HTTP/2 and uses Protocol Buffer &lt;code&gt;.proto&lt;/code&gt; files to define service contracts. These definitions enable automatic code generation across multiple programming languages. The binary serialization format provides more efficient data transmission than JSON, while HTTP/2's multiplexing supports high-performance communication.&lt;/p&gt;

&lt;p&gt;Moreover, the official gRPC documentation recommends using non-PHP languages for gRPC servers due to PHP-FPM's request lifecycle limitations.&lt;/p&gt;

&lt;h3&gt;
  
  
  gRPC with FrankenPHP
&lt;/h3&gt;

&lt;p&gt;Thankfully, FrankenPHP offers a way to write extensions in Go that can be exposed in PHP, making it possible to use gRPC with API Platform. The FrankenPHP gRPC extension is available on GitHub and is testable. It uses the Go gRPC server and is designed to be used with or without API Platform.&lt;/p&gt;

&lt;p&gt;For more information on configuration and usage of this extension, please refer to the &lt;a href="https://github.com/dunglas/frankenphp-grpc" rel="noopener noreferrer"&gt;GitHub repository documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Testing can be done with gRPCui. It is important to note that this solution is still experimental.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extend Caddy Web Server with Your Favorite Language (Sylvain Combraque)
&lt;/h2&gt;

&lt;p&gt;Caddy is a modern, fast, and easy-to-use web server that simplifies the process of serving websites and web applications. It is known for its automatic HTTPS configuration and simple syntax. Matt Holt, the creator of Caddy, has made significant contributions to the web server landscape with Caddy's unique features and ease of use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extending Caddy
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Using xcaddy Build
&lt;/h4&gt;

&lt;p&gt;Extending Caddy can be done using the &lt;code&gt;xcaddy&lt;/code&gt; build tool, which allows you to customize and extend Caddy with plugins written in Go. This tool provides a straightforward way to add new functionalities to Caddy.&lt;/p&gt;

&lt;h4&gt;
  
  
  Using WebUI from Caddy Website
&lt;/h4&gt;

&lt;p&gt;Caddy also offers a WebUI that can be accessed from the Caddy website. This interface provides an easy way to manage and configure your Caddy web server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extending Caddy with Go
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flz8xh7oa7mvftlsldnjl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flz8xh7oa7mvftlsldnjl.jpg" title="An example of a Caddy extension made with GO" alt="An example of a Caddy extension made with GO" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For more detailed information on how to extend Caddy with Go, you can refer to the &lt;a href="https://caddyserver.com/docs/extending-caddy" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Interpreter
&lt;/h3&gt;

&lt;p&gt;While using interpreters to extend Caddy has its advantages, there are also some drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need one interpreter per language.&lt;/li&gt;
&lt;li&gt;New versions of the language require new interpreters.&lt;/li&gt;
&lt;li&gt;Each interpreter is maintained separately.&lt;/li&gt;
&lt;li&gt;You may need to re-implement types.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WASM x WASI x darkweak/wazemmes for Caddy Extension in Any Language
&lt;/h3&gt;

&lt;p&gt;WebAssembly (WASM) is a binary instruction format that promises to enable programs to run at near-native speed on the web. The promise of "build once, run everywhere" makes WASM an attractive option for extending Caddy. However, the current documentation is not user-friendly, and there are some bugs to be aware of.&lt;/p&gt;

&lt;p&gt;WebAssembly System Interface (WASI) is a system interface designed to allow WebAssembly modules to interact with the operating system in a secure and portable way. This combination of WASM and WASI allows developers to write code in their preferred language and compile it to WASM for execution in a browser or server environment.&lt;/p&gt;

&lt;p&gt;For more information on using WASM and WASI in Caddy, you can check out the &lt;a href="https://github.com/darkweak/wazemmes" rel="noopener noreferrer"&gt;darkweak/wazemmes repository on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mercure, SSE, API Platform and an LLM Elevate a Chat(bot) (Mathieu Santostefano)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slides of this talk are available : &lt;a href="https://welcomattic.github.io/slides-real-time-ai-chatbot-with-mercure/1" rel="noopener noreferrer"&gt;https://welcomattic.github.io/slides-real-time-ai-chatbot-with-mercure/1&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Origin of the Subject
&lt;/h3&gt;

&lt;p&gt;The initial customer need was to create paid expert chat exchanges. The first version used an API + React, but lacked message history. Mercure was chosen for secure message distribution to customers via JWT.&lt;/p&gt;

&lt;p&gt;Evolved needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assist experts with an AI assistant&lt;/li&gt;
&lt;li&gt;Allow AI to handle the first part of the conversation&lt;/li&gt;
&lt;li&gt;Allow experts to take over when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Toolbox
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Mercure - Real-time Exchanges
&lt;/h4&gt;

&lt;p&gt;Architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server =&amp;gt; Hub =&amp;gt; Client&lt;/li&gt;
&lt;li&gt;Client =&amp;gt; Hub =&amp;gt; Client&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  SSE
&lt;/h4&gt;

&lt;p&gt;Server-Sent Events (SSE) is a technology that allows a server to send real-time updates to a client via a persistent HTTP connection. The client listens to a stream of events sent by the server.&lt;/p&gt;

&lt;h4&gt;
  
  
  API Platform
&lt;/h4&gt;

&lt;h5&gt;
  
  
  LLM
&lt;/h5&gt;

&lt;p&gt;Data generation and intelligent responses&lt;/p&gt;

&lt;h5&gt;
  
  
  Symfony Messenger
&lt;/h5&gt;

&lt;p&gt;Asynchronous process management&lt;/p&gt;

&lt;h5&gt;
  
  
  Symfony AI
&lt;/h5&gt;

&lt;p&gt;Equivalent to Mailer/Notifier but for AI providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform&lt;/strong&gt;: unified interface for all AI providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt;: agentic AI creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt;: data storage abstraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP SDK&lt;/strong&gt;: now officially supported by Anthropic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Bundle&lt;/strong&gt;: full integration with Symfony&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Bundle&lt;/strong&gt;: additional components&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;Technical flow:&lt;br&gt;
&lt;code&gt;User =&amp;gt; message =&amp;gt; Mercure (Storage) &amp;lt;= Symfony SSE client =&amp;gt; Symfony Messenger =&amp;gt; Mistral =&amp;gt; Mercure =&amp;gt; AI response =&amp;gt; User&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Secure private chat via JWT (JSON Web Tokens) - an open standard for securely exchanging data between parties.&lt;/p&gt;
&lt;h4&gt;
  
  
  Sending a message to Mercure
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hubURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Send JWT cookie&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MercureUpdateData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;private&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// restrict message to subscribed clients&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Connecting to Mercure
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sse-endpoint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New event received: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Symfony SSE Client
&lt;/h4&gt;

&lt;p&gt;Built-in EventSourceHttpClient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\Chunk\ServerSentEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\EventSourceHttpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\HttpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$eventSourceClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSourceHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nv"&gt;$connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$eventSourceClient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"YOUR-MERCURE-URL"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eventSourceClient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isTimeout&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Keep the connection alive.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isLast&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Connection closed by server.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;ServerSentEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;processSSE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Dispatching messages with Messenger
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\Chunk\ServerSentEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;processSSE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ServerSentEvent&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getArrayData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// do some checks before asking LLM&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;shouldProcessWithAi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Dispacth message to ask LLM&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;messageBus&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessAiResponseMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'conversationId'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;sseMessageId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timestamp'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Symfony AI Configuration
&lt;/h4&gt;

&lt;h5&gt;
  
  
  YAML configuration of AI bundle with Mistral
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;mistral&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%env(MISTRAL_API_KEY)%'&lt;/span&gt;

    &lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;symfony_ai.platform.mistral'&lt;/span&gt;
            &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Symfony\AI\Platform\Bridge\Mistral\Mistral'&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!php/const&lt;/span&gt; &lt;span class="s"&gt;Symfony\AI\Platform\Bridge\Mistral\Mistral::MISTRAL_LARGE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Handler Example
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Symfony AI Bundle code example&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\AI\Agent\AgentInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\AI\Agent\Chat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\AI\Platform\Message\Message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\AI\Platform\Message\MessageBag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\AI\Store\StoreInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessageHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;AgentInterface&lt;/span&gt; &lt;span class="nv"&gt;$agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;StoreInterface&lt;/span&gt; &lt;span class="nv"&gt;$messageStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ProcessAiResponseMessage&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;messageStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UNIQUE_ID_TO_PROMPT"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;messageStore&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"UNIQUE_ID_TO_PROMPT"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$messages&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// retrieve system prompt from somewhere ...&lt;/span&gt;

            &lt;span class="c1"&gt;// Programmatic System prompt injection&lt;/span&gt;
            &lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;initiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MessageBag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forSystem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SYSTEM_PROMPT_INJECTION"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$llmAnswer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$chat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;ofUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// do something with the answer&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In his final words Mathieu dedicated his talk in memory of Ryan Weaver, whose contributions continue to inspire the Symfony community. He also thanked Christopher Hertel for the Symfony AI initiative.&lt;/p&gt;




&lt;h2&gt;
  
  
  How API Platform 4.2 is Redefining API Development (Antoine Bluchet)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slides of this talk are available : &lt;a href="https://soyuka.me/api-platform-4-2-redefining-api-development/" rel="noopener noreferrer"&gt;https://soyuka.me/api-platform-4-2-redefining-api-development/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Looking back at version 4.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;610 commits&lt;/li&gt;
&lt;li&gt;~200,000 lines of code&lt;/li&gt;
&lt;li&gt;291 issues opened&lt;/li&gt;
&lt;li&gt;230 issues closed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's New in 4.2
&lt;/h3&gt;

&lt;p&gt;Key features of this release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FrankenPHP integration&lt;/li&gt;
&lt;li&gt;State Options&lt;/li&gt;
&lt;li&gt;Query parameters enhancements&lt;/li&gt;
&lt;li&gt;Performance improvements&lt;/li&gt;
&lt;li&gt;Laravel compatibility&lt;/li&gt;
&lt;li&gt;PHP File Metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Metadata Enhancements
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Metadata from PHP Files
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1z9cw9en3mn20iwvvpe.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1z9cw9en3mn20iwvvpe.jpg" title="An example of a PHP file metadata" alt="An example of a PHP file metadata" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;New metadata system allows extracting API configuration directly from PHP files. It is not documented yet (AFAIK) but you can see the related PR of Loïc Frémont &lt;a href="https://github.com/api-platform/core/pull/7017" rel="noopener noreferrer"&gt;https://github.com/api-platform/core/pull/7017&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Metadata Mutator
&lt;/h4&gt;

&lt;p&gt;A new way to programmatically modify metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More flexible configuration&lt;/li&gt;
&lt;li&gt;Runtime adjustments&lt;/li&gt;
&lt;li&gt;Cleaner architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  From API Filter to Parameters
&lt;/h3&gt;

&lt;h4&gt;
  
  
  API Filter Retrospective
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;#[ApiFilter]&lt;/code&gt; attribute was doing a lot of things in the background, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declare services with filter tags&lt;/li&gt;
&lt;li&gt;Generate documentation&lt;/li&gt;
&lt;li&gt;Apply database operations&lt;/li&gt;
&lt;li&gt;Work with multiple properties&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was confusing and also not respecting Single Responsibility Principle. That's the reason why API Platform maintainers have decided to rework that to Parameters.&lt;/p&gt;

&lt;h4&gt;
  
  
  Filter Documentation Improvements
&lt;/h4&gt;

&lt;p&gt;Now documentations are generated separately. This can be done with two new interfaces&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JsonSchemaFilterInterface&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OpenApiParameterFilter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Filtering System
&lt;/h4&gt;

&lt;p&gt;Now Filter are independent through a new &lt;code&gt;FilterInterface&lt;/code&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simplified &lt;code&gt;apply()&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;No constructor requirements&lt;/li&gt;
&lt;li&gt;Dependency-free design&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Parameter System
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;#[ApiFilter]&lt;/code&gt; attribute will leave his place to a new property of Operations attributes called &lt;code&gt;parameters&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F733nj3gdwsl82qpcgqvs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F733nj3gdwsl82qpcgqvs.jpg" title="An example of Parameters usage" alt="An example of Parameters usage" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  New Filter Types
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Free text search capabilities&lt;/li&gt;
&lt;li&gt;URI variable provider&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  JSON Schema Enhancements
&lt;/h3&gt;

&lt;p&gt;Some improvements were made on JSON Schema generation. Those changes could imply a backward compatibility break for tools using the former JSON Schema.&lt;/p&gt;

&lt;p&gt;Improvements&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema mutualization&lt;/li&gt;
&lt;li&gt;30% smaller OpenAPI specification files&lt;/li&gt;
&lt;li&gt;Reduced I/O operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A new tool is now recommended : &lt;a href="https://pb33f.io" rel="noopener noreferrer"&gt;pb33f.io&lt;/a&gt; as it is more feature-rich and better maintained than Swagger UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;Performance benchmarks comparing Nginx vs FrankenPHP:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca88dv38n1sjiuql8sfq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca88dv38n1sjiuql8sfq.jpg" title="Performance comparison between Nginx and FrankenPHP 1" alt="Performance comparison between Nginx and FrankenPHP 1" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nmnf07glk5uz0z4qt0o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nmnf07glk5uz0z4qt0o.jpg" title="Performance comparison between Nginx and FrankenPHP 2" alt="Performance comparison between Nginx and FrankenPHP 2" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More benchmarks available at &lt;a href="https://soyuka.github.io/sylius-benchmarks/" rel="noopener noreferrer"&gt;soyuka.github.io/sylius-benchmarks/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JSON Streamer improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~32.4% better request/second performance&lt;/li&gt;
&lt;li&gt;Configurable via settings&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  State Options
&lt;/h3&gt;

&lt;p&gt;New features for querying subresources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More efficient data loading&lt;/li&gt;
&lt;li&gt;Entity class magic (RIP Ryan)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data Mapping
&lt;/h3&gt;

&lt;p&gt;New mapping capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database to API representation mapping&lt;/li&gt;
&lt;li&gt;Symfony ObjectMapper integration&lt;/li&gt;
&lt;li&gt;Better data transformation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Debugging
&lt;/h3&gt;

&lt;p&gt;Profiling tools are back!&lt;/p&gt;

&lt;h3&gt;
  
  
  Backward Compatibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Many new features added&lt;/li&gt;
&lt;li&gt;No deprecations in this version&lt;/li&gt;
&lt;li&gt;Parameters system is no longer experimental&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Looking Ahead to API Platform 5.0
&lt;/h3&gt;

&lt;p&gt;Planned changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#[ApiFilter]&lt;/code&gt; deprecation (migration script coming soon)&lt;/li&gt;
&lt;li&gt;More JSON Streamer usage&lt;/li&gt;
&lt;li&gt;Object Mapper feature requests&lt;/li&gt;
&lt;li&gt;Community-driven improvements&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Design pattern the treasure is in the vendor (Smaïne Milianni)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slides of this talk are available : &lt;a href="https://ismail1432.github.io/conferences/2025/apip_con/index.html" rel="noopener noreferrer"&gt;https://ismail1432.github.io/conferences/2025/apip_con/index.html&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this talk Smaïne made a tour on Design Pattern that are commonly used without any knowledge that they are in the vendors we use on a daily basis. He also showcased small and comprehensible code PHP snippets explaining some of them.&lt;/p&gt;

&lt;p&gt;Among them were :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Strategy Pattern&lt;/li&gt;
&lt;li&gt;The Adapter Pattern&lt;/li&gt;
&lt;li&gt;The Factory Pattern&lt;/li&gt;
&lt;li&gt;The Builder Pattern&lt;/li&gt;
&lt;li&gt;The Proxy Pattern&lt;/li&gt;
&lt;li&gt;The Observer Pattern&lt;/li&gt;
&lt;li&gt;The Event Dispatcher Pattern&lt;/li&gt;
&lt;li&gt;The Decorator Pattern&lt;/li&gt;
&lt;li&gt;The Facade Pattern&lt;/li&gt;
&lt;li&gt;The Template Pattern&lt;/li&gt;
&lt;li&gt;The Chain of Responsibility Pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Smaïne also ended with a shoutout to Ryan Weaver.&lt;/p&gt;




&lt;h2&gt;
  
  
  What if we do Event Storming in our API Platform projects ? (Gregory Planchat)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Event Storming
&lt;/h3&gt;

&lt;p&gt;Event Storming is a collaborative workshop technique that brings together both users and developers in the same room. This methodology shines a light on misunderstandings that often exist between business stakeholders and technical teams.&lt;/p&gt;

&lt;p&gt;The beauty of Event Storming lies in its simplicity: it uses physical post-it notes to encourage different team members to exchange ideas, see each other, share knowledge, meet face-to-face, and confront their understanding of the business domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparation
&lt;/h3&gt;

&lt;p&gt;The Event Storming process follows a structured approach with several key steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List the events&lt;/strong&gt; - Start by identifying all the significant events that happen in your business domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organize the events&lt;/strong&gt; - Arrange these events in a chronological or logical order&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up the commands&lt;/strong&gt; - Identify what actions trigger each event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up the actors&lt;/strong&gt; - Determine who or what initiates each command&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Green post-its&lt;/strong&gt; - Add the data necessary for users to make decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add external systems&lt;/strong&gt; - Include third-party systems that interact with your domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregates&lt;/strong&gt; - Group related events and commands into cohesive business concepts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The team mentioned that they conducted multiple sessions "until no one had any more questions, whether from the technical team or the business team." It's a self-documenting process that can be repeated as the business evolves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;p&gt;Event Storming brings several concrete benefits to development teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Process documentation&lt;/strong&gt; - The workshop naturally creates living documentation of your business processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilitated onboarding&lt;/strong&gt; - New team members can quickly understand the domain by looking at the Event Storming artifacts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reveals uncertainties&lt;/strong&gt; - Hidden assumptions and unclear requirements surface during the collaborative sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  With API Platform
&lt;/h3&gt;

&lt;h4&gt;
  
  
  The Anemic Model Problem
&lt;/h4&gt;

&lt;p&gt;Most applications suffer from what's called the anemic model anti-pattern, where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic is scattered across numerous services&lt;/li&gt;
&lt;li&gt;Loss of user intention tracking&lt;/li&gt;
&lt;li&gt;Entities become mere data containers with getters and setters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typically, when you want to modify information in your entity, you call a setter method. This often happens across multiple services and classes, hence the "business logic disseminated in numerous services" problem.&lt;/p&gt;

&lt;p&gt;The intention is essential for third-party systems to understand what actually happened in your application.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rich Models
&lt;/h4&gt;

&lt;p&gt;The alternative approach uses rich domain models that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Require significant cost&lt;/strong&gt; - More complex to implement initially&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require detailed application understanding&lt;/strong&gt; - Team needs deep domain knowledge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guarantee consistency over time&lt;/strong&gt; - Business rules are enforced at the model level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply business constraints&lt;/strong&gt; - Validation logic lives where it belongs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralize business logic&lt;/strong&gt; - Everything related to a concept lives in one or two classes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model guarantees integrity&lt;/strong&gt; - Invalid states become impossible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a rich model, all changes happen within the entity itself, keeping the business logic centralized and coherent.&lt;/p&gt;

&lt;h4&gt;
  
  
  The CRUD Problem
&lt;/h4&gt;

&lt;p&gt;Traditional CRUD operations are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limited to 4 operations (Create, Read, Update, Delete)&lt;/li&gt;
&lt;li&gt;SQL-centric thinking&lt;/li&gt;
&lt;li&gt;Tools like PostgREST generate REST APIs automatically but provide little business value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To do better, we can leverage the power of API Platform's State Providers and State Processors.&lt;/p&gt;

&lt;p&gt;But how do we preserve intention in our application?&lt;/p&gt;

&lt;p&gt;The solution follows this flow:&lt;br&gt;
&lt;strong&gt;Repository → EventBus → Event → Handler&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Model Modification
&lt;/h4&gt;

&lt;p&gt;The team implemented a pattern with three key methods in their entities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;recordThat()&lt;/strong&gt; - Records that an event occurred (e.g., "a deployment was launched")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;apply()&lt;/strong&gt; - Applies the modifications related to the event (e.g., updates the deployment date)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;releaseEvents()&lt;/strong&gt; - A cleanup step that happens during the save process, just before persist/flush, then dispatches events throughout the application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach ensures that every business action is captured as a meaningful event, preserving the user's intention and providing a clear audit trail of what happened in the system.&lt;/p&gt;
&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;The team reported several concrete improvements after implementing this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An API and codebase that better resembled the company's business domain&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User intention was preserved&lt;/strong&gt; throughout the application lifecycle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better understanding of actions performed&lt;/strong&gt; in the application, both for developers and business stakeholders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5c6ciqape365oj0bo5vv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5c6ciqape365oj0bo5vv.jpg" title="Abstract of the OpenAPI documentation of an Event Stormed done API" alt="Abstract of the OpenAPI documentation of an Event Stormed done API" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Scaling Databases (Tobias Petry)
&lt;/h2&gt;

&lt;p&gt;Tobias Petry shared insights on database scaling strategies. His talk highlighted why scalability issues usually originate at the database level and walked through the most common solutions, their advantages, and their pitfalls.&lt;/p&gt;
&lt;h3&gt;
  
  
  Solutions
&lt;/h3&gt;

&lt;p&gt;There is no silver bullet: every application has its own constraints. Still, several well-known strategies exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Find and fix slow queries&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Before considering infrastructure, always check the basics. Tools like &lt;a href="https://mysqlexplain.com" rel="noopener noreferrer"&gt;mysqlexplain.com&lt;/a&gt; can help detect inefficient queries and suggest improvements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache results&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Serving cached responses drastically reduces the load on the database and avoids repeating costly operations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vertical scaling (bigger machines)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Sometimes the simplest option is to scale up: move the database to a more powerful server. However, this approach quickly reaches physical and financial limits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-master replication&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In this setup, several servers accept both reads and writes. It improves write scalability but creates the risk of conflicts when parallel writes occur. Conflict resolution strategies can mitigate this, but complexity grows with the number of nodes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Read replication&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Here, a single primary node handles writes, while replicas serve read queries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous replication&lt;/strong&gt; ensures that changes are propagated to all replicas before acknowledging the write. This guarantees consistency but adds latency, as every replica must confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous replication&lt;/strong&gt; acknowledges the write immediately and updates replicas later. It reduces latency but risks temporary inconsistency between nodes.
In practice, most applications tolerate eventual consistency. A cache layer in front of the primary often hides replication lag. Still, studies show that 90–98% of applications encounter latency issues if relying only on replicas for reads.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sharding&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Sharding distributes data across multiple databases. This enables &lt;em&gt;theoretically infinite scalability&lt;/em&gt;. For example, users might be split across shards based on their ID.&lt;br&gt;&lt;br&gt;
The challenge comes with cross-shard queries: if you need to fetch all orders of a user across multiple shops, and users and shops are sharded differently, you must query several shards and aggregate results manually. Some companies even introduce &lt;em&gt;shards of shards&lt;/em&gt;, adding another layer of complexity.&lt;br&gt;&lt;br&gt;
Because of this overhead, sharding is usually reserved for very large-scale systems. For most use cases, read replication is sufficient.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In general, these strategies are designed for CRUD workloads. Analytical queries (dashboards, reports) are harder to scale with a standard relational database. Developers can explore resources like &lt;a href="https://sqlfordevs.com" rel="noopener noreferrer"&gt;sqlfordevs.com&lt;/a&gt; (free course on making analytics faster) or specialized systems such as &lt;a href="https://www.timescale.com" rel="noopener noreferrer"&gt;TimescaleDB&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Sounds complicated
&lt;/h3&gt;

&lt;p&gt;Tobias emphasized a crucial point: scaling decisions must be made before hitting database bottlenecks. Once data is structured and scaling strategies are in place, rolling back becomes almost impossible. Database architecture is one of those areas where it is far easier to make the right decision early than to correct mistakes later.&lt;/p&gt;


&lt;h2&gt;
  
  
  API Platform, JsonStreamer and ESA for skyrocketing API (Mathias Arlaud)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slides of this talk are available : &lt;a href="https://www.canva.com/design/DAGyYPxkygw/M1RzOiv8_cMp0Pa7Mh0u4g/view" rel="noopener noreferrer"&gt;https://www.canva.com/design/DAGyYPxkygw/M1RzOiv8_cMp0Pa7Mh0u4g/view&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Storytelling: imagine a bookstore. A customer orders &lt;strong&gt;all&lt;/strong&gt; Symfony-related books. The bookseller tries to gather them all, but it's heavy—takes time, lots of books. The second time, the same request, but the pile is so large that the bookseller collapses under the weight.&lt;/p&gt;

&lt;p&gt;In the API world, &lt;strong&gt;JSON is king&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At the heart of our stack is &lt;strong&gt;API Platform&lt;/strong&gt;, which relies on Symfony’s Serializer. But sometimes the Serializer is like that bookseller: it works well until the load becomes too heavy.&lt;/p&gt;
&lt;h3&gt;
  
  
  Serialization / Normalization in Symfony
&lt;/h3&gt;

&lt;p&gt;Serialization in Symfony (and in API Platform) involves turning PHP objects into arrays or scalar values, then encoding to formats like JSON or XML. &lt;strong&gt;Normalization&lt;/strong&gt; transforms the internal object graph into a neutral data structure (arrays, scalars), applying metadata such as groups or attributes. &lt;strong&gt;Encoding&lt;/strong&gt; then converts that structure into the final JSON string. The reverse process (&lt;strong&gt;denormalization&lt;/strong&gt;) handles input JSON → arrays → objects.&lt;/p&gt;

&lt;p&gt;When objects or collections are small, this works fine. But with thousands of items, large graphs, deep associations, and nested arrays, memory usage and time-to-first-byte degrade. Serialization becomes a bottleneck.&lt;/p&gt;
&lt;h3&gt;
  
  
  Streaming as a solution
&lt;/h3&gt;

&lt;p&gt;Instead of building a huge in-memory structure, streaming emits JSON pieces &lt;strong&gt;incrementally&lt;/strong&gt;. You only keep in memory what’s necessary at each moment.&lt;/p&gt;

&lt;p&gt;Symfony 7.3 introduces the &lt;strong&gt;JsonStreamer&lt;/strong&gt; component for that purpose.&lt;/p&gt;

&lt;p&gt;Some key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works best with &lt;strong&gt;POPOs&lt;/strong&gt; (Plain Old PHP Objects) having public properties, without complex constructors.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;#[JsonStreamable]&lt;/code&gt; attribute can be used on classes to mark them as streamable. This also allows pre-generation of code during cache warm-up.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;TypeInfo&lt;/strong&gt; component (&lt;a href="https://symfony.com/blog/new-in-symfony-7-3-jsonstreamer-component" rel="noopener noreferrer"&gt;link&lt;/a&gt;) to describe types of collections and objects (e.g., &lt;code&gt;Type::list(Type::object(MyDto::class))&lt;/code&gt;). This helps JsonStreamer guess the shape of the output JSON without loading everything in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a code snippet from the Symfony documentation showing basic usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example class&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Dto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\JsonStreamer\Attribute\JsonStreamable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[JsonStreamable]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$age&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In controller&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\JsonStreamer\StreamWriterInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\TypeInfo\Type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpFoundation\StreamedResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;retrieveUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StreamWriterInterface&lt;/span&gt; &lt;span class="nv"&gt;$jsonStreamWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UserRepository&lt;/span&gt; &lt;span class="nv"&gt;$userRepository&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StreamedResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$userRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nv"&gt;$json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$jsonStreamWriter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benchmarks &amp;amp; comparisons&lt;/p&gt;

&lt;p&gt;For a dataset of 10,000 objects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Memory usage / footprint (rough / relative)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serializer (traditional)&lt;/td&gt;
&lt;td&gt;~ 204 ms&lt;/td&gt;
&lt;td&gt;~ 16 MB (grows with size)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JsonStreamer&lt;/td&gt;
&lt;td&gt;~ 87 ms&lt;/td&gt;
&lt;td&gt;~ 8 MB (much more constant)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Challenges with metadata, JSON-LD and how API Platform adapts&lt;/p&gt;

&lt;p&gt;API Platform adds metadata, JSON-LD contexts, property metadata, etc. That adds overhead in serialization. To integrate JsonStreamer while preserving rich metadata:&lt;/p&gt;

&lt;p&gt;They use PropertyMetadataLoader extension points to provide metadata to JsonStreamer. This lets JsonStreamer know property names, whether they're exposed, etc., without traversing the full object tree in memory.&lt;/p&gt;

&lt;p&gt;API Platform&lt;/p&gt;

&lt;p&gt;Use of ValueTransformers that can transform any value at runtime. But caution: heavy logic in transformers can degrade performance (they run per value).&lt;/p&gt;

&lt;p&gt;Symfony&lt;br&gt;
+1&lt;/p&gt;

&lt;p&gt;Use of ObjectMapper to convert entities (e.g., Doctrine objects) into POPOs (DTOs) that are suitable for streaming. This helps because entities often have lazy properties, proxies, relations etc., which complicate streaming.&lt;/p&gt;

&lt;p&gt;ESA (Edge Side APIs) pattern&lt;/p&gt;

&lt;p&gt;Edge Side APIs refers to breaking large JSON payloads into smaller, progressive calls or chunks, often delivered from the edge / CDN to improve perceived performance, especially in high latency/slow networks. In context of this talk:&lt;/p&gt;

&lt;p&gt;Instead of sending one huge JSON structure, partition or paginate so the client can start receiving some data quickly.&lt;/p&gt;

&lt;p&gt;Combine with streaming so that parts of the response start being delivered early (TTFB improves).&lt;/p&gt;

&lt;p&gt;Good user experience: user sees something quickly rather than waiting for full load.&lt;/p&gt;

&lt;p&gt;Takeaways&lt;/p&gt;

&lt;p&gt;Serializer works, but for large data sets it becomes inefficient.&lt;/p&gt;

&lt;p&gt;JsonStreamer gives significant improvements in both memory usage and time to first byte.&lt;/p&gt;

&lt;p&gt;When you have metadata layers (API Platform, JSON-LD), use the extension points provided to plug streaming without losing features.&lt;/p&gt;

&lt;p&gt;Avoid heavy computations / transformations in runtime‐hot paths (e.g., ValueTransformers).&lt;/p&gt;

&lt;p&gt;Design your API knowing these options early, because once core serialization path is deeply embedded, changing is hard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benchmarks &amp;amp; comparisons
&lt;/h3&gt;

&lt;p&gt;For a dataset of &lt;strong&gt;10,000 objects&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Memory usage / footprint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serializer (traditional)&lt;/td&gt;
&lt;td&gt;~204 ms&lt;/td&gt;
&lt;td&gt;~16 MB (grows with size)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JsonStreamer&lt;/td&gt;
&lt;td&gt;~87 ms&lt;/td&gt;
&lt;td&gt;~8 MB (much more constant)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Challenges with metadata, JSON-LD and how API Platform adapts
&lt;/h3&gt;

&lt;p&gt;API Platform adds &lt;strong&gt;metadata&lt;/strong&gt;, JSON-LD contexts, and property metadata. That overhead makes serialization heavier. To integrate JsonStreamer while keeping these features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;PropertyMetadataLoader&lt;/strong&gt; extension points to provide metadata to JsonStreamer. This tells it which properties to expose, without traversing the full object tree.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;ValueTransformers&lt;/strong&gt; to adjust values at runtime. But beware: heavy logic here will degrade performance, since transformers run for every value.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;ObjectMapper&lt;/strong&gt; to convert entities (e.g., Doctrine objects) into POPOs (DTOs) that are easier to stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ESA (Edge Side APIs) pattern
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Edge Side APIs (ESA)&lt;/strong&gt; refers to breaking large JSON payloads into smaller, progressive chunks, often delivered from the edge or a CDN to improve perceived performance, especially in high-latency or slow networks.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of sending one huge JSON structure, partition or paginate so the client starts receiving data earlier.&lt;/li&gt;
&lt;li&gt;Combine with streaming so that parts of the response arrive incrementally, improving time-to-first-byte.&lt;/li&gt;
&lt;li&gt;The user experience is better: data appears quickly instead of waiting for everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Symfony’s Serializer is fine for small to medium datasets.&lt;/li&gt;
&lt;li&gt;JsonStreamer provides &lt;strong&gt;significant improvements&lt;/strong&gt; in memory usage and TTFB.&lt;/li&gt;
&lt;li&gt;API Platform integrates it through extension points (PropertyMetadataLoader, ValueTransformers, ObjectMapper).&lt;/li&gt;
&lt;li&gt;Avoid heavy runtime transformations for best performance.&lt;/li&gt;
&lt;li&gt;Design your API with these options in mind early—serialization decisions are very difficult to change later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Cover image by &lt;a href="https://ncls.tv/" rel="noopener noreferrer"&gt;Nicolas Detrez&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>techtalks</category>
      <category>api</category>
      <category>symfony</category>
    </item>
    <item>
      <title>SymfonyLive Paris 2025 : Ce qu’il faut retenir</title>
      <dc:creator>Thérage Kévin</dc:creator>
      <pubDate>Wed, 02 Apr 2025 11:53:54 +0000</pubDate>
      <link>https://dev.to/sensiolabs/symfonylive-paris-2025-ce-quil-faut-retenir-59ia</link>
      <guid>https://dev.to/sensiolabs/symfonylive-paris-2025-ce-quil-faut-retenir-59ia</guid>
      <description>&lt;p&gt;J’ai eu la chance d’assister au &lt;strong&gt;SymfonyLive Paris 2025&lt;/strong&gt;, et voici un résumé des annonces, conférences et tendances clés à retenir !&lt;/p&gt;

&lt;p&gt;Retrouvez les slides sur ce merveilleux dépôt (Merci Romain Gautier🙏) : &lt;a href="https://github.com/SymfonyLive/paris-2025-talks" rel="noopener noreferrer"&gt;https://github.com/SymfonyLive/paris-2025-talks&lt;/a&gt;)&lt;/p&gt;




&lt;h1&gt;
  
  
  Keynote : Scaling to 0  (🎤  Fabien Potencier)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  🚀 Qu'est-ce qu'un projet Open Source ?
&lt;/h2&gt;

&lt;p&gt;Un projet open source, ce n'est pas seulement du code. Voici la hiérarchie des priorités :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Communauté&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Une communauté active et une documentation solide sont essentielles pour assurer la pérennité d’un projet OS.&lt;/p&gt;

&lt;p&gt;Avec le temps, Symfony a intégré une nouvelle priorité : l’&lt;strong&gt;expérience développeur (DX)&lt;/strong&gt;. La hiérarchie évolue donc ainsi :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Communauté&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expérience développeur (DX)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🏗️ Comment créer une application Symfony simple ?
&lt;/h2&gt;

&lt;p&gt;Suite aux débats dans la communauté Laravel sur la complexité d’installation et la multitude de dépendances requises pour démarrer un projet, Fabien s’est posé la question :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Quelle est l'application Symfony la plus simple possible ?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En réponse, Symfony propose désormais trois nouveaux points d’entrée, permettant de commencer petit et d’évoluer progressivement.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 Symfony Hello
&lt;/h3&gt;

&lt;p&gt;Une version "Hello world" &lt;strong&gt;ultra-minimaliste&lt;/strong&gt; de Symfony, offrant la puissance du full-stack framework avec une empreinte réduite.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Échelle&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Taille d'équipe&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Structuration&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Conventions&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fichier unique&lt;/td&gt;
&lt;td&gt;Solo dev&lt;/td&gt;
&lt;td&gt;Aucune&lt;/td&gt;
&lt;td&gt;Aucune&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;✅ Idéal pour :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rapidité&lt;/li&gt;
&lt;li&gt;Flexibilité&lt;/li&gt;
&lt;li&gt;Prototypage (&lt;strong&gt;PoC&lt;/strong&gt;) minimaliste&lt;/li&gt;
&lt;li&gt;Projets personnels non maintenus&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔹 Symfony Solo
&lt;/h3&gt;

&lt;p&gt;Symfony &lt;strong&gt;sans Flex&lt;/strong&gt;, offrant un contrôle total sur l’architecture.&lt;/p&gt;

&lt;p&gt;💡 Il permet un front controller &lt;strong&gt;CLI &amp;amp; HTTP&lt;/strong&gt; grâce au composant &lt;code&gt;Runtime&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;PHP_SAPI&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'cli'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'console'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'http'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Échelle&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Taille d'équipe&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Structuration&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Conventions&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Petits/moyens projets&lt;/td&gt;
&lt;td&gt;Solo dev&lt;/td&gt;
&lt;td&gt;Aucune (personnalisable)&lt;/td&gt;
&lt;td&gt;Aucune (liberté totale)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;✅ Idéal pour :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commencer en toute légèreté&lt;/li&gt;
&lt;li&gt;Scalabilité progressive (&lt;strong&gt;scale as you go&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Applications mono-tâche&lt;/li&gt;
&lt;li&gt;Projets personnels avancés&lt;/li&gt;
&lt;li&gt;PoC élaborés&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔹 Symfony Team
&lt;/h3&gt;

&lt;p&gt;Le framework &lt;strong&gt;full-stack classique&lt;/strong&gt;, conçu pour les projets de grande envergure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Échelle&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Taille d'équipe&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Structuration&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Conventions&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Moyens à grands projets&lt;/td&gt;
&lt;td&gt;Équipe de devs&lt;/td&gt;
&lt;td&gt;Standardisée&lt;/td&gt;
&lt;td&gt;Documentées&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;✅ Idéal pour :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Industrialisation&lt;/li&gt;
&lt;li&gt;Travail collaboratif&lt;/li&gt;
&lt;li&gt;Maintenance à long terme&lt;/li&gt;
&lt;li&gt;Projets professionnels&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚙️ Un petit teaser à propos de Symfony 8
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disparition des fichiers XML&lt;/strong&gt; pour la configuration des routes, désormais remplacés par des fichiers PHP.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤖 Hot Takes sur les LLMs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Les LLMs ne remplaceront pas les développeurs.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Ce sont d’excellents &lt;em&gt;stagiaires&lt;/em&gt; : disponibles 24/7, polis… mais parfois incohérents !&lt;/li&gt;
&lt;li&gt;Très utiles contre le &lt;strong&gt;syndrome de la page blanche&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; s’avère particulièrement efficace pour la génération de code et de documentation.&lt;/li&gt;
&lt;li&gt;Possibilité d’utiliser les LLMs pour :

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mise à jour automatique du code&lt;/strong&gt; (avec prudence).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation des PRs&lt;/strong&gt; pour améliorer la revue de code.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h1&gt;
  
  
  Symfony ObjectMapper Component (🎤 Antoine Bluchet)
&lt;/h1&gt;

&lt;p&gt;Après une partie d'historique sur l'arrivée du composant dans Symfony. Antoine a démarré par présenter le ce qui existait dans les autres langages et en PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ Object Mapping dans d'autres languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Spring : jackson&lt;/li&gt;
&lt;li&gt;.Net : automapper&lt;/li&gt;
&lt;li&gt;Java : 

&lt;ul&gt;
&lt;li&gt;modelmapper&lt;/li&gt;
&lt;li&gt;Java Map Struct&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Ruby : Ruby Object Mapper (ROM)&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛠️ Etat du mapping en d'objet en PHP
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Valinor&lt;/strong&gt; : offre du mapping et bien plus (ce qui n'est pas une bonne chose selon Antoine)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automapper (JoliCode)&lt;/strong&gt; : Fonctionne sur le principe de génération de classe PHP pour mapper les objets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MicroMapper (SymfonyCasts)&lt;/strong&gt; : version simpliste.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;et bien d'autres&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎯 Mapping vs Hydratation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hydratation&lt;/strong&gt; : remplir un objet avec des données.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping&lt;/strong&gt; : transformer un objet en un autre objet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Contrairement au serializer, le mapping en PHP permet une meilleure séparation des responsabilités et une meilleure intégration avec Symfony.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le composant
&lt;/h2&gt;

&lt;p&gt;Le composant offre une approche simple via des attributs permettant de faire un mapping rapide via des attributs.&lt;/p&gt;

&lt;p&gt;vous pouvez retrouver la Pull-Request &lt;a href="https://github.com/symfony/symfony/pull/51741" rel="noopener noreferrer"&gt;ici&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Exemple :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\ObjectMapper\ObjectMapper&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\ObjectMapper\Attributes\Map&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This maps class `A` to class `B`.&lt;/span&gt;
&lt;span class="na"&gt;#[Map(B::class)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;A&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This maps A::foo to B::bar.&lt;/span&gt;
    &lt;span class="na"&gt;#[Map(target: 'bar')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$foo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// This calls ucfirst on A::transform&lt;/span&gt;
    &lt;span class="na"&gt;#[Map(transform: 'ucfirst')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// This doesn't map A::bar if it's value is falsy.&lt;/span&gt;
    &lt;span class="na"&gt;#[Map(if: 'boolval')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$mapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$mapper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;A&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  PostgreSQL pour vos besoins NoSQL (🎤 David Buchmann)
&lt;/h1&gt;

&lt;p&gt;Après une brève introduction et un rappel sur le fait que le NoSQL n'est pas clairement défini alternant entre base de donnée non-relationnel et le fait qu'une base de donnée relationnel permette aussi de stocker des élements sans relation, il a fait le tours des fonctionnalité de PostGreSQL et de MySQL (ce qui a été une petite surprise compte tenu du titre) permettant de stocker et de requête du JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 Type de colonne JSON vs JSONb
&lt;/h2&gt;

&lt;p&gt;En PostGreSQL deux types de colonnes son proposés &lt;em&gt;JSON&lt;/em&gt; et &lt;em&gt;JSONb&lt;/em&gt; avec leurs avantages et leurs incovénients.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Avantages&lt;/th&gt;
&lt;th&gt;Inconvénients&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Préserve l'ordre des clés dans l'objet&lt;/td&gt;
&lt;td&gt;stocké sous forme de chaîne de caractères&lt;br&gt;aucune optimisation des caractères répétés&lt;br&gt;accepte du JSON invalide (garde les clés dupliquées)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSONb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;format binaire optimisé&lt;br&gt;Performant pour les requêtes&lt;/td&gt;
&lt;td&gt;Aucune mention faite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En dehors de cas d'utilisation non standard, il est préférable d'utiliser JSONB.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Opérateurs utiles
&lt;/h2&gt;

&lt;p&gt;Soit le JSON suivant :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"David"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-&amp;gt;&lt;/code&gt; : extrait une information JSON. ie : renvoie &lt;code&gt;"David"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; : extrait une donnée native tout en etant 2.5 fois plus rapide. ie : renvoie &lt;code&gt;David&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L'opérateur &lt;code&gt;-&amp;gt;&lt;/code&gt; existe aussi pour MySQL mais il faut préciser le JSONPath :&lt;br&gt;
&lt;code&gt;-&amp;gt;'$.author'&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Ajouter un index sur un champ json
&lt;/h2&gt;

&lt;p&gt;Il est possible d'ajouter un index sur un champ JSON via l'instruction&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;json_column&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;json_column&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce qui améliore le temps de requête considérablement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Générer une colonne à partir d'une propriété JSON
&lt;/h2&gt;

&lt;p&gt;Il est possible d'extraire et de générer une colonne à partir d'une propriété JSON via l'instruction&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;foo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;#&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="n"&gt;VAR_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_column&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;#&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;l'extraction d'une date est compliqué est nécessite de passer par une procédure stockée.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonctions utiles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;contient :

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;json_column @&amp;gt; '{"author": "David"}'&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;la clé existe :

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;json_column ? 'author'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jsonb_exists(json_column, 'author')&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;au moins une clé existe :

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;json_column ?| array['author', 'it_does_not_exists']&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jsonb_exists_any(json_column, array['author', 'it_does_not_exists'])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;toutes les clé existent :

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;json_column ?&amp;amp; array['author', 'it_does_not_exists']&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jsonb_exists_all(json_column, array['author', 'it_does_not_exists'])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h1&gt;
  
  
  Passkeys pour une authentification fluide et sécurisée (🎤 Rémi Janot)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  🔐 Problèmes des mots de passe
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Créés dans les années 60, ils sont devenus vulnérables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brute-force&lt;/strong&gt;, &lt;strong&gt;phishing&lt;/strong&gt;, &lt;strong&gt;réutilisation&lt;/strong&gt;…&lt;/li&gt;
&lt;li&gt;HaveIBeenPwned révèle l’ampleur des fuites de données.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔑 2FA / MFA
&lt;/h2&gt;

&lt;p&gt;Réside sur 3 piliers :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ce que l'on sait&lt;/strong&gt; (mot de passe).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ce que l'on a&lt;/strong&gt; (téléphone, clé de sécurité).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ce que l'on est&lt;/strong&gt; (empreinte digitale, reconnaissance faciale).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🌍 WebAuthn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;API JS, supportée par &lt;strong&gt;96 % des navigateurs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;S'appuie sur les 3 piliers du 2FA / MFA&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  💻 Démonstration
&lt;/h1&gt;

&lt;p&gt;La démonstration d'un intégration d'un système d'authentification WebAuthn a ensuite été présentée par Rémi avec une explication étape par étape de ce qui était transmis au serveur (payload JSON, request/response).&lt;/p&gt;

&lt;p&gt;Retrouvez le dépôt du projet de démonstration &lt;a href="https://github.com/rjanot/webauthn-demo-symfony" rel="noopener noreferrer"&gt;ici&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Symfony UX : Points forts de 2024 et perspectives d’avenir (🎤 Simon André)
&lt;/h1&gt;

&lt;p&gt;5 ans après : où en est Symfony UX ?&lt;/p&gt;

&lt;h2&gt;
  
  
  👨‍💻 Symfony UX core team
&lt;/h2&gt;

&lt;p&gt;Officialisation de la core team Symfony UX avec pour membres :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ryan Weaver&lt;/li&gt;
&lt;li&gt;Kévin Bond&lt;/li&gt;
&lt;li&gt;Hugo Alliaume&lt;/li&gt;
&lt;li&gt;Simon André&lt;/li&gt;
&lt;li&gt;Mathéo Daninos&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📌 Adoption
&lt;/h2&gt;

&lt;p&gt;Utilisé par &lt;strong&gt;Symfony&lt;/strong&gt;, &lt;strong&gt;EasyAdmin&lt;/strong&gt;, &lt;strong&gt;PrestaShop&lt;/strong&gt;, &lt;strong&gt;Sylius&lt;/strong&gt;, &lt;strong&gt;SensioLabs&lt;/strong&gt;, et même le &lt;strong&gt;ministère de l’Intérieur&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 Quelques chiffres
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2022&lt;/strong&gt; → 800 000 téléchargements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2023&lt;/strong&gt; → 4 000 000 téléchargements
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2024&lt;/strong&gt; → 1 500 000 téléchargements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Les packages UX les plus populaires :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;95 000/semaine&lt;/strong&gt; : Twig Component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;50 000/semaine&lt;/strong&gt; : UX Icons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;40 000/semaine&lt;/strong&gt; : Live Component.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔮 UX en 2025
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typony Express&lt;/strong&gt; : Création d'un jeu &lt;a href="https://zty.pe/" rel="noopener noreferrer"&gt;https://zty.pe/&lt;/a&gt; entièrement avec Symfony UX comme une expérimentation et avec une documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refonte du site UX&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un UX Bundle unique&lt;/strong&gt; pour simplifier l’écosystème et l'ajout de nouveau "modules" UX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UX v3 prévu pour l’été 2025&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symfony 8 : retour à une synchronisation des versions&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Rôles &amp;amp; Permissions : Marque blanche et Feature Flipping  (🎤 Florian Bogey)
&lt;/h1&gt;

&lt;p&gt;Florian nous as montré comment Rôle et Permissions peuvent être utilisé de concert pour permettre la mise en place d'un marque blanche et d'un mécanisme de feature flag/flipping/toggle/whatever you may call something that activate or not a feature on your website.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎭 Rôles vs Permissions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rôle&lt;/strong&gt; : catégorie utilisateur statique.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission&lt;/strong&gt; : règle métier dynamique.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Symfony gère les permission via les &lt;strong&gt;Voters&lt;/strong&gt;.&lt;br&gt;
Il est possible de cumuler les attributs &lt;code&gt;#[IsGranted()]&lt;/code&gt; pour différencier contrôles d'accès par les rôles des contrôles d'accès par permissions.&lt;/p&gt;

&lt;p&gt;Exemple :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Security\Http\Attribute\IsGranted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FooController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="na"&gt;#[Route('/foo/{id}', name: 'foo')]&lt;/span&gt;
    &lt;span class="na"&gt;#[IsGranted('ROLE_ADMIN')]&lt;/span&gt;
    &lt;span class="na"&gt;#[IsGranted('foo_voter', 'id')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🗳 Voter
&lt;/h2&gt;

&lt;p&gt;Les voters peuvent être configurés selon 4 stratégies :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Affirmative (par défaut) : Au moins un voter autorise l'action.&lt;/li&gt;
&lt;li&gt;Consensus : nécessite de voter qui autorise l'action que de voter qui ne l'interdise.&lt;/li&gt;
&lt;li&gt;Unanime : autorise l'action si aucun voter ne l'a interdite.&lt;/li&gt;
&lt;li&gt;Priority : renvoie la décision du premier voter qui ne s'est pas abstenu en s'appuyant sur la priorité définie dans les services.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🏢 Gestion de clients exigeants
&lt;/h2&gt;

&lt;p&gt;Deux besoins principaux :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Marque blanche&lt;/strong&gt; → Identité visuelle, branding personnalisé.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature Flipping&lt;/strong&gt; → Activer/désactiver des fonctionnalités par client.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💮 Marque blanche
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Définition :&lt;/strong&gt; La marque blanche est un&amp;nbsp;modèle commercial qui permet à une entreprise de vendre des produits ou des services conçus par une autre entreprise sous sa propre marque.&lt;/p&gt;

&lt;p&gt;Afin de pouvoir avoir une application "Multi-tenant" (comprendre un seul code source pour plusieurs marques/clients différents), Florian nous as expliqué qu'il était nécessaire que chaque dispose de sont propre fichier d'environnement.&lt;/p&gt;

&lt;p&gt;C'est rendu possible grâce&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;l'ajout d'une variable d'environnement côté serveur web (SetEnv apache et set nginx)&lt;/li&gt;
&lt;li&gt;une légère modification du front controller (le fichier public/index.php) de Symfony afin d'ajouter la logique permettant le chargement du bon fichier DotEnv.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DotEnv&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;loadEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s/.env.%s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'APP_SOME_NAME'&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;une extension TWIG pour transmettre les variables d'environnement nécessaire a TWIG.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🔄 Feature Flipping
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Définition&lt;/strong&gt; : Permettre d'activer/désactiver une fonctionnalité ou des partie d'une application sous certaines conditions.&lt;/p&gt;

&lt;p&gt;Après une présentation de plusieurs alternative et bundle permettant un feature flipping, Florian a présenté la solution maison qu'ils ont mis en place chez GL Events (retrouver les slides de Florian &lt;a href="https://slides.com/florianbogey/sf-live-roles-permissions" rel="noopener noreferrer"&gt;ici&lt;/a&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Async avec Messenger, AMQP et Mercure
&lt;/h1&gt;

&lt;h2&gt;
  
  
  🔄 Rappel sur Messenger
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Permet d’exécuter des tâches &lt;strong&gt;asynchrones&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Transports disponibles : &lt;strong&gt;sync&lt;/strong&gt;, &lt;strong&gt;in-memory&lt;/strong&gt;, &lt;strong&gt;AMQP&lt;/strong&gt;…&lt;/li&gt;
&lt;li&gt;Messages stockés dans une &lt;strong&gt;queue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Gestion avec un &lt;strong&gt;superviseur&lt;/strong&gt; (&lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;supervisord&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 &lt;strong&gt;Conseil&lt;/strong&gt; : Pour les imports &lt;strong&gt;async&lt;/strong&gt;, ne stockez &lt;strong&gt;pas&lt;/strong&gt; les fichiers en BDD/queue, mais sur un disque partagé.&lt;/p&gt;

&lt;h2&gt;
  
  
  📡 Donner du feedback en temps réel (exemple : import)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Mauvaises solutions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XHR Polling&lt;/strong&gt; : inefficace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt; : complexe et lourd à maintenir.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Solution moderne : Mercure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Surcouche à SSE (Server-Sent Events)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full duplex&lt;/strong&gt; et optimisé pour les updates en temps réel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Installation&lt;/strong&gt; via Composer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connexion JS&lt;/strong&gt; via &lt;code&gt;new EventSource()&lt;/code&gt; présent nativement dans les navigateurs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔐 Sécurité
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Authentification JWT intégrée.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publisher authentifié&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscriber anonyme ou non&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Symfony UX + Turbo + SSE
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;turbo_stream_listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'csv:'&lt;/span&gt; &lt;span class="err"&gt;~&lt;/span&gt; &lt;span class="nv"&gt;importId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permet de recevoir des mises à jour &lt;strong&gt;sans écrire une ligne de JS&lt;/strong&gt; !&lt;/p&gt;




&lt;h1&gt;
  
  
  Atteindre la qualité d’une SPA avec HTMX et Twig
&lt;/h1&gt;

&lt;h2&gt;
  
  
  🌍 HTMX : une alternative aux SPA
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Créé par Carson Gross en &lt;strong&gt;2020&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;50 KB&lt;/strong&gt;, plus léger que jQuery.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛠️ Fonctions principales
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;hx-get / hx-post&lt;/code&gt; : appels Ajax.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;hx-trigger&lt;/code&gt; : changement d’événement (&lt;code&gt;once&lt;/code&gt;, &lt;code&gt;throttle&lt;/code&gt;, &lt;code&gt;revealed&lt;/code&gt;…).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;hx-target&lt;/code&gt; : où afficher la réponse Ajax.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;hx-swap&lt;/code&gt; : méthode d’insertion (&lt;code&gt;innerHTML&lt;/code&gt;, &lt;code&gt;outerHTML&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;…).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;hx-boost&lt;/code&gt; : transforme tous les liens en requêtes AJAX.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📌 Twig et animations CSS
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Utilisation de &lt;code&gt;renderBlock()&lt;/code&gt; pour ne rendre qu’un bloc spécifique d’un template.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Morphing des éléments via un ID conservé.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Du Lego de composants pour un bundle Gotenberg
&lt;/h1&gt;

&lt;h2&gt;
  
  
  📜 Génération de PDF en Symfony
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Utilisation du bundle &lt;strong&gt;Gotenberg&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Permet de &lt;strong&gt;convertir HTML en PDF facilement&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exemples et démonstrations disponibles sur &lt;a href="https://medium.com/the-sensiolabs-tech-blog/how-to-generate-a-pdf-file-in-a-few-lines-of-code-with-symfony-39786a679d29" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>symfony</category>
      <category>symfonylive</category>
      <category>discuss</category>
      <category>php</category>
    </item>
    <item>
      <title>Symfony &amp; Doctrine Migrations: Validation in CI</title>
      <dc:creator>Thérage Kévin</dc:creator>
      <pubDate>Thu, 05 Sep 2024 10:13:59 +0000</pubDate>
      <link>https://dev.to/sensiolabs/symfony-doctrine-migrations-validation-in-ci-54pd</link>
      <guid>https://dev.to/sensiolabs/symfony-doctrine-migrations-validation-in-ci-54pd</guid>
      <description>&lt;p&gt;I had the opportunity to work on a project with a team that was relatively new to Doctrine migrations. To help them get used to it, and to discard the possibility of having pull (or merge) requests with changes to doctrine entities without generating a migration.&lt;/p&gt;

&lt;p&gt;Here is how I did it. I hope you'll enjoy it!&lt;/p&gt;

&lt;h2&gt;
  
  
  How Doctrine Migrations works
&lt;/h2&gt;

&lt;p&gt;When generating the migration, Doctrine will make a delta between its mapping and the current schema of the database. With this delta in "mind" (dare I say 😉) it will generate a &lt;strong&gt;migration file&lt;/strong&gt; with two main methods :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;up&lt;/code&gt; applies the SQL commands to fill the gap between the current database schema and its mapping. Used to deploy changes in the schema of your database.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;down&lt;/code&gt; allows to revert the migration with the SQL commands needed to "negate" the changes made in the up method. Used to roll back changes in the schema of your database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The magic trick
&lt;/h2&gt;

&lt;p&gt;There is currently no way to easily check if a migration has not been generated. Having this code merged could lead to a database schema being out of sync with your entity mapping and so resulting in a server error.&lt;/p&gt;

&lt;p&gt;The keywords in the above description are &lt;strong&gt;migration files&lt;/strong&gt;. I'll use the fact that, running the command bin/console doctrine:migration:diff will result in a newly generated file and will fail if there are no changes to apply.&lt;/p&gt;

&lt;p&gt;Knowing the list of existing files before the execution of that command, and then running it, can let me know that there are changes that were not committed to a &lt;strong&gt;migration file&lt;/strong&gt; in this pull (or merge) request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps to do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create your database&lt;/li&gt;
&lt;li&gt;Run your existing migrations&lt;/li&gt;
&lt;li&gt;Then run the step to check for missing changes (see below)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Advantages
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Testing that your migrations does not fail&lt;/li&gt;
&lt;li&gt;Ensure database schema consistency with Doctrine's mapping&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  You want the code snippet right!?
&lt;/h2&gt;

&lt;p&gt;Here is the bash code :&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pipefail

&lt;span class="c"&gt;# run doctrine migration diff to check if there is a new migration file generated and check last exit code&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;bin/console doctrine:migrations:diff &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error ! bin/console doctrine:migration:diff found a new migration which must not be the case."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c"&gt;# cat last file (should be the newly generated one)&lt;/span&gt;
    &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-Art&lt;/span&gt; migrations/&lt;span class="k"&gt;*&lt;/span&gt;.php | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c"&gt;# remove that file (just in case to comply with my paranoïac side)&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-Art&lt;/span&gt; migrations/&lt;span class="k"&gt;*&lt;/span&gt;.php | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! You can now ensure that each pull (or merge) request has working migrations, with no pending changes left out of the migrations!&lt;/p&gt;

&lt;h2&gt;
  
  
  Ok but why not use &lt;code&gt;bin/console doctrine:schema:validate&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;The reason was that the project we were working on was using doctrine's schema_filter configuration to filter out some tables we did not want to deal with (project-related inconvenience).&lt;/p&gt;

&lt;p&gt;The problem with bin/console doctrine:schema:validate was that it did not take care of the configuration, and so was dumping changes (trying to delete all the "normally" filtered out tables) not related to what we wanted.&lt;/p&gt;

&lt;p&gt;A colleague told me that this is a known issue that might be fixed soon (&lt;a href="https://github.com/doctrine/migrations/issues/1406" rel="noopener noreferrer"&gt;https://github.com/doctrine/migrations/issues/1406&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Thank you for reading this article and please leave your comments if you have any questions!&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Cover image: Unsplash by Ethan Weil&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>doctrine</category>
      <category>ci</category>
      <category>doctrinemigration</category>
    </item>
  </channel>
</rss>
