<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Kévin THÉRAGE | Expert Symfony Developer</title>
    <description><![CDATA[Technical blog over web development, Symfony, PHP and best practices. Find tutorials and advices for developers.]]></description>
    <lastBuildDate>2026-04-08T10:33:22+00:00</lastBuildDate>
    <link href="https://ktherage.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <link href="https://ktherage.github.io/" rel="alternate" type="text/html" />
    <item>
      <guid>https://ktherage.github.io/about-me/</guid>
      <title>about-me</title>
      <pubDate>2026-04-08T10:33:09+00:00</pubDate>
      <link href="https://ktherage.github.io/about-me/" rel="alternate" type="text/html" />
      <description><![CDATA[<h1 id="about-me">About Me</h1>
<p>I am a PHP/Symfony developer with over 9 years of experience in web development. Currently at SensioLabs, I have had the opportunity to work on various projects for demanding sectors: banks, public services, e-commerce and French press.</p>
<p>My specialization in Symfony (certified Expert on versions 6 and 7) allows me to work on complex architectures, critical migrations and large-scale technical overhauls. I particularly enjoy technical challenges that go beyond the usual scope: Domain-Driven Design, AWS Lambda deployments, integrations with technologies like ElasticSearch, Redis or RabbitMQ.</p>
<h2 id="my-journey">My journey</h2>
<p>My experience has been built around ambitious technical projects. I have notably participated in the multi-year overhaul of a major French bank's intranet, developed DDD backend applications hosted on AWS Lambda for a major press publisher, and supported the progressive migration of critical platforms for public services.</p>
<p>This diversity of contexts has allowed me to develop solid technical expertise, but also an ability to adapt to complex environments and specific business constraints. Whether it's modernizing legacy systems, designing robust APIs or optimizing performance on high-traffic applications, I enjoy taking on technical challenges that require creativity and rigor.</p>
<h2 id="my-vision-of-development">My vision of development</h2>
<p>I believe that code quality and collaboration are the foundations of a job well done. Each project is an opportunity to learn, share and improve my practices. I attach particular importance to technical design freedom, which allows proposing solutions adapted to real needs rather than standardized responses.</p>
<p>Mutual aid between developers and knowledge sharing are an integral part of my way of working. It is in this spirit that I regularly share my thoughts and feedback through technical articles on this site. My goal is to contribute to a technical community where everyone can progress and flourish.</p>]]></description>
    </item>
    <item>
      <guid>https://ktherage.github.io/blog/gitignore-blacklisting-whitelisting/</guid>
      <title>Debugging Git&#039;s .gitignore: Why Whitelisting Files in Subdirectories Fails</title>
      <pubDate>2026-03-25T00:00:00+00:00</pubDate>
      <link href="https://ktherage.github.io/blog/gitignore-blacklisting-whitelisting/" rel="alternate" type="text/html" />
      <description><![CDATA[<h2 id="introduction">Introduction</h2>
<p>When working with Git, it's common to use <code>.gitignore</code> to exclude files and directories. But sometimes, even well-intentioned rules can lead to unexpected behavior—especially when dealing with nested directories. In this post, I'll walk through a real-world example of how a <code>.gitignore</code> rule intended to keep a project clean ended up hiding important files, and how we fixed it.</p>
<hr>
<h2 id="the-setup">The Setup</h2>
<p>I wanted to keep the <code>.tools/</code> directory clean, tracking only <code>composer.json</code>, <code>composer.lock</code>, and the <code>.gitignore</code> file itself. My initial <code>.tools/.gitignore</code> looked like this:</p>
<pre><code class="language-gitignore">*
!.gitignore
!composer.json
!composer.lock</code></pre>
<p>Goal: Track only composer.json, composer.lock, and .gitignore in .tools/ and its subdirectories, ignoring everything else.</p>
<hr>
<h2 id="the-problem">The Problem</h2>
<p>After pushing this change, a colleague reported that their composer.lock file in <code>.tools/rector/</code> was being ignored. We used the following command to debug:</p>
<pre><code class="language-bash hljs bash">$ git check-ignore -v .tools/rector/composer.lock
.tools/.gitignore:1:*     .tools/rector/composer.lock</code></pre>
<hr>
<h2 id="root-cause">Root Cause</h2>
<p>Git's rule: <em>"It is not possible to re-include a file if a parent directory of that file is excluded."</em></p>
<p>The <code>*</code> pattern ignores both files and directories, which means Git never even looks inside <code>.tools/rector/</code>—so the whitelist rules for <code>composer.json</code> and <code>composer.lock</code> never apply.</p>
<hr>
<h2 id="the-solution">The Solution</h2>
<p>After debugging, we updated the <code>.gitignore</code> to explicitly allow directory traversal and re-include the necessary files:</p>
<pre><code class="language-gitignore"># Ignore all files and directories at this level
*

# But allow Git to inspect subdirectories
!*/

# Explicitly ignore vendor directories
vendor

# Whitelist composer.json in any subdirectory
!*/composer.json

# Whitelist composer.lock in any subdirectory
!*/composer.lock

# Always keep this .gitignore file
!.gitignore</code></pre>
<hr>
<h2 id="key-takeaways">Key Takeaways</h2>
<table>
<thead>
<tr>
<th>Directory/File</th>
<th>Rule Applied</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>.tools/</td>
<td><code>*</code></td>
<td>Ignored</td>
</tr>
<tr>
<td>.tools/rector/</td>
<td><code>!*/</code></td>
<td>Inspected</td>
</tr>
<tr>
<td>.tools/rector/vendor</td>
<td><code>vendor</code></td>
<td>Ignored</td>
</tr>
<tr>
<td>.tools/rector/composer.json</td>
<td><code>!*/composer.json</code></td>
<td>Tracked</td>
</tr>
</tbody>
</table>
<ul>
<li><strong>Git's Directory Traversal:</strong> When you use <code>*</code> to ignore everything, Git won't look inside directories unless you explicitly allow it with <code>!*/</code>.</li>
<li><strong>Testing Your Rules:</strong> Always test your <code>.gitignore</code> with <code>git check-ignore -v &lt;file&gt;</code> and <code>git status</code> to ensure the expected files are tracked.</li>
<li><strong>Order Matters:</strong> Place general exclusions first, then re-include specific files or directories.</li>
<li><strong>Common Pitfalls:</strong> Remember to re-exclude directories like <code>vendor</code> after whitelisting, or they'll be included in your repository.</li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Debugging <code>.gitignore</code> issues can be tricky, but understanding how Git evaluates directory traversal and pattern matching makes it much easier. Always test your rules with nested directories before committing, and don't hesitate to use <code>git check-ignore</code> to verify your setup.</p>]]></description>
    </item>
    <item>
      <guid>https://ktherage.github.io/blog/api-platform-con-2025-day-1/</guid>
      <title>API Platform con 2025 - DAY 1</title>
      <pubDate>2025-09-23T00:00:00+00:00</pubDate>
      <link href="https://ktherage.github.io/blog/api-platform-con-2025-day-1/" rel="alternate" type="text/html" />
      <description><![CDATA[<p>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.</p>
<p>Table of contents :</p>
<div id="toc"><ul>
<li><a href="#enhance-your-api-platform-apis-with-go-thanks-to-frankenphp-kevin-dunglas">Enhance your API Platform APIs with Go thanks to FrankenPHP (Kévin Dunglas)</a><ul>
<li><a href="#one-model-many-api-architecture-types">One Model, Many API Architecture Types</a></li>
<li><a href="#why-grpc-is-missing-in-api-platform">Why gRPC is missing in API Platform</a></li>
<li><a href="#how-grpc-works">How gRPC Works</a></li>
<li><a href="#grpc-with-frankenphp">gRPC with FrankenPHP</a></li>
</ul>
</li>
<li><a href="#extend-caddy-web-server-with-your-favorite-language-sylvain-combraque">Extend Caddy Web Server with Your Favorite Language (Sylvain Combraque)</a><ul>
<li><a href="#extending-caddy">Extending Caddy</a></li>
<li><a href="#extending-caddy-with-go">Extending Caddy with Go</a></li>
<li><a href="#using-interpreter">Using Interpreter</a></li>
<li><a href="#wasm-x-wasi-x-darkweak-wazemmes-for-caddy-extension-in-any-language">WASM x WASI x darkweak/wazemmes for Caddy Extension in Any Language</a></li>
</ul>
</li>
<li><a href="#mercure-sse-api-platform-and-an-llm-elevate-a-chat-bot-mathieu-santostefano">Mercure, SSE, API Platform and an LLM Elevate a Chat(bot) (Mathieu Santostefano)</a><ul>
<li><a href="#origin-of-the-subject">Origin of the Subject</a></li>
<li><a href="#toolbox">Toolbox</a></li>
<li><a href="#implementation">Implementation</a></li>
</ul>
</li>
<li><a href="#how-api-platform-4-2-is-redefining-api-development-antoine-bluchet">How API Platform 4.2 is Redefining API Development (Antoine Bluchet)</a><ul>
<li><a href="#what-s-new-in-4-2">What's New in 4.2</a></li>
<li><a href="#metadata-enhancements">Metadata Enhancements</a></li>
<li><a href="#from-api-filter-to-parameters">From API Filter to Parameters</a></li>
<li><a href="#json-schema-enhancements">JSON Schema Enhancements</a></li>
<li><a href="#performance">Performance</a></li>
<li><a href="#state-options">State Options</a></li>
<li><a href="#data-mapping">Data Mapping</a></li>
<li><a href="#debugging">Debugging</a></li>
<li><a href="#backward-compatibility">Backward Compatibility</a></li>
<li><a href="#looking-ahead-to-api-platform-5-0">Looking Ahead to API Platform 5.0</a></li>
</ul>
</li>
<li><a href="#design-pattern-the-treasure-is-in-the-vendor-smaine-milianni">Design pattern the treasure is in the vendor (Smaïne Milianni)</a></li>
<li><a href="#what-if-we-do-event-storming-in-our-api-platform-projects-gregory-planchat">What if we do Event Storming in our API Platform projects ? (Gregory Planchat)</a><ul>
<li><a href="#event-storming">Event Storming</a></li>
<li><a href="#preparation">Preparation</a></li>
<li><a href="#advantages">Advantages</a></li>
<li><a href="#with-api-platform">With API Platform</a></li>
<li><a href="#results">Results</a></li>
</ul>
</li>
<li><a href="#scaling-databases-tobias-petry">Scaling Databases (Tobias Petry)</a><ul>
<li><a href="#solutions">Solutions</a></li>
<li><a href="#sounds-complicated">Sounds complicated</a></li>
</ul>
</li>
<li><a href="#api-platform-jsonstreamer-and-esa-for-skyrocketing-api-mathias-arlaud">API Platform, JsonStreamer and ESA for skyrocketing API (Mathias Arlaud)</a><ul>
<li><a href="#serialization-normalization-in-symfony">Serialization / Normalization in Symfony</a></li>
<li><a href="#streaming-as-a-solution">Streaming as a solution</a></li>
<li><a href="#benchmarks-comparisons">Benchmarks &amp; comparisons</a></li>
<li><a href="#challenges-with-metadata-json-ld-and-how-api-platform-adapts">Challenges with metadata, JSON-LD and how API Platform adapts</a></li>
<li><a href="#esa-edge-side-apis-pattern">ESA (Edge Side APIs) pattern</a></li>
<li><a href="#takeaways">Takeaways</a></li>
</ul>
</li>
<li><a href="#credits">Credits</a></li>
</ul></div>
<hr>
<h2 id="enhance-your-api-platform-apis-with-go-thanks-to-frankenphp-kevin-dunglas">Enhance your API Platform APIs with Go thanks to FrankenPHP (Kévin Dunglas)</h2>
<p><strong>Slides of this talk are available : <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">https://dunglas.dev/2025/09/the-best-of-both-worlds-go-powered-grpc-for-your-php-and-api-platform-apps/</a></strong></p>
<p>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.</p>
<p>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.</p>
<p>He also paid tribute to Ryan Weaver, a key contributor, and encouraged attendees to support his family through the <a href="https://gofund.me/31ec53011" rel="noopener noreferrer">GoFundMe "In memory of Ryan Weaver: For his son Beckett"</a>.</p>
<h3 id="one-model-many-api-architecture-types">One Model, Many API Architecture Types</h3>
<p>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.</p>
<p>Here are some of the supported formats:</p>
<ul>
<li>Hydra</li>
<li>OpenAPI</li>
<li>HAL</li>
<li>JSON:API</li>
<li>GraphQL</li>
<li>Mercure (SSE support)</li>
</ul>
<p>This approach eliminates code duplication across different API format requirements.</p>
<h3 id="why-grpc-is-missing-in-api-platform">Why gRPC is missing in API Platform</h3>
<p>Currently, gRPC is not supported by API Platform. Here's why:</p>
<ul>
<li>gRPC does not follow REST principles.</li>
<li>It is different from GraphQL.</li>
<li>It uses Protobuf (a binary format) instead of JSON for the output format.</li>
</ul>
<p>Most of the time, in classic gRPC architecture, PHP is not a candidate.</p>
<figure>
<picture title="Schema of Typical gRPC Architecture">
<source type="image/webp" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 800w" width="800" height="560" sizes="100vw">
<img src="https://ktherage.github.io/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg" 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" loading="lazy" decoding="async" class="img-fluid rounded mx-auto d-block" width="800" height="560" style=";max-width:100%;height:auto;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAMgBkAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8/pRwaSioKLkUgAqdZBWaGI704SMO9UmI1BKPWl80Vl+aacJT600yWjSEgqeNs1mI5JrYsYDJiqREpWFCkio5FIFdDDphZelVr3TzGp4qrEKaZzMvWowuasXEZVyKSNM0mi3KyI9lFWxHxRUWZHtDCpVUscUVYtk3OKmx0PRDo7Nn7VKdNfHSuj02xV1GRWq+moE6VpGBwzxDTscBLalO1V8YNdNqdsqE8Vz0q4ajlN6dTmWo6EZcV12iopK5rkIzhga6PS7kpirjEVRXO/t4kCDpWdq4QIcYqtFqLBOtUNQvS6nJrRQuYxjY5+8A801DHRPJucmo1fFU4Gtm0WwRiiq/mUVPKRyMycVatTtcVGUoUlTXMdrVzsNMu0RRk1qyahHs61w0V0yDg1Kb9yMZrSJzSopsv6pcK5OK55+WNWJJ2k6mosZrRK5cYcosMeWFdDp1oWxgVj2iZcV22i2wYLxWjVjKrdLQfFpzFOlUNRsmRTxXbRW4VOlYutRAIcCpUzGm3fU87nXbIRUVW75cTGqlJzudiQUUuKKXMPlITTKKK50bCilNFFaRJYlKKKK2iSXbL74rvND6LRRVS2MKp1C/crC1r7jUUVkc8PiPP7/AP1p+tUhRRUndEcKKKKRZ//Z);background-repeat:no-repeat;background-position:center;background-size:cover;" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 800w" sizes="100vw">
</picture>
<figcaption>Schema of Typical gRPC Architecture</figcaption>
</figure>
<p>However, gRPC has several advantages:</p>
<ul>
<li>Fast and efficient</li>
<li>Strongly typed</li>
<li>Language agnostic: a code generator allows generating data structures in many languages.</li>
</ul>
<p>Use cases for gRPC include:</p>
<ul>
<li>Microservices</li>
<li>Internet of Things (IoT)</li>
<li>Critical components where performance is essential</li>
</ul>
<h3 id="how-grpc-works">How gRPC Works</h3>
<p>gRPC operates on HTTP/2 and uses Protocol Buffer <code>.proto</code> 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.</p>
<p>Moreover, the official gRPC documentation recommends using non-PHP languages for gRPC servers due to PHP-FPM's request lifecycle limitations.</p>
<h3 id="grpc-with-frankenphp">gRPC with FrankenPHP</h3>
<p>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.</p>
<p>For more information on configuration and usage of this extension, please refer to the <a href="https://github.com/dunglas/frankenphp-grpc" rel="noopener noreferrer">GitHub repository documentation</a>.</p>
<p>Testing can be done with gRPCui. It is important to note that this solution is still experimental.</p>
<hr>
<h2 id="extend-caddy-web-server-with-your-favorite-language-sylvain-combraque">Extend Caddy Web Server with Your Favorite Language (Sylvain Combraque)</h2>
<p>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.</p>
<h3 id="extending-caddy">Extending Caddy</h3>
<h4 id="using-xcaddy-build">Using xcaddy Build</h4>
<p>Extending Caddy can be done using the <code>xcaddy</code> 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.</p>
<h4 id="using-webui-from-caddy-website">Using WebUI from Caddy Website</h4>
<p>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.</p>
<h3 id="extending-caddy-with-go">Extending Caddy with Go</h3>
<figure>
<picture title="An example of a Caddy extension made with GO">
<source type="image/webp" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.webp 800w" width="800" height="560" sizes="100vw">
<img src="https://ktherage.github.io/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg" alt="An example of a Caddy extension made with GO" loading="lazy" decoding="async" class="img-fluid rounded mx-auto d-block" width="800" height="560" style=";max-width:100%;height:auto;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAMgBkAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8/pRwaSioKLkUgAqdZBWaGI704SMO9UmI1BKPWl80Vl+aacJT600yWjSEgqeNs1mI5JrYsYDJiqREpWFCkio5FIFdDDphZelVr3TzGp4qrEKaZzMvWowuasXEZVyKSNM0mi3KyI9lFWxHxRUWZHtDCpVUscUVYtk3OKmx0PRDo7Nn7VKdNfHSuj02xV1GRWq+moE6VpGBwzxDTscBLalO1V8YNdNqdsqE8Vz0q4ajlN6dTmWo6EZcV12iopK5rkIzhga6PS7kpirjEVRXO/t4kCDpWdq4QIcYqtFqLBOtUNQvS6nJrRQuYxjY5+8A801DHRPJucmo1fFU4Gtm0WwRiiq/mUVPKRyMycVatTtcVGUoUlTXMdrVzsNMu0RRk1qyahHs61w0V0yDg1Kb9yMZrSJzSopsv6pcK5OK55+WNWJJ2k6mosZrRK5cYcosMeWFdDp1oWxgVj2iZcV22i2wYLxWjVjKrdLQfFpzFOlUNRsmRTxXbRW4VOlYutRAIcCpUzGm3fU87nXbIRUVW75cTGqlJzudiQUUuKKXMPlITTKKK50bCilNFFaRJYlKKKK2iSXbL74rvND6LRRVS2MKp1C/crC1r7jUUVkc8PiPP7/AP1p+tUhRRUndEcKKKKRZ//Z);background-repeat:no-repeat;background-position:center;background-size:cover;" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_100400.abb2d9d9a62cfa0ca9981e2861fe6671.jpg 800w" sizes="100vw">
</picture>
<figcaption>An example of a Caddy extension made with GO</figcaption>
</figure>
<p>For more detailed information on how to extend Caddy with Go, you can refer to the <a href="https://caddyserver.com/docs/extending-caddy" rel="noopener noreferrer">official documentation</a>.</p>
<h3 id="using-interpreter">Using Interpreter</h3>
<p>While using interpreters to extend Caddy has its advantages, there are also some drawbacks:</p>
<ul>
<li>You need one interpreter per language.</li>
<li>New versions of the language require new interpreters.</li>
<li>Each interpreter is maintained separately.</li>
<li>You may need to re-implement types.</li>
</ul>
<h3 id="wasm-x-wasi-x-darkweak-wazemmes-for-caddy-extension-in-any-language">WASM x WASI x darkweak/wazemmes for Caddy Extension in Any Language</h3>
<p>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.</p>
<p>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.</p>
<p>For more information on using WASM and WASI in Caddy, you can check out the <a href="https://github.com/darkweak/wazemmes" rel="noopener noreferrer">darkweak/wazemmes repository on GitHub</a>.</p>
<hr>
<h2 id="mercure-sse-api-platform-and-an-llm-elevate-a-chat-bot-mathieu-santostefano">Mercure, SSE, API Platform and an LLM Elevate a Chat(bot) (Mathieu Santostefano)</h2>
<p><strong>Slides of this talk are available : <a href="https://welcomattic.github.io/slides-real-time-ai-chatbot-with-mercure/1" rel="noopener noreferrer">https://welcomattic.github.io/slides-real-time-ai-chatbot-with-mercure/1</a></strong></p>
<h3 id="origin-of-the-subject">Origin of the Subject</h3>
<p>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.</p>
<p>Evolved needs:</p>
<ul>
<li>Assist experts with an AI assistant</li>
<li>Allow AI to handle the first part of the conversation</li>
<li>Allow experts to take over when needed</li>
</ul>
<h3 id="toolbox">Toolbox</h3>
<h4 id="mercure-real-time-exchanges">Mercure - Real-time Exchanges</h4>
<p>Architecture:</p>
<ul>
<li>Server =&gt; Hub =&gt; Client</li>
<li>Client =&gt; Hub =&gt; Client</li>
</ul>
<h4 id="sse">SSE</h4>
<p>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.</p>
<h4 id="api-platform">API Platform</h4>
<h5 id="llm">LLM</h5>
<p>Data generation and intelligent responses</p>
<h5 id="symfony-messenger">Symfony Messenger</h5>
<p>Asynchronous process management</p>
<h5 id="symfony-ai">Symfony AI</h5>
<p>Equivalent to Mailer/Notifier but for AI providers:</p>
<ul>
<li><strong>Platform</strong>: unified interface for all AI providers</li>
<li><strong>Agent</strong>: agentic AI creation</li>
<li><strong>Store</strong>: data storage abstraction</li>
<li><strong>MCP SDK</strong>: now officially supported by Anthropic</li>
<li><strong>AI Bundle</strong>: full integration with Symfony</li>
<li><strong>MCP Bundle</strong>: additional components</li>
</ul>
<h3 id="implementation">Implementation</h3>
<p>Technical flow:
<code>User =&gt; message =&gt; Mercure (Storage) &lt;= Symfony SSE client =&gt; Symfony Messenger =&gt; Mistral =&gt; Mercure =&gt; AI response =&gt; User</code></p>
<p>Secure private chat via JWT (JSON Web Tokens) - an open standard for securely exchanging data between parties.</p>
<h4 id="sending-a-message-to-mercure">Sending a message to Mercure</h4>
<pre><code class="language-javascript hljs javascript">fetch(<span class="hljs-keyword">this</span>.hubURL, {
    <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
    <span class="hljs-attr">credentials</span>: <span class="hljs-string">'include'</span>, <span class="hljs-comment">// Send JWT cookie</span>
    <span class="hljs-attr">body</span>: <span class="hljs-keyword">new</span> URLSearchParams({
        <span class="hljs-attr">topic</span>: topic,
        <span class="hljs-attr">data</span>: <span class="hljs-built_in">JSON</span>.stringify(
            <span class="hljs-keyword">new</span> MercureUpdateData(
                conversationId,
                msg,
            ),
        ),
        <span class="hljs-attr">private</span>: <span class="hljs-string">'on'</span> <span class="hljs-comment">// restrict message to subscribed clients</span>
    })
});</code></pre>
<h4 id="connecting-to-mercure">Connecting to Mercure</h4>
<pre><code class="language-javascript hljs javascript"><span class="hljs-keyword">const</span> eventSource = <span class="hljs-keyword">new</span> EventSource(<span class="hljs-string">'/sse-endpoint'</span>);
eventSource.onmessage = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'New event received: '</span>, event.data);
};</code></pre>
<h4 id="symfony-sse-client">Symfony SSE Client</h4>
<p>Built-in EventSourceHttpClient:</p>
<pre><code class="language-php hljs php"><span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpClient</span>\<span class="hljs-title">Chunk</span>\<span class="hljs-title">ServerSentEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpClient</span>\<span class="hljs-title">EventSourceHttpClient</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpClient</span>\<span class="hljs-title">HttpClient</span>;

$eventSourceClient = <span class="hljs-keyword">new</span> EventSourceHttpClient(HttpClient::create());

$connection = $eventSourceClient-&gt;connect(<span class="hljs-string">"YOUR-MERCURE-URL"</span>);

<span class="hljs-keyword">while</span> (<span class="hljs-keyword">true</span>) {
    <span class="hljs-keyword">foreach</span> ($eventSourceClient-&gt;stream($connection, <span class="hljs-number">2</span>) <span class="hljs-keyword">as</span> $r =&gt; $chunk) {
        <span class="hljs-keyword">if</span> ($chunk-&gt;isTimeout()) <span class="hljs-keyword">continue</span>; <span class="hljs-comment">// Keep the connection alive.</span>
        <span class="hljs-keyword">if</span> ($chunk-&gt;isLast()) <span class="hljs-keyword">return</span>; <span class="hljs-comment">// Connection closed by server.</span>
        <span class="hljs-keyword">if</span> ($chunk <span class="hljs-keyword">instanceof</span> ServerSentEvent) <span class="hljs-keyword">$this</span>-&gt;processSSE($chunk);
    }
}</code></pre>
<h4 id="dispatching-messages-with-messenger">Dispatching messages with Messenger</h4>
<pre><code class="language-php hljs php"><span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpClient</span>\<span class="hljs-title">Chunk</span>\<span class="hljs-title">ServerSentEvent</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processSSE</span><span class="hljs-params">(ServerSentEvent $event)</span>: <span class="hljs-title">void</span>
</span>{
    $data = $event-&gt;getArrayData();

    <span class="hljs-comment">// do some checks before asking LLM</span>
    <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">$this</span>-&gt;shouldProcessWithAi($data)) {
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-comment">// Dispacth message to ask LLM</span>
    <span class="hljs-keyword">$this</span>-&gt;messageBus-&gt;dispatch(
        <span class="hljs-keyword">new</span> ProcessAiResponseMessage(
            conversationId: $data[<span class="hljs-string">'conversationId'</span>],
            userMessage: $data[<span class="hljs-string">'message'</span>],
            sseMessageId: $event-&gt;getId(),
            timestamp: $data[<span class="hljs-string">'timestamp'</span>]
        ),
    );
}</code></pre>
<h4 id="symfony-ai-configuration">Symfony AI Configuration</h4>
<h5 id="yaml-configuration-of-ai-bundle-with-mistral">YAML configuration of AI bundle with Mistral</h5>
<pre><code class="language-yaml hljs yaml"><span class="hljs-attr">ai:</span>
    <span class="hljs-attr">platform:</span>
        <span class="hljs-attr">mistral:</span>
            <span class="hljs-attr">api_key:</span> <span class="hljs-string">'%env(MISTRAL_API_KEY)%'</span>

    <span class="hljs-attr">agent:</span>
        <span class="hljs-attr">default:</span>
            <span class="hljs-attr">platform:</span> <span class="hljs-string">'symfony_ai.platform.mistral'</span>
            <span class="hljs-attr">model:</span>
                <span class="hljs-attr">class:</span> <span class="hljs-string">'Symfony\AI\Platform\Bridge\Mistral\Mistral'</span>
                <span class="hljs-attr">name:</span> <span class="hljs-type">!php</span><span class="hljs-string">/const</span> <span class="hljs-string">Symfony\AI\Platform\Bridge\Mistral\Mistral::MISTRAL_LARGE</span></code></pre>
<h5 id="handler-example">Handler Example</h5>
<pre><code class="language-php hljs php"><span class="hljs-comment">// Symfony AI Bundle code example</span>
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">AI</span>\<span class="hljs-title">Agent</span>\<span class="hljs-title">AgentInterface</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">AI</span>\<span class="hljs-title">Agent</span>\<span class="hljs-title">Chat</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">AI</span>\<span class="hljs-title">Platform</span>\<span class="hljs-title">Message</span>\<span class="hljs-title">Message</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">AI</span>\<span class="hljs-title">Platform</span>\<span class="hljs-title">Message</span>\<span class="hljs-title">MessageBag</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">AI</span>\<span class="hljs-title">Store</span>\<span class="hljs-title">StoreInterface</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessageHandler</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(
        private readonly AgentInterface $agent,
        private readonly StoreInterface $messageStore,
    )</span> </span>{
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span><span class="hljs-params">(ProcessAiResponseMessage $message)</span>
    </span>{
        $chat = <span class="hljs-keyword">new</span> Chat(<span class="hljs-keyword">$this</span>-&gt;agent, <span class="hljs-keyword">$this</span>-&gt;messageStore, <span class="hljs-string">"UNIQUE_ID_TO_PROMPT"</span>);
        $messages = <span class="hljs-keyword">$this</span>-&gt;messageStore-&gt;load(<span class="hljs-string">"UNIQUE_ID_TO_PROMPT"</span>);

        <span class="hljs-keyword">if</span> ($messages-&gt;count() === <span class="hljs-number">0</span>) {
            <span class="hljs-comment">// retrieve system prompt from somewhere ...</span>

            <span class="hljs-comment">// Programmatic System prompt injection</span>
            $chat-&gt;initiate(<span class="hljs-keyword">new</span> MessageBag(
                Message::forSystem(<span class="hljs-string">"SYSTEM_PROMPT_INJECTION"</span>),
            ));
        }

        $llmAnswer = $chat-&gt;submit(Message::ofUser($message-&gt;userMessage));

        <span class="hljs-comment">// do something with the answer</span>
    }
}</code></pre>
<p>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.</p>
<hr>
<h2 id="how-api-platform-4-2-is-redefining-api-development-antoine-bluchet">How API Platform 4.2 is Redefining API Development (Antoine Bluchet)</h2>
<p><strong>Slides of this talk are available : <a href="https://soyuka.me/api-platform-4-2-redefining-api-development/" rel="noopener noreferrer">https://soyuka.me/api-platform-4-2-redefining-api-development/</a></strong></p>
<p>Looking back at version 4.0:</p>
<ul>
<li>610 commits</li>
<li>~200,000 lines of code</li>
<li>291 issues opened</li>
<li>230 issues closed</li>
</ul>
<h3 id="what-s-new-in-4-2">What's New in 4.2</h3>
<p>Key features of this release:</p>
<ul>
<li>FrankenPHP integration</li>
<li>State Options</li>
<li>Query parameters enhancements</li>
<li>Performance improvements</li>
<li>Laravel compatibility</li>
<li>PHP File Metadata</li>
</ul>
<h3 id="metadata-enhancements">Metadata Enhancements</h3>
<h4 id="metadata-from-php-files">Metadata from PHP Files</h4>
<figure>
<picture title="An example of a PHP file metadata">
<source type="image/webp" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.webp 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.webp 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.webp 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.webp 800w" width="800" height="453" sizes="100vw">
<img src="https://ktherage.github.io/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.jpg" alt="An example of a PHP file metadata" loading="lazy" decoding="async" class="img-fluid rounded mx-auto d-block" width="800" height="453" style=";max-width:100%;height:auto;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAMgBkAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8Axbe0LHpWimnEjpU1jGCw4rehhUgcUkS2c8NLOelWY9PZe1dCtuvpUq26+lVcVznhp7elSrYsO1dCtsvpTxbL6UXC5gCzb0pfsbY6V0Ath6UG2HpRcOY5mWxZh0qm+lsT0NdgbZfSmG1X0ouFzjv7LYdqgmsSg6V2j2ygdKyNQjCqeKdyZS0OWMWDRVhyA5oqrHPzs1rKAqRxW5CvyioIrcKelXI1xWSOlskVamQU1RUqCqEPVaeBQKeBQK4YpCKfigimMjIphFSkVGaQiGQcViakhKnFbrDiqFzDvB4oWgmcXJA288UV0TWILHiitOYnlNNVqVVoVakVayRoKoqVRSKtSBaoQ5aeKQLTwKBCgUEUoGKCKBkZqM1MRTGWgCBqhkFWWWoXWkIqFeaKkK80UgHCpFoooRRKtSiiimSyQU8UUUxC0UUUFDTTTRRQBEaheiikIgPWiiikB//Z);background-repeat:no-repeat;background-position:center;background-size:cover;" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.jpg 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.jpg 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.jpg 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_134546.7364faf0d7b325c432fb8832ad102fcb.jpg 800w" sizes="100vw">
</picture>
<figcaption>An example of a PHP file metadata</figcaption>
</figure>
<p>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 <a href="https://github.com/api-platform/core/pull/7017" rel="noopener noreferrer">https://github.com/api-platform/core/pull/7017</a>.</p>
<h4 id="metadata-mutator">Metadata Mutator</h4>
<p>A new way to programmatically modify metadata:</p>
<ul>
<li>More flexible configuration</li>
<li>Runtime adjustments</li>
<li>Cleaner architecture</li>
</ul>
<h3 id="from-api-filter-to-parameters">From API Filter to Parameters</h3>
<h4 id="api-filter-retrospective">API Filter Retrospective</h4>
<p>The <code>#[ApiFilter]</code> attribute was doing a lot of things in the background, such as:</p>
<ul>
<li>Declare services with filter tags</li>
<li>Generate documentation</li>
<li>Apply database operations</li>
<li>Work with multiple properties</li>
</ul>
<p>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.</p>
<h4 id="filter-documentation-improvements">Filter Documentation Improvements</h4>
<p>Now documentations are generated separately. This can be done with two new interfaces</p>
<ul>
<li><code>JsonSchemaFilterInterface</code></li>
<li><code>OpenApiParameterFilter</code></li>
</ul>
<h4 id="filtering-system">Filtering System</h4>
<p>Now Filter are independent through a new <code>FilterInterface</code> with:</p>
<ul>
<li>Simplified <code>apply()</code> method</li>
<li>No constructor requirements</li>
<li>Dependency-free design</li>
</ul>
<h4 id="parameter-system">Parameter System</h4>
<p>The <code>#[ApiFilter]</code> attribute will leave his place to a new property of Operations attributes called <code>parameters</code></p>
<figure>
<picture title="An example of Parameters usage">
<source type="image/webp" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.webp 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.webp 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.webp 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.webp 800w" width="800" height="442" sizes="100vw">
<img src="https://ktherage.github.io/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.jpg" alt="An example of Parameters usage" loading="lazy" decoding="async" class="img-fluid rounded mx-auto d-block" width="800" height="442" style=";max-width:100%;height:auto;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAMgBkAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A89QFjxVyK3cjpUlnZFnGa6mx0oMgOKqUWRGrF7HNpbSA9KvwwMO1dOmjr/dqddIH92kinO5ziwt6VIsLeldIulD0qQaWP7tXclyOYMT+lKIXPauoGlj+7S/2WP7tFxcxyE1uxHSs6SykLdDXfHSgf4ajbSFP8NPmHzHAmzcdRTGhI7V2tzpSop4rCurXYx4q0uY5q1dQMMxGitDyaKXs2cv1tFmwiUyCuw0+IbBXFaVPvkFd3p3MYp1JJl4enKO5fSEY6VMsI9KVBxUgFZHYIIR6U4RD0p4p4oAjEQ9KPKHpUwFGKAIDEPSmNEPSrJFRt0osIyb1BsNcpfgBzXW6gcIa4rU5trmuqiefiqUp7FU4zRVTz/eiunQ8/wCq1CLRQfNFei6bxGtee6Rw4Nd1Yz4jFeYfSyjY31IxUgxWctzx1qRbketFiLGgKeDWeLoetO+1e9OwWZoAilyKz/tY9aQ3Y9aLCsXyRUbEVSN2PWmtdigViHUfuGuD1cHea7S7n3Ia5HUl3MatS5SopN6mBg0VYMfNFV7ZmnIhdK+8K7Gz+4KKKyKkXxTxRRTRA4UoooqikJSGiigTGk0lFFSQyC4+6a52+6miipYR3Mw9aKKKg1P/2Q==);background-repeat:no-repeat;background-position:center;background-size:cover;" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.jpg 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.jpg 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.jpg 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_135352.37a61667fe9cf52f8b7347629b3a055f.jpg 800w" sizes="100vw">
</picture>
<figcaption>An example of Parameters usage</figcaption>
</figure>
<h4 id="new-filter-types">New Filter Types</h4>
<ul>
<li>Free text search capabilities</li>
<li>URI variable provider</li>
</ul>
<h3 id="json-schema-enhancements">JSON Schema Enhancements</h3>
<p>Some improvements were made on JSON Schema generation. Those changes could imply a backward compatibility break for tools using the former JSON Schema.</p>
<p>Improvements</p>
<ul>
<li>Schema mutualization</li>
<li>30% smaller OpenAPI specification files</li>
<li>Reduced I/O operations</li>
</ul>
<p>A new tool is now recommended : <a href="https://pb33f.io" rel="noopener noreferrer">pb33f.io</a> as it is more feature-rich and better maintained than Swagger UI.</p>
<h3 id="performance">Performance</h3>
<p>Performance benchmarks comparing Nginx vs FrankenPHP:</p>
<figure>
<picture title="Performance comparison between Nginx and FrankenPHP 1">
<source type="image/webp" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.webp 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.webp 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.webp 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.webp 800w" width="800" height="503" sizes="100vw">
<img src="https://ktherage.github.io/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.jpg" alt="Performance comparison between Nginx and FrankenPHP 1 " loading="lazy" decoding="async" class="img-fluid rounded mx-auto d-block" width="800" height="503" style=";max-width:100%;height:auto;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAMgBkAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8/paKKSQx6dasrHkVXjHNaEERcdK1iS2V/LpRHWiLNz2pwsn9KqwuYzhHTtuK0fsL+lMaycdqpWJ5igRxUL9auy27KOlUnBBrWKuJsjxT1XNCoTVmOL2pyRlKViDYaKu+T7UVlymftCH+zH9KcNKc9jXcrpqf3alXTU/u1gkdTmcPDpD7hwa37DSTgZWuhj01M9Kvw2ioOlUQ5mSmkrjpTv7LUHpW6UVRVd2AapbOarVcTPXSlI+7UU2lqB0rciIxUM5BpXZEqr5bnK3WlhgcCseXRW3fdruxCr9qRrJPStYVLGlKbkjhU0Yj+GrA0sqOldh9jQdqbLaqF6Vo6lypRucj9hPpRXQtbrnpRRzEezLQnQdxTlukHcVxZ1o/wB6mHWyP4qhQN2md9HdJnqKtpMCOK89t9bJcfNXTadeGZRzVcliJXRrzS4FU/MLNVopvFItvzWc43Oea5kIshC1C8pJq2YeKiaDHNJx0M4x0sNjkCjmke7Re9U7yXyUNcxeasyOQDVwpm9KNjr/ALcnqKZJeIV61w/9stnqaeNWZu9OSsdCizqWulz1orlv7Rb1oqOYfIznyajNFFbQN2WLT/WCu50T7q0UVUjlqHURfdFSiiisWczHHpUUnSiimSjC1X7hrhL/AP1poorWB00SitTpRRWdQ7CWiiisBH//2Q==);background-repeat:no-repeat;background-position:center;background-size:cover;" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.jpg 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.jpg 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.jpg 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_140008.c0143ed4065f1c319a49d3932b6470ea.jpg 800w" sizes="100vw">
</picture>
<figcaption>Performance comparison between Nginx and FrankenPHP 1</figcaption>
</figure>
<figure>
<picture title="Performance comparison between Nginx and FrankenPHP 2">
<source type="image/webp" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.webp 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.webp 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.webp 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.webp 800w" width="800" height="435" sizes="100vw">
<img src="https://ktherage.github.io/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.jpg" alt="Performance comparison between Nginx and FrankenPHP 2" loading="lazy" decoding="async" class="img-fluid rounded mx-auto d-block" width="800" height="435" style=";max-width:100%;height:auto;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAMgBkAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8qhXmtGEdKpxLg1ehHIoGaNshbFa0MDY6VFpduJCOK6y20wFAcVcdCGzn/IYdqURN6V1H9mD0ph05QelPmIc0jm/JfHSmmF/SuqTTA3anNpIx92mpCU09TkDA3pVS4hYA8V18unhT0qjdWA2E4oU7szVZXscU6kE5oQZq9ew7HNVo15rdQ0HOY4JxRU4HFFLkMvamJHZv6VaitXDDg12CaIP7tWI9FGR8tcqR2uRR0S3IK5FdxZxDYOKzLLTxFjitqLCLVJGUmSNGAOlUpcBqsyXAA61nyTBm61M9DixEmloX7YA1aeMbKpWsgAFWZLgBetCTsKjK8TNuVG41QuI90Zq5PKGahIvMWpi/eMI39ocRqdqxc4FZqWzg9K7+40sSH7tVf7GA/hrvjUVjv5bnJC3bHSiut/skelFHOifZmwkC+lTrAPShBU6iuNHQNWMCopm2jipz0qCRdwraBhNsy7ids1BG7M1XpLbcelLFaYPSoqxuzOa5oWHxEhagnnYVoCDC1BLa7u1Ul7tjGlFxZmK7O3NbFmvAzVdLTB6VoQR7RWChqW4+/dE3lAjpTGhX0qcHimMa01OlMgMa56UVJRU6lXK61MKKKaKENMNFFaxMJkZ609KKKTIexMOlMaiihbEoQdamSiipKJO1MNFFBaG0UUVJR//2Q==);background-repeat:no-repeat;background-position:center;background-size:cover;" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.jpg 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.jpg 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.jpg 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250918_140037.f722bcd68b0b03577988e877110ceb80.jpg 800w" sizes="100vw">
</picture>
<figcaption>Performance comparison between Nginx and FrankenPHP 2</figcaption>
</figure>
<p>More benchmarks available at <a href="https://soyuka.github.io/sylius-benchmarks/" rel="noopener noreferrer">soyuka.github.io/sylius-benchmarks/</a></p>
<p>JSON Streamer improvements:</p>
<ul>
<li>~32.4% better request/second performance</li>
<li>Configurable via settings</li>
</ul>
<h3 id="state-options">State Options</h3>
<p>New features for querying subresources:</p>
<ul>
<li>More efficient data loading</li>
<li>Entity class magic (RIP Ryan)</li>
</ul>
<h3 id="data-mapping">Data Mapping</h3>
<p>New mapping capabilities:</p>
<ul>
<li>Database to API representation mapping</li>
<li>Symfony ObjectMapper integration</li>
<li>Better data transformation</li>
</ul>
<h3 id="debugging">Debugging</h3>
<p>Profiling tools are back!</p>
<h3 id="backward-compatibility">Backward Compatibility</h3>
<ul>
<li>Many new features added</li>
<li>No deprecations in this version</li>
<li>Parameters system is no longer experimental</li>
</ul>
<h3 id="looking-ahead-to-api-platform-5-0">Looking Ahead to API Platform 5.0</h3>
<p>Planned changes:</p>
<ul>
<li><code>#[ApiFilter]</code> deprecation (migration script coming soon)</li>
<li>More JSON Streamer usage</li>
<li>Object Mapper feature requests</li>
<li>Community-driven improvements</li>
</ul>
<hr>
<h2 id="design-pattern-the-treasure-is-in-the-vendor-smaine-milianni">Design pattern the treasure is in the vendor (Smaïne Milianni)</h2>
<p><strong>Slides of this talk are available : <a href="https://ismail1432.github.io/conferences/2025/apip_con/index.html" rel="noopener noreferrer">https://ismail1432.github.io/conferences/2025/apip_con/index.html</a></strong></p>
<p>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.</p>
<p>Among them were :</p>
<ul>
<li>The Strategy Pattern</li>
<li>The Adapter Pattern</li>
<li>The Factory Pattern</li>
<li>The Builder Pattern</li>
<li>The Proxy Pattern</li>
<li>The Observer Pattern</li>
<li>The Event Dispatcher Pattern</li>
<li>The Decorator Pattern</li>
<li>The Facade Pattern</li>
<li>The Template Pattern</li>
<li>The Chain of Responsibility Pattern</li>
</ul>
<p>Smaïne also ended with a shoutout to Ryan Weaver.</p>
<hr>
<h2 id="what-if-we-do-event-storming-in-our-api-platform-projects-gregory-planchat">What if we do Event Storming in our API Platform projects ? (Gregory Planchat)</h2>
<h3 id="event-storming">Event Storming</h3>
<p>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.</p>
<p>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.</p>
<h3 id="preparation">Preparation</h3>
<p>The Event Storming process follows a structured approach with several key steps:</p>
<ol>
<li><strong>List the events</strong> - Start by identifying all the significant events that happen in your business domain</li>
<li><strong>Organize the events</strong> - Arrange these events in a chronological or logical order</li>
<li><strong>Set up the commands</strong> - Identify what actions trigger each event</li>
<li><strong>Set up the actors</strong> - Determine who or what initiates each command</li>
<li><strong>Green post-its</strong> - Add the data necessary for users to make decisions</li>
<li><strong>Add external systems</strong> - Include third-party systems that interact with your domain</li>
<li><strong>Aggregates</strong> - Group related events and commands into cohesive business concepts</li>
</ol>
<p>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.</p>
<h3 id="advantages">Advantages</h3>
<p>Event Storming brings several concrete benefits to development teams:</p>
<ul>
<li><strong>Process documentation</strong> - The workshop naturally creates living documentation of your business processes</li>
<li><strong>Facilitated onboarding</strong> - New team members can quickly understand the domain by looking at the Event Storming artifacts</li>
<li><strong>Reveals uncertainties</strong> - Hidden assumptions and unclear requirements surface during the collaborative sessions</li>
</ul>
<h3 id="with-api-platform">With API Platform</h3>
<h4 id="the-anemic-model-problem">The Anemic Model Problem</h4>
<p>Most applications suffer from what's called the anemic model anti-pattern, where:</p>
<ul>
<li>Business logic is scattered across numerous services</li>
<li>Loss of user intention tracking</li>
<li>Entities become mere data containers with getters and setters</li>
</ul>
<p>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.</p>
<p>The intention is essential for third-party systems to understand what actually happened in your application.</p>
<h4 id="rich-models">Rich Models</h4>
<p>The alternative approach uses rich domain models that:</p>
<ul>
<li><strong>Require significant cost</strong> - More complex to implement initially</li>
<li><strong>Require detailed application understanding</strong> - Team needs deep domain knowledge</li>
<li><strong>Guarantee consistency over time</strong> - Business rules are enforced at the model level</li>
<li><strong>Apply business constraints</strong> - Validation logic lives where it belongs</li>
<li><strong>Centralize business logic</strong> - Everything related to a concept lives in one or two classes</li>
<li><strong>The model guarantees integrity</strong> - Invalid states become impossible</li>
</ul>
<p>With a rich model, all changes happen within the entity itself, keeping the business logic centralized and coherent.</p>
<h4 id="the-crud-problem">The CRUD Problem</h4>
<p>Traditional CRUD operations are:</p>
<ul>
<li>Limited to 4 operations (Create, Read, Update, Delete)</li>
<li>SQL-centric thinking</li>
<li>Tools like PostgREST generate REST APIs automatically but provide little business value</li>
</ul>
<p>To do better, we can leverage the power of API Platform's State Providers and State Processors.</p>
<p>But how do we preserve intention in our application?</p>
<p>The solution follows this flow:
<strong>Repository → EventBus → Event → Handler</strong></p>
<h4 id="model-modification">Model Modification</h4>
<p>The team implemented a pattern with three key methods in their entities:</p>
<ul>
<li><strong>recordThat()</strong> - Records that an event occurred (e.g., "a deployment was launched")</li>
<li><strong>apply()</strong> - Applies the modifications related to the event (e.g., updates the deployment date)</li>
<li><strong>releaseEvents()</strong> - A cleanup step that happens during the save process, just before persist/flush, then dispatches events throughout the application</li>
</ul>
<p>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.</p>
<h3 id="results">Results</h3>
<p>The team reported several concrete improvements after implementing this approach:</p>
<ul>
<li><strong>An API and codebase that better resembled the company's business domain</strong></li>
<li><strong>User intention was preserved</strong> throughout the application lifecycle</li>
<li><strong>Better understanding of actions performed</strong> in the application, both for developers and business stakeholders</li>
</ul>
<figure>
<picture title="Abstract of the OpenAPI documentation of an Event Stormed done API">
<source type="image/webp" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.webp 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.webp 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.webp 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.webp 800w" width="800" height="394" sizes="100vw">
<img src="https://ktherage.github.io/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.jpg" alt="Abstract of the OpenAPI documentation of an Event Stormed done API" loading="lazy" decoding="async" class="img-fluid rounded mx-auto d-block" width="800" height="394" style=";max-width:100%;height:auto;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAMgBkAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8Wity54FXE0p3HStLTrMO4GK6600pCgJFMlSucPHoz+hq3Ho7/3TXeJpSD+Gp00xB/CKVyrHDR6M/oav2+kOpHBrsk05P7tWEsUHai4WOT/st2XpUf8AYjk9DXcpZp6Cpls4/QU1KwHDR6G47Vci0Zx2NdktpGOwqQWyDtVe0YWOUTSWA6VBd6cyoeK7QwqB0rM1FF8s0KTbGcDJZneeKK1ZVHmGiuixHMc5pX3xXZWjfIK5TS4TuFddaQnYK5mY007ltWqVXpEgqwlv7VJ0DVepVc09bf2qdbf2oAiVjUqsamW29qlW29qAIAxp4Y1ZFtTxb+1AFJicVkaiTsNdI1vx0rI1K3+Q1cNxS2OLlz5hoq3LB+8PFFdljnuZmnQgMK6m2ACiuNtL4Kw5rai1QADmuJodJ3OlQrVhCK5tNUHrVmPUwe9Kx0HRIRU6YrAj1IetWY9QB70co0jdXFSrisdL4etTrej1o5WFjWAFOAFZi3o9alW7B70crC5ebG2sXU8bDV5rnI61i6pcfIaqEXcmT0MGVh5hoqjLP+8PNFdnKc5yduea0YycUUVxsqkWENW4yaKKEdCLsRq7EaKKpGiLcZqwpooqkTIlWrEdFFNmZN/DWRqf3DRRShuTLY5SX/WGiiiuswP/2Q==);background-repeat:no-repeat;background-position:center;background-size:cover;" srcset="https://ktherage.github.io/thumbnails/480x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.jpg 480w, https://ktherage.github.io/thumbnails/640x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.jpg 640w, https://ktherage.github.io/thumbnails/768x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.jpg 768w, https://ktherage.github.io/thumbnails/800x/img/IMG_20250919_103045.8c36cb59c99c943fce44906124033d9f.jpg 800w" sizes="100vw">
</picture>
<figcaption>Abstract of the OpenAPI documentation of an Event Stormed done API</figcaption>
</figure>
<hr>
<h2 id="scaling-databases-tobias-petry">Scaling Databases (Tobias Petry)</h2>
<p>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.</p>
<h3 id="solutions">Solutions</h3>
<p>There is no silver bullet: every application has its own constraints. Still, several well-known strategies exist:</p>
<ul>
<li>
<p><strong>Find and fix slow queries</strong><br>
Before considering infrastructure, always check the basics. Tools like <a href="https://mysqlexplain.com" rel="noopener noreferrer">mysqlexplain.com</a> can help detect inefficient queries and suggest improvements.</p>
</li>
<li>
<p><strong>Cache results</strong><br>
Serving cached responses drastically reduces the load on the database and avoids repeating costly operations.</p>
</li>
<li>
<p><strong>Vertical scaling (bigger machines)</strong><br>
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.</p>
</li>
<li>
<p><strong>Multi-master replication</strong><br>
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.</p>
</li>
<li>
<p><strong>Read replication</strong><br>
Here, a single primary node handles writes, while replicas serve read queries.</p>
<ul>
<li><strong>Synchronous replication</strong> ensures that changes are propagated to all replicas before acknowledging the write. This guarantees consistency but adds latency, as every replica must confirm.</li>
<li><strong>Asynchronous replication</strong> acknowledges the write immediately and updates replicas later. It reduces latency but risks temporary inconsistency between nodes.<br>
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.</li>
</ul>
</li>
<li>
<p><strong>Sharding</strong><br>
Sharding distributes data across multiple databases. This enables <em>theoretically infinite scalability</em>. For example, users might be split across shards based on their ID.<br>
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 <em>shards of shards</em>, adding another layer of complexity.<br>
Because of this overhead, sharding is usually reserved for very large-scale systems. For most use cases, read replication is sufficient.</p>
</li>
</ul>
<p>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 <a href="https://sqlfordevs.com" rel="noopener noreferrer">sqlfordevs.com</a> (free course on making analytics faster) or specialized systems such as <a href="https://www.timescale.com" rel="noopener noreferrer">TimescaleDB</a>.</p>
<h3 id="sounds-complicated">Sounds complicated</h3>
<p>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.</p>
<hr>
<h2 id="api-platform-jsonstreamer-and-esa-for-skyrocketing-api-mathias-arlaud">API Platform, JsonStreamer and ESA for skyrocketing API (Mathias Arlaud)</h2>
<p><strong>Slides of this talk are available : <a href="https://www.canva.com/design/DAGyYPxkygw/M1RzOiv8_cMp0Pa7Mh0u4g/view" rel="noopener noreferrer">https://www.canva.com/design/DAGyYPxkygw/M1RzOiv8_cMp0Pa7Mh0u4g/view</a></strong></p>
<p>Storytelling: imagine a bookstore. A customer orders <strong>all</strong> 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.</p>
<p>In the API world, <strong>JSON is king</strong>.</p>
<p>At the heart of our stack is <strong>API Platform</strong>, which relies on Symfony’s Serializer. But sometimes the Serializer is like that bookseller: it works well until the load becomes too heavy.</p>
<h3 id="serialization-normalization-in-symfony">Serialization / Normalization in Symfony</h3>
<p>Serialization in Symfony (and in API Platform) involves turning PHP objects into arrays or scalar values, then encoding to formats like JSON or XML. <strong>Normalization</strong> transforms the internal object graph into a neutral data structure (arrays, scalars), applying metadata such as groups or attributes. <strong>Encoding</strong> then converts that structure into the final JSON string. The reverse process (<strong>denormalization</strong>) handles input JSON → arrays → objects.</p>
<p>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.</p>
<h3 id="streaming-as-a-solution">Streaming as a solution</h3>
<p>Instead of building a huge in-memory structure, streaming emits JSON pieces <strong>incrementally</strong>. You only keep in memory what’s necessary at each moment.</p>
<p>Symfony 7.3 introduces the <strong>JsonStreamer</strong> component for that purpose.</p>
<p>Some key features:</p>
<ul>
<li>Works best with <strong>POPOs</strong> (Plain Old PHP Objects) having public properties, without complex constructors.</li>
<li>The <code>#[JsonStreamable]</code> attribute can be used on classes to mark them as streamable. This also allows pre-generation of code during cache warm-up.</li>
<li>Use the <strong>TypeInfo</strong> component (<a href="https://symfony.com/blog/new-in-symfony-7-3-jsonstreamer-component" rel="noopener noreferrer">link</a>) to describe types of collections and objects (e.g., <code>Type::list(Type::object(MyDto::class))</code>). This helps JsonStreamer guess the shape of the output JSON without loading everything in memory.</li>
</ul>
<p>Here is a code snippet from the Symfony documentation showing basic usage:</p>
<pre><code class="language-php hljs php"><span class="hljs-comment">// Example class</span>
<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Dto</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">JsonStreamer</span>\<span class="hljs-title">Attribute</span>\<span class="hljs-title">JsonStreamable</span>;

<span class="hljs-comment">#[JsonStreamable]</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span>
</span>{
    <span class="hljs-keyword">public</span> string $name;
    <span class="hljs-keyword">public</span> int $age;
    <span class="hljs-keyword">public</span> string $email;
}

<span class="hljs-comment">// In controller</span>
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">JsonStreamer</span>\<span class="hljs-title">StreamWriterInterface</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">TypeInfo</span>\<span class="hljs-title">Type</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpFoundation</span>\<span class="hljs-title">StreamedResponse</span>;

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">retrieveUsers</span><span class="hljs-params">(StreamWriterInterface $jsonStreamWriter, UserRepository $userRepository)</span>: <span class="hljs-title">StreamedResponse</span>
</span>{
    $users = $userRepository-&gt;findAll();
    $type = Type::list(Type::object(User::class));
    $json = $jsonStreamWriter-&gt;write($users, $type);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> StreamedResponse($json);
}</code></pre>
<p>Benchmarks &amp; comparisons</p>
<p>For a dataset of 10,000 objects:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Time</th>
<th>Memory usage / footprint (rough / relative)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Serializer (traditional)</td>
<td>~ 204 ms</td>
<td>~ 16 MB (grows with size)</td>
</tr>
<tr>
<td>JsonStreamer</td>
<td>~ 87 ms</td>
<td>~ 8 MB (much more constant)</td>
</tr>
</tbody>
</table>
<p>Challenges with metadata, JSON-LD and how API Platform adapts</p>
<p>API Platform adds metadata, JSON-LD contexts, property metadata, etc. That adds overhead in serialization. To integrate JsonStreamer while preserving rich metadata:</p>
<p>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.</p>
<p>API Platform</p>
<p>Use of ValueTransformers that can transform any value at runtime. But caution: heavy logic in transformers can degrade performance (they run per value).</p>
<p>Symfony
+1</p>
<p>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.</p>
<p>ESA (Edge Side APIs) pattern</p>
<p>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:</p>
<p>Instead of sending one huge JSON structure, partition or paginate so the client can start receiving some data quickly.</p>
<p>Combine with streaming so that parts of the response start being delivered early (TTFB improves).</p>
<p>Good user experience: user sees something quickly rather than waiting for full load.</p>
<p>Takeaways</p>
<p>Serializer works, but for large data sets it becomes inefficient.</p>
<p>JsonStreamer gives significant improvements in both memory usage and time to first byte.</p>
<p>When you have metadata layers (API Platform, JSON-LD), use the extension points provided to plug streaming without losing features.</p>
<p>Avoid heavy computations / transformations in runtime‐hot paths (e.g., ValueTransformers).</p>
<p>Design your API knowing these options early, because once core serialization path is deeply embedded, changing is hard.</p>
<h3 id="benchmarks-comparisons">Benchmarks &amp; comparisons</h3>
<p>For a dataset of <strong>10,000 objects</strong>:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Time</th>
<th>Memory usage / footprint</th>
</tr>
</thead>
<tbody>
<tr>
<td>Serializer (traditional)</td>
<td>~204 ms</td>
<td>~16 MB (grows with size)</td>
</tr>
<tr>
<td>JsonStreamer</td>
<td>~87 ms</td>
<td>~8 MB (much more constant)</td>
</tr>
</tbody>
</table>
<h3 id="challenges-with-metadata-json-ld-and-how-api-platform-adapts">Challenges with metadata, JSON-LD and how API Platform adapts</h3>
<p>API Platform adds <strong>metadata</strong>, JSON-LD contexts, and property metadata. That overhead makes serialization heavier. To integrate JsonStreamer while keeping these features:</p>
<ul>
<li>Use <strong>PropertyMetadataLoader</strong> extension points to provide metadata to JsonStreamer. This tells it which properties to expose, without traversing the full object tree.</li>
<li>Use <strong>ValueTransformers</strong> to adjust values at runtime. But beware: heavy logic here will degrade performance, since transformers run for every value.</li>
<li>Use <strong>ObjectMapper</strong> to convert entities (e.g., Doctrine objects) into POPOs (DTOs) that are easier to stream.</li>
</ul>
<h3 id="esa-edge-side-apis-pattern">ESA (Edge Side APIs) pattern</h3>
<p><strong>Edge Side APIs (ESA)</strong> 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.</p>
<p>In practice:</p>
<ul>
<li>Instead of sending one huge JSON structure, partition or paginate so the client starts receiving data earlier.</li>
<li>Combine with streaming so that parts of the response arrive incrementally, improving time-to-first-byte.</li>
<li>The user experience is better: data appears quickly instead of waiting for everything.</li>
</ul>
<h3 id="takeaways">Takeaways</h3>
<ul>
<li>Symfony’s Serializer is fine for small to medium datasets.</li>
<li>JsonStreamer provides <strong>significant improvements</strong> in memory usage and TTFB.</li>
<li>API Platform integrates it through extension points (PropertyMetadataLoader, ValueTransformers, ObjectMapper).</li>
<li>Avoid heavy runtime transformations for best performance.</li>
<li>Design your API with these options in mind early—serialization decisions are very difficult to change later.</li>
</ul>
<h2 id="credits">Credits</h2>
<p>Cover image by <a href="https://ncls.tv/" rel="noopener noreferrer">Nicolas Detrez</a></p>]]></description>
    </item>
    <item>
      <guid>https://ktherage.github.io/blog/api-platform-con-2025-day-2/</guid>
      <title>API Platform con 2025 - DAY 2</title>
      <pubDate>2025-09-23T00:00:00+00:00</pubDate>
      <link href="https://ktherage.github.io/blog/api-platform-con-2025-day-2/" rel="alternate" type="text/html" />
      <description><![CDATA[<p>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.</p>
<p>Table of contents :</p>
<div id="toc"><ul>
<li><a href="#how-llms-are-changing-the-way-we-should-build-apis-fabien-potentier">How LLMs are changing the way we should build APIs (Fabien Potentier)</a><ul>
<li><a href="#agents">Agents ?</a></li>
<li><a href="#who-can-consume-your-app">Who can consume your app ?</a></li>
<li><a href="#the-challenge-apis-for-humans-vs-machines-vs-ai-agents">The Challenge: APIs for Humans vs. Machines vs. AI Agents</a></li>
<li><a href="#best-practices-for-llm-friendly-apis">Best Practices for LLM-Friendly APIs</a></li>
<li><a href="#testing-challenges">Testing Challenges</a></li>
<li><a href="#technical-considerations">Technical Considerations</a></li>
<li><a href="#log-everything">Log Everything</a></li>
<li><a href="#the-new-experience-ax-ai-experience">The New Experience: AX (AI Experience)</a></li>
</ul>
</li>
<li><a href="#build-a-decoupled-application-with-api-platform-and-vue-js-nathan-de-pachtere">Build a decoupled application with API Platform and Vue.js (Nathan de Pachtere)</a><ul>
<li><a href="#headless">Headless</a></li>
<li><a href="#decoupled">Decoupled</a></li>
<li><a href="#why-choose-this-approach">Why Choose This Approach?</a></li>
<li><a href="#headless-implementation">Headless Implementation</a></li>
<li><a href="#decoupled-implementation">Decoupled Implementation</a></li>
<li><a href="#version-management">Version Management</a></li>
</ul>
</li>
<li><a href="#jean-beru-presents-fun-with-flags-hubert-lenoir">Jean-Beru presents: Fun with flags (Hubert Lenoir)</a><ul>
<li><a href="#what-are-feature-flags">What are Feature Flags?</a></li>
<li><a href="#types-of-feature-flags">Types of Feature Flags</a></li>
<li><a href="#implementation">Implementation</a></li>
<li><a href="#with-api-platform">With API Platform</a></li>
<li><a href="#advantages-1">Advantages</a></li>
</ul>
</li>
<li><a href="#pie-the-next-big-thing-alexandre-daubois">PIE : The next Big Thing (Alexandre Daubois)</a><ul>
<li><a href="#extensions">Extensions ?</a></li>
<li><a href="#installing-a-third-party-lib">Installing a third-party lib</a></li>
<li><a href="#pecl">PECL</a></li>
<li><a href="#docker-php-extension-installer">docker-php-extension-installer</a></li>
<li><a href="#project-to-replace-pecl">Project to replace PECL</a></li>
<li><a href="#the-future-of-extensions">The future of extensions</a></li>
</ul>
</li>
<li><a href="#make-your-devs-happy-by-normalizing-your-api-errors-clement-herreman">Make your devs happy by normalizing your API errors (Clément Herreman)</a><ul>
<li><a href="#what-is-an-error">What is an error?</a></li>
<li><a href="#why-normalize-errors">Why normalize errors?</a></li>
<li><a href="#how">How?</a></li>
</ul>
</li>
<li><a href="#symfony-and-dependency-injection-from-past-to-future-imen-ezzine">Symfony and Dependency Injection: From past to future (Imen Ezzine)</a><ul>
<li><a href="#the-early-days">The early days</a></li>
<li><a href="#symfony-2-and-the-paradigm-shift">Symfony 2 and the paradigm shift</a></li>
<li><a href="#symfony-5-to-symfony-7">Symfony 5 to Symfony 7</a></li>
<li><a href="#takeaways">Takeaways</a></li>
</ul>
</li>
<li><a href="#credits">Credits</a></li>
</ul></div>
<hr>
<h2 id="how-llms-are-changing-the-way-we-should-build-apis-fabien-potentier">How LLMs are changing the way we should build APIs (Fabien Potentier)</h2>
<p><strong>Slides of this talk are available : <a href="https://speakerdeck.com/fabpot/how-ai-agents-are-changing-the-way-we-should-build-apis" rel="noopener noreferrer">https://speakerdeck.com/fabpot/how-ai-agents-are-changing-the-way-we-should-build-apis</a></strong></p>
<p>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.</p>
<h3 id="agents">Agents ?</h3>
<p>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.</p>
<p>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.</p>
<h3 id="who-can-consume-your-app">Who can consume your app ?</h3>
<p>Back in the days, the consumers were clearly defined:</p>
<p><strong>Website:</strong></p>
<ul>
<li>Human users only</li>
</ul>
<p><strong>CLI tools:</strong></p>
<ul>
<li>Only for developers</li>
</ul>
<p><strong>API:</strong></p>
<ul>
<li>Only for machines</li>
<li>Semi-private (to decouple frontend) or public</li>
</ul>
<p>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:</p>
<ul>
<li><strong>Websites can be scraped by AI</strong> - agents can navigate and extract information from web interfaces</li>
<li><strong>CLI tools can be used through MCP servers</strong> - providing structured tool access</li>
<li><strong>APIs</strong> - LLMs (e.g., in chatbots) are often wrappers on top of APIs. Furthermore, LLMs can also write API calls directly.</li>
</ul>
<p>But all three have different expectations, and this creates new challenges.</p>
<h3 id="the-challenge-apis-for-humans-vs-machines-vs-ai-agents">The Challenge: APIs for Humans vs. Machines vs. AI Agents</h3>
<p>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.</p>
<p>Take HTTP status codes as an example. They provide information about problems, but AI agents need more context.
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.</p>
<p>Here is a common workflow pattern followed by LLM : Thought → Action → Observation.
Without guidance provided via prompts, it can loop over the same problem, encountering the same Observation after performing the same Action—potentially forever.
LLMs will try to guess and self-correct, which is probably bad for two reasons:</p>
<ul>
<li><strong>Costly</strong> - more API calls and processing</li>
<li><strong>Time loss</strong> - inefficient problem resolution</li>
<li><strong>Resource greedy</strong> - GPU time and electricity are consumed without solving the problem</li>
</ul>
<p><strong>Tip:</strong> The fewer round trips you have with an LLM, the more "deterministic" it becomes, even though LLMs are inherently not deterministic.</p>
<h3 id="best-practices-for-llm-friendly-apis">Best Practices for LLM-Friendly APIs</h3>
<p>Everything that is valid for LLMs is also valid for humans.</p>
<h4 id="error-messages">Error Messages</h4>
<p>Be precise with your error messages: "Bad date format. Use 'YYYY-MM-DD'."
Benefits:</p>
<ul>
<li>Fewer tokens consumed</li>
<li>Smaller context window usage</li>
<li>Faster resolution</li>
</ul>
<h4 id="consistent-naming">Consistent Naming</h4>
<p>Use the same naming pattern everywhere. For example, use <code>user_id</code> consistently across all endpoints.
Benefits:</p>
<ul>
<li>Predictable patterns</li>
<li>LLMs like consistency</li>
<li>Easier to understand and use</li>
</ul>
<h4 id="documentation">Documentation</h4>
<ul>
<li>Fix examples and remove outdated content</li>
<li>Fewer problems and hallucinations</li>
<li>Consider using <code>llms.txt</code> files - documentation specifically formatted for LLMs in Markdown</li>
</ul>
<h4 id="performance-considerations">Performance Considerations</h4>
<p>AI agents are slow, so reducing the number of requests provides a significant performance boost.</p>
<h4 id="intent-first-api-design">Intent-First API Design</h4>
<p>Design your APIs to capture and preserve user intent rather than just exposing CRUD operations.</p>
<h3 id="testing-challenges">Testing Challenges</h3>
<p>Testing AI agents is super difficult because:</p>
<ul>
<li>LLMs are not deterministic</li>
<li>You need to set temperature to 0 for more consistent results</li>
<li>Use concise prompts</li>
<li>Ultimately, you need a human to judge the quality of actions performed by the LLM, making automated testing complex</li>
</ul>
<h3 id="technical-considerations">Technical Considerations</h3>
<h4 id="tokens-vs-text">Tokens vs. Text</h4>
<p>Understanding tokenization is crucial. Tools like <a href="https://tiktokenizer.vercel.app" rel="noopener noreferrer">tiktokenizer.vercel.app</a> help visualize how text is tokenized:</p>
<ul>
<li><strong>Language matters:</strong> English costs less in tokens than French or Japanese for example</li>
<li><strong>Unique IDs are problematic:</strong> UUIDs are bad for tokenizers, ULIDs are better</li>
<li><strong>Shorter is not always better</strong> in terms of token efficiency</li>
<li><strong>Date formats matter</strong> for token consumption</li>
<li><strong>JSON is not the best format</strong> for LLMs - Markdown is better and uses fewer tokens</li>
</ul>
<p>More tokens require more money and create larger context windows, which negatively impact AI agent response times and relevance.</p>
<h4 id="security-and-credentials">Security and Credentials</h4>
<p>AI agents are bad at dealing with credentials. The solution is to use MCP (Model Context Protocol) servers that:</p>
<ul>
<li>Handle credentials securely</li>
<li>Provide tools to AI agents</li>
<li>Give limited scope permissions to MCP actions</li>
<li>Act as a secure intermediary between the LLM and your APIs</li>
</ul>
<h3 id="log-everything">Log Everything</h3>
<p>Given the complexity and unpredictability of AI agent interactions, comprehensive logging becomes essential for debugging and improving the system.</p>
<h3 id="the-new-experience-ax-ai-experience">The New Experience: AX (AI Experience)</h3>
<p>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.</p>
<p>Key aspects of good AX include:</p>
<ul>
<li>Up-to-date documentation and examples (avoiding outdated examples that could mislead the LLM)</li>
<li>Using <code>llms.txt</code> files with all useful documentation for the LLM in Markdown format</li>
<li>Clear, consistent error messages</li>
<li>Intent-preserving API design</li>
<li>Efficient token usage</li>
</ul>
<p>The fascinating aspect is that many improvements for AX also benefit traditional DX, making APIs better for both human developers and AI agents.</p>
<hr>
<h2 id="build-a-decoupled-application-with-api-platform-and-vue-js-nathan-de-pachtere">Build a decoupled application with API Platform and Vue.js (Nathan de Pachtere)</h2>
<p>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.</p>
<h3 id="headless">Headless</h3>
<p>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.</p>
<p>The goal is to create business logic and provide an API that everyone can utilize for their own purposes.</p>
<h3 id="decoupled">Decoupled</h3>
<p>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.</p>
<h3 id="why-choose-this-approach">Why Choose This Approach?</h3>
<h4 id="advantages">Advantages</h4>
<ul>
<li><strong>Separation of responsibilities</strong> - Clear boundaries between frontend and backend concerns</li>
<li><strong>Team management</strong> - Enables specialist teams to work independently on their expertise areas</li>
<li><strong>Capitalization</strong> - Reusable components and logic across different projects</li>
<li><strong>Future-proofing</strong> - AI might be the interface used in the future, making an API-first approach valuable</li>
</ul>
<h4 id="disadvantages">Disadvantages</h4>
<ul>
<li><strong>Complexity</strong> - More complex setup for existing projects that need to be refactored</li>
</ul>
<h3 id="headless-implementation">Headless Implementation</h3>
<h4 id="using-api-platform">using API Platform</h4>
<p>The process follows a business-driven approach:</p>
<ol>
<li><strong>Represent the API based on business needs</strong> - Focus on what the business actually does</li>
<li><strong>Translate into entities and workflows</strong> - Convert business processes into technical implementations</li>
<li><strong>Write only the necessary code</strong> - Keep it simple initially</li>
<li><strong>Then optimize and refactor</strong> - Improve performance and code quality</li>
<li><strong>Iterate</strong> - Continuously improve based on feedback</li>
<li><strong>Go beyond CRUD</strong> - Implement meaningful business operations, not just basic data manipulation</li>
</ol>
<h4 id="providing-api-keys">Providing API Keys</h4>
<p>For machine-to-machine authentication:</p>
<ul>
<li><strong>Create a simple interface</strong> for creating/deleting configurable keys with specific permissions</li>
<li><strong>Consider external identity providers</strong> like Keycloak or Zitadel for more advanced use cases</li>
<li><strong>Important principle:</strong> Don't mix human users with machine users - they have different needs and security requirements</li>
</ul>
<p>Nathan emphasized making tests simple and easy to implement, integrating them naturally into the development workflow rather than treating them as an afterthought.</p>
<h4 id="deprecation-strategy">Deprecation Strategy</h4>
<p>When evolving your API:</p>
<ul>
<li><strong>Deprecate endpoints, resources, and properties</strong> gradually</li>
<li><strong>Give consumers time to adapt</strong> to changes</li>
<li><strong>Communicate changes clearly</strong> before removing functionality</li>
</ul>
<p>This approach maintains backward compatibility while allowing the API to evolve.</p>
<h3 id="decoupled-implementation">Decoupled Implementation</h3>
<h4 id="using-vue-js">using Vue.js</h4>
<p>Nathan chose Vue.js for several reasons:</p>
<ul>
<li><strong>Independent and community-driven</strong> - Not controlled by a single corporation</li>
<li><strong>Composition API (Vue 3)</strong> - Promotes code reusability and better organization</li>
<li><strong>Excellent Developer Experience</strong> - Great tooling and development workflow</li>
<li><strong>Top performance</strong> - Fast and efficient (until the next framework comes along, as he joked)</li>
</ul>
<h4 id="api-connection">API Connection</h4>
<p>For connecting the Vue.js frontend to the API Platform backend:</p>
<h5 id="code-generation">Code Generation</h5>
<p>Use <strong>openapi-ts.dev</strong> to generate TypeScript types and composables from your OpenAPI specification. This ensures type safety and reduces manual work.</p>
<p><strong>Important principle:</strong> 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.</p>
<h5 id="http-client-and-state-management">HTTP Client and State Management</h5>
<ul>
<li><strong>Tanstack Query</strong> - For efficient data fetching and caching</li>
<li><strong>TypeScript throughout</strong> - Ensures type safety across the application</li>
<li><strong>VS Code for Vue.js development</strong> - Better integration compared to JetBrains IDEs for Vue.js work</li>
</ul>
<h5 id="high-level-sdks">High-Level SDKs</h5>
<p>Provide high-level SDKs to facilitate API integration, making it easier for developers to work with your API.</p>
<h3 id="version-management">Version Management</h3>
<h4 id="polyrepo-vs-monorepo">Polyrepo vs Monorepo</h4>
<p><strong>Monorepo doesn't mean monolith</strong> - this is a crucial distinction:</p>
<ul>
<li><strong>Monorepo</strong> = Multiple separate projects in a single repository</li>
<li><strong>Monolith</strong> = Single application handling everything</li>
</ul>
<h4 id="monorepo-benefits">Monorepo Benefits</h4>
<p>The goal is to simplify the workflow:</p>
<ul>
<li><strong>Unified way of thinking about code</strong> - Consistent patterns across projects</li>
<li><strong>Consistency</strong> - Shared tooling and configurations</li>
<li><strong>Facilitates sharing</strong> - Easy code and component reuse</li>
<li><strong>More efficient teamwork</strong> - Simplified collaboration and dependency management</li>
</ul>
<h4 id="tooling">Tooling</h4>
<p>Nathan recommended <strong>moonrepo.dev</strong> as an open-source tool for managing monorepos. You can find more information at <strong>monorepo.tools</strong>.</p>
<h4 id="real-world-example">Real-World Example</h4>
<p>Nathan shared their experience with enormous benefits:</p>
<ul>
<li><strong>Code generalization</strong> - Reusable patterns and components</li>
<li><strong>Functionality sharing</strong> - Common libraries across projects</li>
<li><strong>Technology-based organization</strong> - Projects use shared libraries organized by technology stack</li>
</ul>
<p>You can see a practical example of this approach in the <a href="https://github.com/alpsify/lychen" rel="noopener noreferrer">Lychen project</a> (<a href="https://lychen.fr/" rel="noopener noreferrer">lychen.fr</a>), which demonstrates a well-structured monorepo with clear separation between backend, frontend, and shared tools.</p>
<p>The Lychen project shows how to organize a monorepo with:</p>
<ul>
<li><strong>Backend</strong> (API Platform/PHP)</li>
<li><strong>Frontend</strong> (Vue.js/TypeScript)</li>
<li><strong>Shared tooling</strong> (Moonrepo, Docker, testing tools)</li>
<li><strong>Clear technology boundaries</strong> while maintaining efficient code sharing</li>
</ul>
<hr>
<h2 id="jean-beru-presents-fun-with-flags-hubert-lenoir">Jean-Beru presents: Fun with flags (Hubert Lenoir)</h2>
<p><strong>Slides of this talk are available : <a href="https://jean-beru.github.io/2025_09_apiplatformcon_fun_with_flags" rel="noopener noreferrer">https://jean-beru.github.io/2025_09_apiplatformcon_fun_with_flags</a></strong></p>
<p>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.</p>
<h3 id="what-are-feature-flags">What are Feature Flags?</h3>
<p>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.</p>
<h3 id="types-of-feature-flags">Types of Feature Flags</h3>
<h4 id="release-flags">Release Flags</h4>
<p>Mainly used to test new features in production environments safely.</p>
<ul>
<li><strong>Continuous development</strong> - Even if a feature is not ready, you can continue developing and deploying (not ready = disabled)</li>
<li><strong>Safe deployment</strong> - Deploy code with features turned off, then enable them when ready</li>
<li><strong>Gradual rollout</strong> - Enable features for small groups before full release</li>
</ul>
<h4 id="experiment-flags">Experiment Flags</h4>
<p>Used to compare different versions of your application.</p>
<ul>
<li><strong>A/B testing</strong> - Compare different implementations or user experiences</li>
<li><strong>Must be followed by metrics</strong> - Track performance and user behavior</li>
<li><strong>Partial enablement</strong> - Enable for specific percentages (e.g., 20% of users)</li>
</ul>
<p>This approach allows data-driven decisions about which features or implementations work best for your users.</p>
<h4 id="permission-flags">Permission Flags</h4>
<p>Control access to features based on user permissions or subscription levels.</p>
<ul>
<li><strong>Blocking access based on permissions</strong> - For example, paid features only available to premium subscribers</li>
<li><strong>Role-based feature access</strong> - Different features for different user types</li>
<li><strong>Subscription tiers</strong> - Enable advanced features for higher-tier customers</li>
</ul>
<h4 id="operational-flags">Operational Flags</h4>
<p>Security belt and kill switch functionality.</p>
<ul>
<li><strong>Allow disabling cumbersome features</strong> - Quickly turn off resource-intensive features during high load</li>
<li><strong>Emergency response</strong> - Disable problematic features without deployment</li>
<li><strong>Performance management</strong> - Control system load by toggling expensive operations</li>
</ul>
<p>For more detailed information about feature flag patterns, Martin Fowler has an excellent article at <a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer">https://martinfowler.com/articles/feature-toggles.html</a>.</p>
<h3 id="implementation">Implementation</h3>
<p>There are many feature flag providers available in the market, but the implementation doesn't necessarily need to use Symfony's Security component.</p>
<h4 id="why-not-security-component">Why Not Security Component?</h4>
<ul>
<li><strong>Restricted to current user context</strong> - Limitations when flags need to work across different user contexts</li>
<li><strong>Authentication timing issues</strong> - Authentication happens after routing, which can lead to unwanted forbidden error codes</li>
<li><strong>Flexibility needs</strong> - Custom implementations can better integrate with existing providers like Unleash</li>
</ul>
<h4 id="requirements-for-a-good-implementation">Requirements for a Good Implementation</h4>
<p>A solid feature flag system should provide:</p>
<ul>
<li><strong>Simplicity</strong> - Easy to implement and use</li>
<li><strong>Integrated debugging</strong> - Clear visibility into which flags are active</li>
<li><strong>Multiple sources</strong> - Ability to switch between different flag providers</li>
<li><strong>Various provider support</strong> - Work with different feature flag services</li>
<li><strong>Cacheable</strong> - Performance optimization through caching mechanisms</li>
</ul>
<h4 id="symfony-integration">Symfony Integration</h4>
<p>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.</p>
<h3 id="with-api-platform">With API Platform</h3>
<p>Feature flags can be easily tested via a separated bundle: <a href="https://github.com/ajgarlag/feature-flag-bundle" rel="noopener noreferrer">ajgarlag/feature-flag-bundle</a>.</p>
<h4 id="implementation-steps">Implementation Steps</h4>
<ol>
<li><strong>Decoration of API Platform provider</strong> - Use the decorator pattern to wrap existing providers with feature flag logic</li>
<li><strong>Use FeatureFlag WIP component interface</strong> - Integrate with the upcoming Symfony FeatureFlag component</li>
</ol>
<h4 id="example-with-gitlab-provider">Example with GitLab Provider</h4>
<p>GitLab provides a feature flag service that uses Unleash in the background. This integration allows you to:</p>
<ul>
<li><strong>Manage flags through GitLab UI</strong> - Familiar interface for teams already using GitLab</li>
<li><strong>Leverage Unleash capabilities</strong> - Powerful feature flag engine under the hood</li>
<li><strong>Integrate with CI/CD pipelines</strong> - Automatic flag management as part of deployment process</li>
</ul>
<h4 id="profiler-integration">Profiler Integration</h4>
<p>The implementation includes Symfony Profiler integration, providing:</p>
<ul>
<li><strong>Debug information</strong> - See which flags are active during development</li>
<li><strong>Performance insights</strong> - Monitor the impact of feature flag checks</li>
<li><strong>Development workflow</strong> - Easy testing and debugging of flag behavior</li>
</ul>
<h3 id="advantages-1">Advantages</h3>
<p>Implementing feature flags brings several significant benefits:</p>
<h4 id="deploy-continuously">Deploy Continuously</h4>
<ul>
<li><strong>Decouple deployment from release</strong> - Deploy code safely with features disabled</li>
<li><strong>Reduce deployment risk</strong> - Lower chance of breaking production</li>
<li><strong>Faster iteration cycles</strong> - More frequent, smaller deployments</li>
</ul>
<h4 id="progressive-testing">Progressive Testing</h4>
<ul>
<li><strong>A/B testing capabilities</strong> - Compare different approaches with real users</li>
<li><strong>Gradual rollouts</strong> - Start with small user groups and expand</li>
<li><strong>Data-driven decisions</strong> - Make choices based on actual usage metrics</li>
</ul>
<h4 id="quick-turn-off">Quick Turn Off</h4>
<ul>
<li><strong>No redeployment needed</strong> - Instantly disable problematic features</li>
<li><strong>Emergency response</strong> - Rapid reaction to production issues</li>
<li><strong>Business continuity</strong> - Keep core functionality working while fixing problems</li>
</ul>
<h4 id="separate-code-from-feature-release">Separate Code from Feature Release</h4>
<ul>
<li><strong>Independent timelines</strong> - Development and business release schedules can differ</li>
<li><strong>Marketing coordination</strong> - Align feature releases with marketing campaigns</li>
<li><strong>Stakeholder management</strong> - Give business teams control over when features go live</li>
</ul>
<p>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.</p>
<hr>
<h2 id="pie-the-next-big-thing-alexandre-daubois">PIE : The next Big Thing (Alexandre Daubois)</h2>
<h3 id="extensions">Extensions ?</h3>
<p>Extensions are like composer packages, but written in C, C++, Rust, and now Go.<br>
They live at a lower level, which makes them much faster than pure PHP code.</p>
<p>Frameworks like <strong>Phalcon</strong> are themselves shipped as extensions.</p>
<h3 id="installing-a-third-party-lib">Installing a third-party lib</h3>
<p>Traditionally, installing an extension is much more painful than a <code>composer install</code>.<br>
It usually involves:</p>
<ol>
<li>Downloading the source code.</li>
<li>Compiling it with <code>phpize</code> and <code>make</code>.</li>
<li>Adding a line to <code>php.ini</code> to enable it.</li>
<li>Restarting PHP-FPM or Apache to load it.</li>
</ol>
<p>This workflow makes extensions harder to distribute and standardize compared to Composer packages.</p>
<h3 id="pecl">PECL</h3>
<ul>
<li>Clunky and outdated.</li>
<li>Slow to install.</li>
<li>Lacks proper security (no package signing).</li>
<li>Not officially backed by PHP, and some in the community want to phase it out.</li>
</ul>
<h3 id="docker-php-extension-installer">docker-php-extension-installer</h3>
<p>A widely used community project that simplifies extension installation inside Docker images.<br>
Instead of writing complex <code>apt-get</code> + <code>phpize</code> + <code>make</code> commands, you just add:</p>
<pre><code class="language-dockerfile hljs dockerfile"><span class="hljs-keyword">COPY</span><span class="bash"> --from=ghcr.io/mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/<span class="hljs-built_in">local</span>/bin/</span>

<span class="hljs-keyword">RUN</span><span class="bash"> install-php-extensions xdebug redis</span></code></pre>
<p>This is great, but still not perfect—it remains Docker-specific and doesn’t integrate with Composer or Packagist.</p>
<h3 id="project-to-replace-pecl">Project to replace PECL</h3>
<p>The <strong>pie-design</strong> repository defines the foundations of <strong>PIE</strong>, a new way to install extensions as easily as PHP packages.</p>
<ul>
<li>Started in March 2024.</li>
<li>Version 1 released in June 2025.</li>
<li>PIE is distributed as a single <code>phar</code> file: just download it and use it.</li>
<li>All extension metadata is stored in Packagist.</li>
</ul>
<h4 id="command-options">Command options</h4>
<ul>
<li><code>pie install ext-xdebug</code> → installs an extension and updates <code>php.ini</code>.</li>
<li><code>pie uninstall ext-redis</code> → removes an extension.</li>
<li><code>pie update</code> → upgrades to the latest available version.</li>
<li><code>pie search redis</code> → searches for extensions in Packagist.</li>
<li>Running <code>pie</code> without arguments reads extensions from <code>composer.json</code> and installs them.</li>
</ul>
<p>Other features:</p>
<ul>
<li>Add repositories via Composer, VCS, or local paths.</li>
<li>Automatic <code>php.ini</code> update.</li>
<li>Support for <code>GH_TOKEN</code> to install from private repositories.</li>
<li>OS compatibility restrictions.</li>
<li>Symfony CLI integration: <code>symfony pie install</code>.</li>
</ul>
<h3 id="the-future-of-extensions">The future of extensions</h3>
<p>PIE is the theoretical replacement for PECL.<br>
An RFC vote was held, closing on <strong>September 20, 2025</strong>.<br>
Almost everyone voted <em>yes</em>, which means PIE is now the official successor to PECL.</p>
<hr>
<h2 id="make-your-devs-happy-by-normalizing-your-api-errors-clement-herreman">Make your devs happy by normalizing your API errors (Clément Herreman)</h2>
<p>Errors are not just bugs. They’re an opportunity to give users autonomy through clear feedback.</p>
<h3 id="what-is-an-error">What is an error?</h3>
<p>An error is any behavior—intentional or not—that prevents the user from completing their task.</p>
<h3 id="why-normalize-errors">Why normalize errors?</h3>
<ol>
<li>
<p>To react properly to a precise issue:</p>
<ul>
<li>Retrying a token.</li>
<li>Handling distributed system failures.</li>
<li>Fixing configuration issues.</li>
</ul>
</li>
<li>
<p>To present errors consistently:</p>
<ul>
<li>Clear and understandable messages for end-users.</li>
<li>Precise identification to ease support.</li>
<li>Keeping some details vague for security reasons.</li>
</ul>
</li>
</ol>
<h3 id="how">How?</h3>
<p>Errors can be classified into three categories:</p>
<ol>
<li>Errors that belong to your domain: you own them, so enrich them with context.</li>
<li>Errors that don’t belong to your domain but still happen: wrap them with a code and enrich them.</li>
<li>Rare/unexpected errors: keep the default JSON output.</li>
</ol>
<h4 id="rfc-7807-problem-details-for-http-apis">RFC 7807: Problem Details for HTTP APIs</h4>
<p>This RFC defines a standard JSON structure for errors:</p>
<ul>
<li><code>type</code>: unique machine-readable code.</li>
<li><code>title</code>: short, human-readable summary.</li>
<li><code>detail</code>: contextual explanation of this particular error.</li>
<li><code>instance</code>: URL to the error catalog.</li>
<li><code>...</code>: any custom fields you want.</li>
</ul>
<h5 id="example-http-response">Example HTTP response</h5>
<pre><code class="language-http hljs http">HTTP/1.1 <span class="hljs-number">401</span> Unauthorized
<span class="hljs-attribute">Content-Type</span>: application/problem+json

{
  "type": "https://example.com/errors/authentication_failed",
  "title": "Authentication failed",
  "detail": "Your token has expired. Please request a new one.",
  "instance": "/login"
}</code></pre>
<h4 id="api-platform">API Platform</h4>
<p>API Platform provides a ready-to-use <code>ApiPlatform\Problem\Error</code> class to implement RFC 7807.</p>
<h4 id="organizing-errors">Organizing errors</h4>
<ul>
<li>Keep only business exceptions in the domain layer.</li>
<li>Wrap infrastructure errors before sending them to the client.</li>
</ul>
<h4 id="documenting-errors">Documenting errors</h4>
<p>Errors can be declared as attributes on operations, making them explicit in the API docs.</p>
<h4 id="improvements-rfc-9457">Improvements: RFC 9457</h4>
<p>RFC 9457 is essentially the same as RFC 7807, with some additions:</p>
<ul>
<li>A registry of errors via <code>schema.org</code>.</li>
<li>A mechanism for returning multiple errors at once (though strongly discouraged).</li>
</ul>
<p>As Clément highlighted: RFC 9457 doesn’t bring much practical value, and some of its suggestions are even discouraged in the spec.</p>
<hr>
<h2 id="symfony-and-dependency-injection-from-past-to-future-imen-ezzine">Symfony and Dependency Injection: From past to future (Imen Ezzine)</h2>
<p>Dependency Injection (DI) is the “D” in SOLID, and it has been a cornerstone of Symfony’s design for nearly two decades.<br>
This talk explored its history, evolution, and what’s next.</p>
<h3 id="the-early-days">The early days</h3>
<p><strong>2007 – Symfony 1</strong></p>
<ul>
<li>Services instantiated directly, often via <code>sfContext()</code> (a singleton).</li>
<li>Hard to test, rigid, tightly coupled.</li>
<li>No real container.</li>
</ul>
<h3 id="symfony-2-and-the-paradigm-shift">Symfony 2 and the paradigm shift</h3>
<p><strong>2011 – Symfony 2</strong></p>
<ul>
<li>Introduction of a central container.</li>
<li>Services configured via YAML and parameters.</li>
<li>Dependencies injected as constructor arguments.</li>
<li>Autowiring introduced in <strong>Symfony 2.8</strong>.</li>
</ul>
<p><strong>2015 – API Platform v1</strong></p>
<ul>
<li>Heavy reliance on autowiring (then experimental).</li>
</ul>
<p><strong>2016 – API Platform v2</strong></p>
<ul>
<li><code>@ApiResource</code> annotation magic powered by the DI component.</li>
<li>Data persisters and providers had to be tagged manually.</li>
</ul>
<p><strong>2017 – Symfony 3.3 / API Platform 2.2</strong></p>
<ul>
<li>Autowiring + autoconfigure.</li>
<li>Manual tagging mostly eliminated (providers/persisters automatically wired).</li>
<li>Symfony 3.4: services private by default.</li>
</ul>
<h3 id="symfony-5-to-symfony-7">Symfony 5 to Symfony 7</h3>
<p><strong>2021 – Symfony 5.3</strong></p>
<ul>
<li>DI powered by attributes → much less YAML.</li>
<li><code>#[When]</code> attribute for conditional services.</li>
</ul>
<p><strong>Symfony 6.0 – 6.3</strong></p>
<ul>
<li>New attributes for corner cases.</li>
<li><code>#[Autowire]</code> attribute for precise service injection.</li>
<li>Support for env vars and parameters via attributes.</li>
<li><code>#[AsAlias]</code> to alias services.</li>
</ul>
<p><strong>2022 – API Platform 3.0</strong></p>
<ul>
<li>New state processors and providers replace older persister/provider pattern.</li>
</ul>
<p><strong>2023 – Symfony 7</strong></p>
<ul>
<li><code>#[AutoconfigureTag]</code> → automatic tagging (used in API Platform filters).</li>
<li><code>TaggedIterator</code> → inject multiple tagged services.</li>
<li><code>AutowireIterator</code> → autowire all classes implementing an interface.</li>
</ul>
<p><strong>Symfony 7.1 – 7.3</strong></p>
<ul>
<li><code>#[AutowireMethodOf]</code> to autowire a single method.</li>
<li><code>#[WhenNot]</code> for conditional services.</li>
<li><code>when</code> parameter in <code>#[AsAlias]</code>.</li>
</ul>
<h3 id="takeaways">Takeaways</h3>
<p>Over 20 years, DI in Symfony evolved from:</p>
<ul>
<li>Manual instantiation →</li>
<li>Manual configuration →</li>
<li>Automatic configuration through <strong>attributes</strong>.</li>
</ul>
<p>This journey has made Symfony projects <strong>more testable, maintainable, and developer-friendly</strong> while reducing boilerplate.</p>
<h2 id="credits">Credits</h2>
<p>Cover image by <a href="https://ncls.tv/" rel="noopener noreferrer">Nicolas Detrez</a></p>]]></description>
    </item>
    <item>
      <guid>https://ktherage.github.io/blog/symfony-and-doctrine-migrations-validation-in-ci/</guid>
      <title>Symfony &amp; Doctrine Migrations: Validation in CI</title>
      <pubDate>2024-09-05T00:00:00+00:00</pubDate>
      <link href="https://ktherage.github.io/blog/symfony-and-doctrine-migrations-validation-in-ci/" rel="alternate" type="text/html" />
      <description><![CDATA[<p>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.</p>
<p>Here is how I did it. I hope you'll enjoy it!</p>
<h2 id="摩獣污業敲">Disclaimer</h2>
<p>We are the 10th of July 2025 now and this article is outdated due to the merge of the Pull Request <a href="https://github.com/doctrine/migrations/issues/1406" rel="noopener noreferrer">https://github.com/doctrine/migrations/issues/1406</a> and so running <code>bin/console doctrine:migrations:up-to-date</code> shall now take care of <code>schema_filter</code> configuration.</p>
<h2 id="how-doctrine-migrations-works">How Doctrine Migrations works</h2>
<p>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 :wink:) it will generate a <strong>migration file</strong> with two main methods :</p>
<ul>
<li><code>up</code> 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.</li>
<li><code>down</code> 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.</li>
</ul>
<h2 id="the-magic-trick">The magic trick</h2>
<p>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.</p>
<p>The keywords in the above description are <strong>migration files</strong>. 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.</p>
<p>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 <strong>migration file</strong> in this pull (or merge) request.</p>
<h2 id="steps-to-do">Steps to do</h2>
<ol>
<li>Create your database</li>
<li>Run your existing migrations</li>
<li>Then run the step to check for missing changes (see below)</li>
</ol>
<h2 id="advantages">Advantages</h2>
<ol>
<li>Testing that your migrations does not fail</li>
<li>Ensure database schema consistency with Doctrine's mapping</li>
</ol>
<h2 id="you-want-the-code-snippet-right">You want the code snippet right!?</h2>
<p>Here is the bash code :</p>
<pre><code class="language-bash hljs bash"><span class="hljs-meta">#!/bin/bash
</span>
<span class="hljs-built_in">set</span> -e
<span class="hljs-built_in">set</span> -o pipefail

<span class="hljs-comment"># run doctrine migration diff to check if there is a new migration file generated and check last exit code</span>
<span class="hljs-keyword">if</span> [[ -z $(bin/console doctrine:migrations:diff -n --quiet) ]]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Error ! bin/console doctrine:migration:diff found a new migration which must not be the case."</span>;
    <span class="hljs-comment"># cat last file (should be the newly generated one)</span>
    cat $(ls -Art migrations/*.php | tail -n 1);
    <span class="hljs-comment"># remove that file (just in case to comply with my paranoïac side)</span>
    rm -f $(ls -Art migrations/*.php | tail -n 1);
    <span class="hljs-built_in">exit</span> 1;
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">exit</span> 0;
<span class="hljs-keyword">fi</span>
</code></pre>
<p>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!</p>
<h2 id="ok-but-why-not-use-bin-console-doctrine-schema-validate">Ok but why not use <code>bin/console doctrine:schema:validate</code>?</h2>
<p>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).</p>
<p>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.</p>
<p>A colleague told me that this is a known issue that might be fixed soon (<a href="https://github.com/doctrine/migrations/issues/1406" rel="noopener noreferrer">https://github.com/doctrine/migrations/issues/1406</a>).</p>
<p>Thank you for reading this article and please leave your comments if you have any questions!</p>]]></description>
    </item>
    <item>
      <guid>https://ktherage.github.io/blog/bash-and-curl-simple-http-call-performance-testing/</guid>
      <title>Bash &amp; Curl: Simple HTTP call performance testing</title>
      <pubDate>2024-01-19T00:00:00+00:00</pubDate>
      <link href="https://ktherage.github.io/blog/bash-and-curl-simple-http-call-performance-testing/" rel="alternate" type="text/html" />
      <description><![CDATA[<p>HTTP calls are essential to the functioning of the web and are critical to any web development project. You may have wondered how to quickly see how your HTTP call is performing. I’ll show you how I did it easily using Curl.</p>
<p><strong>TL;DR:</strong> You just want the code (I perfectly understand that 😉)? Scroll to the piece of code.</p>
<h1 id="what-was-my-need-regarding-http-calls">What was my need regarding HTTP calls?</h1>
<p>I needed to have a quick idea of how the application cache system I’ve placed on an HTTP endpoint was performing, and I wanted it to be quick and simple.</p>
<p>With my notions of Shell and basic knowledge of Curl (a small gift from a colleague: you can find a Curl cheatsheet here), I knew that I could easily run a hundred times the same HTTP call and get the total time as a result.</p>
<p>This solution is a simple short solution that fits my needs which was to locally test my HTTP endpoint.
If you plan to do real performance/load testing on real servers like staging or production ones then you shall take a look at tools like (<a href="https://jmeter.apache.org/)[Apache" rel="noopener noreferrer">https://jmeter.apache.org/)[Apache</a> JMeter], (<a href="https://gatling.io/)[Gatling" rel="noopener noreferrer">https://gatling.io/)[Gatling</a>], or other similar tools.</p>
<h1 id="how-curl-helped-me-test-my-http-call">How Curl helped me test my HTTP call?</h1>
<p>So I opened my terminal and ran:</p>
<pre><code class="language-bash hljs bash"><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..100}; <span class="hljs-keyword">do</span> curl <span class="hljs-string">'https://some-domain/some-uri/some-path?cache=false'</span> \
-H <span class="hljs-string">'cache-control: no-cache'</span> \
-H <span class="hljs-string">'pragma: no-cache'</span> \
--compressed \
--insecure -s -o /dev/null -w <span class="hljs-string">"%{time_total}s\n"</span>;
<span class="hljs-keyword">done</span>

<span class="hljs-comment"># Which printed :</span>
0.057703s
0.067895s
0.063033s
0.062455s
0.074864s
...</code></pre>
<p>For some explanations, I copied the request sent by my browser as a Curl request (this could be easily done on most browsers see (<a href="https://quickref.me/curl)[here" rel="noopener noreferrer">https://quickref.me/curl)[here</a>]) and wrapped it in a for loop which is running that HTTP call a hundred times.</p>
<p><strong>The only change I made to the Curl request</strong> was to add the options <code>-s</code> for a quiet output, <code>-o /dev/null</code> to avoid having the response body printed and the most important one <code>-w "%{time_total}s\n"</code> which allows me to format Curl’s output to return the total time. You can have a full list of available “Write out variables” (<a href="https://everything.curl.dev/usingcurl/verbose/writeout.html#available-write-out-variables)[here" rel="noopener noreferrer">https://everything.curl.dev/usingcurl/verbose/writeout.html#available-write-out-variables)[here</a>].</p>
<p>And that’s it 🎉 ! <strong>You can have a quick performance idea only with the tools you may already use.</strong></p>
<p>I hope you’ll find it helpful!</p>
<p>Please feel free to leave your comments or questions below.</p>]]></description>
    </item>
    <item>
      <guid>https://ktherage.github.io/blog/postman-automatic-jwt-authentication-with-expired-token-refresh/</guid>
      <title>Postman : Automatic JWT authentication with expired token refresh</title>
      <pubDate>2022-12-20T00:00:00+00:00</pubDate>
      <link href="https://ktherage.github.io/blog/postman-automatic-jwt-authentication-with-expired-token-refresh/" rel="alternate" type="text/html" />
      <description><![CDATA[<p>You’ve always wondered how to get automatically authenticated toward your JWT API. I’ll tell you how I achieved this in this article.</p>
<hr>
<h2 id="short-introduction">Short Introduction</h2>
<p>You may not know what is Postman, so I'll describe it to you.
You may use any IDE (like PHPStorm, VSCode,…) to help you during the development phase of your project with things like autocompletion, test runs, debugging, and so on.</p>
<p>Postman is quite the same as an IDE but designed especially to create APIs calls. It's a lot more pleasant to use than the plain old curl command. Learn more about Postman by visiting their website <a href="https://www.postman.com/" rel="noopener noreferrer">https://www.postman.com/</a>.</p>
<p>What about JSON Web Tokens (shortened to JWT in the rest of this article)? JWT is an open-source industry standard that defines a self-contained (i.e. information are held by the token) way for securely transmitting information between parties as a JSON object.
In our case, they'll be used to give us access to an API. Learn more about JSON Web Tokens by visiting their website <a href="https://jwt.io/" rel="noopener noreferrer">https://jwt.io/</a>.</p>
<hr>
<h2 id="the-journey-to-this-magical-world">The journey to this magical world</h2>
<h3 id="in-the-begining-was-the-manual-request">In the begining, was the manual request</h3>
<p>As a Web Developer, I'm used to call APIs (mine or third party APIs) that requires a JWT, and I usually test them using Postman.
For many months, or maybe years, calling an endpoint on those kinds of APIs led me to manually apply the following workflow:</p>
<ol>
<li>Get a token through a saved request attached to my Postman collection (basically authenticating toward my API)</li>
<li>Copy the token string</li>
<li>Create an "Authorization" header</li>
<li>Paste the token string as a "Bearer" token as value for my "Authorization" header (header example: "Authorization:Bearer pastedTokenString")</li>
<li>Finally, request my endpoint</li>
</ol>
<p>This workflow also had me to update manually the JWT token when it has expired. By the time, I got a bit smarter by using Postman's environment variables in order to have the right token string stored in the proper environment (like having a JWT for the staging environment and one for the production).</p>
<p>But some months ago I was wondering if, as lazy as I am, there was a better way to make it? I also heard legends about some dev who calls his API calls without worrying about Authentication. So why not me?</p>
<h3 id="then-was-the-automatic-request">Then was the automatic request</h3>
<p>So like any good (or not 😛) developer, my journey has started on… (already guessed it 😉?) Stack Overflow!</p>
<p>I found this article there, and even if it was not the right solution, it gave me a clearer idea of where to search. In fact, and to be honest, I was searching for a piece of code that I could quickly copy and paste eyes closed 😅.</p>
<p>So I continued my quest, and I found Utkarsha Baksh's medium article "Using Postman Pre-request Script to Automatically Set Token" which has a perfect (and not too long) step by step tutorial. It details how to make a simple automatic login request before calling the desired API endpoint using small Postman's pre-request script. If you are new to that "pre-request script" concept, really shall read this article or take a look at Postman's documentation.</p>
<p>Amazing plus, there's a piece of code! So I copy/pasted it, followed the steps, adapted it to my use cases, and it worked 🎊 🎉 🪩!</p>
<p>I never got back on it until… I lost this pre-request script because of a Postman reinstallation and not having a paid version 🫣 😩.</p>
<p>This leads us to this day (2022/12/12 😉) where I was again doing the same manual routine and remembered that good old time when it was automatic.</p>
<h3 id="finally-was-the-consecration">Finally was the consecration</h3>
<p>So another time, I packed my bag and got again on that journey to that wonderful land. The trip was a lot faster that time 😅 and I found a new piece of code that looked more detailed and more respectful of JWT's refresh mechanism by not always authenticating toward the API when the token is still valid (I mean not expired).</p>
<p>This code could be found there: <a href="https://gist.github.com/Glideh/0f24b8973bb7d79ae8124fa160966df1" rel="noopener noreferrer">https://gist.github.com/Glideh/0f24b8973bb7d79ae8124fa160966df1</a></p>
<p>The only cons I've found using it directly is that it was not taking advantage of the JWT's refresh token. So I copy/pasted it and made some changes to allow an automatic refresh of the token.</p>
<hr>
<h2 id="the-code">The code</h2>
<p>To get a full tutorial on how to define a the following code as a Postman's pre-request script read Utkarsha Baksh's medium article: "Using Postman Pre-request Script to Automatically Set Token"</p>
<pre><code class="language-javascript hljs javascript"><span class="hljs-comment">/**
 * fill in the blanks
 */</span>
<span class="hljs-keyword">const</span> TOKEN_ENV_VAR_NAME = <span class="hljs-string">'token_client'</span>
<span class="hljs-keyword">const</span> LOGIN_URL = pm.environment.get(<span class="hljs-string">"host"</span>) + <span class="hljs-string">"/api/v2/authenticate"</span>
<span class="hljs-keyword">const</span> LOGIN_BODY={
    <span class="hljs-string">"app_id"</span>: pm.environment.get(<span class="hljs-string">"app_id"</span>),
    <span class="hljs-string">"app_secret"</span>: pm.environment.get(<span class="hljs-string">"app_secret"</span>)
}

<span class="hljs-keyword">const</span> REFRESH_TOKEN_ENV_VAR_NAME = <span class="hljs-string">'refresh_token_client'</span>
<span class="hljs-keyword">const</span> REFRESH_URL = pm.environment.get(<span class="hljs-string">"host"</span>) + <span class="hljs-string">"/api/v2/authenticate/refresh"</span>
<span class="hljs-keyword">const</span> REFRESH_BODY={
    <span class="hljs-string">"refreshToken"</span>: pm.environment.get(REFRESH_TOKEN_ENV_VAR_NAME)
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isExpiredToken</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> jwt = pm.environment.get(TOKEN_ENV_VAR_NAME)
    <span class="hljs-keyword">const</span> payload = <span class="hljs-built_in">JSON</span>.parse(atob(jwt.split(<span class="hljs-string">'.'</span>)[<span class="hljs-number">1</span>]));
    <span class="hljs-comment">// Expiration timestamp (in seconds) is located in the `exp` key</span>
    <span class="hljs-keyword">const</span> millisecBeforeExpiration = (payload.exp * <span class="hljs-number">1000</span>) - (<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()).getTime();
    <span class="hljs-keyword">if</span> (millisecBeforeExpiration &lt;= <span class="hljs-number">0</span>) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Token is expired"</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    }

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Token is still valid ! Expiring in <span class="hljs-subst">${millisecBeforeExpiration <span class="hljs-regexp">/ 1000} seconds`);
    return false;
}

function tokensExists() {
    const token = pm.environment.get(TOKEN_ENV_VAR_NAME)
    const refreshToken = pm.environment.get(REFRESH_TOKEN_ENV_VAR_NAME)
    if (!token) {
        console.log("Token not found");
        return false;
    }

    if (!refreshToken) {
        console.log("Refresh token not found");
        return false;
    }

    return true;
}

function login() {
    console.log("Authenticating")
    const body = JSON.stringify(LOGIN_BODY);
    const request = {
        url: LOGIN_URL,
        method: "POST",
        header: {
            "Content-Type": "application/</span>json<span class="hljs-string">",
            "</span>Accept<span class="hljs-string">": "</span>application<span class="hljs-regexp">/json",
        },
        body,
    };

    pm.sendRequest(request, (err, res) =&gt; {
        if (err || res.code !== 200) {
            console.log("Login failed:");
            console.log(err);
            console.log(res);

            throw new Error('Login failed, check postman\'s console for details')
        }
        pm.environment.set(TOKEN_ENV_VAR_NAME, res.json().token);
        console.log("Token saved");
        pm.environment.set(REFRESH_TOKEN_ENV_VAR_NAME, res.json().refreshToken);
        console.log("Refresh Token saved");
    });
}

function refresh() {
    console.log("Refreshing token")
    const body = JSON.stringify(REFRESH_BODY);
    const request = {
        url: REFRESH_URL,
        method: "POST",
        header: {
            "Content-Type": "application/</span>json<span class="hljs-string">",
            "</span>Accept<span class="hljs-string">": "</span>application<span class="hljs-regexp">/json",
        },
        body,
    };

    pm.sendRequest(request, (err, res) =&gt; {
        if (res.code === 498) {
            console.log('Refresh token has expired or is invalid')
            login()
            return
        }
        if (err || res.code !== 200) {
            console.log("Refreshing token failed:");
            console.log(err);
            console.log(res);

            throw new Error('Refreshing token failed, check postman\'s console for details')
        }
        console.log("Token refreshed");
        pm.environment.set(TOKEN_ENV_VAR_NAME, res.json().token);
    });
}

if (tokensExists()) {
    if (!isExpiredToken()) {
        return
    }

    refresh()
} else {
    login()
}</span></span></span></code></pre>
<hr>
<h2 id="thanks">Thanks</h2>
<p>Postman for producing tools that ease API developer's life</p>
<p>Stack overflow and his community for being the developer's life-buoy</p>
<p>Pierre de LESPINAY for the Gist that inspired me this piece of code and the current article</p>
<p>SensioLabs' Team for their help and their reviews on this article</p>]]></description>
    </item>
  </channel>
</rss>
