<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sorties API Platform on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/series/api-platform-releases/</link><description>Recent content in Sorties API Platform on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Thu, 18 Sep 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/series/api-platform-releases/index.xml" rel="self" type="application/rss+xml"/><item><title>API Platform 4.2 : JSON streamer, ObjectMapper, et autoconfigure</title><link>https://guillaumedelre.github.io/fr/2025/09/18/api-platform-4.2-json-streamer-objectmapper-et-autoconfigure/</link><pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2025/09/18/api-platform-4.2-json-streamer-objectmapper-et-autoconfigure/</guid><description>Part 8 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 4.2 diffuse les grandes collections JSON sans buffering, remplace le câblage DTO manuel par ObjectMapper, et autoconfigure les ressources depuis leurs attributs de classe.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.2 est arrivé en septembre 2025. Trois changements se distinguent : un JSON streamer pour les grandes collections qui évite de bufferiser toute la réponse en mémoire, un ObjectMapper qui remplace le câblage manuel dans les flux DTO basés sur <code>stateOptions</code>, et l&rsquo;autoconfiguration de <code>#[ApiResource]</code> sans enregistrement de service explicite.</p>
<h2 id="json-streamer-pour-les-grandes-collections">JSON streamer pour les grandes collections</h2>
<p>Le serializer Symfony par défaut construit la réponse complète en mémoire avant de l&rsquo;écrire dans la sortie. Pour une collection de 10 000 éléments, cela signifie allouer un tableau PHP, le sérialiser en string, et garder les deux en mémoire jusqu&rsquo;au flush de la réponse. À grande échelle, c&rsquo;est la source des erreurs OOM qui forcent à ajouter de la pagination partout.</p>
<p>La 4.2 ajoute un encodeur JSON en streaming qui écrit la réponse de façon incrémentale :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">api_platform</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">serializer</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">enable_json_streamer</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Avec le streaming activé, la réponse est écrite directement dans le buffer de sortie au fur et à mesure que chaque élément est sérialisé. L&rsquo;utilisation mémoire reste approximativement constante quelle que soit la taille de la collection. La contrepartie : on ne peut pas définir de headers de réponse après le début du streaming, et le code de statut HTTP doit être validé avant que le premier octet soit écrit.</p>
<h2 id="objectmapper-remplace-le-câblage-dto-manuel">ObjectMapper remplace le câblage DTO manuel</h2>
<p>La 3.1 avait introduit <code>stateOptions</code> avec <code>DoctrineOrmOptions</code> pour séparer la ressource API de l&rsquo;entité Doctrine. Le provider recevait des objets entité et le serializer les mappait sur le DTO. Ça fonctionnait, mais le mapping était implicite — le serializer utilisait les noms de propriétés pour faire correspondre les champs, et tout ce qui ne correspondait pas était soit ignoré soit causait une erreur de normalisation.</p>
<p>La 4.2 introduit <code>ObjectMapper</code>, une couche de mapping déclarative entre entités et DTOs :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\ObjectMapper\Attribute\Map</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Map(source: BookEntity::class)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $title;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $authorName;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;attribut <code>#[Map]</code> indique à ObjectMapper que <code>BookDto</code> peut être peuplé depuis <code>BookEntity</code>. Les noms de champs sont mis en correspondance par convention ; les inadéquations sont déclarées explicitement au niveau de chaque propriété :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\ObjectMapper\Attribute\Map</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Map(source: BookEntity::class)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Map(source: &#39;author.fullName&#39;)]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $authorName;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La notation pointée traverse les objets imbriqués. Le mapping s&rsquo;exécute avant la sérialisation et remplace le comportement implicite de correspondance de propriétés du serializer. Les champs non mappés lèvent une erreur au moment de la configuration, pas à l&rsquo;exécution.</p>
<p>ObjectMapper fonctionne avec <code>stateOptions</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Doctrine\Orm\State\Options</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\ObjectMapper\Attribute\Map</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">stateOptions</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Options</span>(<span style="color:#a6e22e">entityClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookEntity</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Map(source: BookEntity::class)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span> {}
</span></span></code></pre></div><p>Le provider récupère les objets <code>BookEntity</code> depuis Doctrine. ObjectMapper les convertit en instances <code>BookDto</code>. Le serializer écrit le DTO. Trois étapes distinctes, chacune avec un contrat clair.</p>
<h2 id="intégration-typeinfo-dans-toute-la-pile">Intégration TypeInfo dans toute la pile</h2>
<p>Symfony 7.1 a introduit le <a href="https://symfony.com/doc/current/components/type_info.html" target="_blank" rel="noopener noreferrer">composant TypeInfo</a>
, une couche unifiée d&rsquo;introspection des types qui comprend les types union, intersection, les collections génériques et les types nullable à travers la reflection, PHPDoc et la syntaxe PHP 8.x.</p>
<p>La 4.2 remplace la résolution de types interne d&rsquo;API Platform par TypeInfo. Cela affecte la génération de schémas de filtres, l&rsquo;inférence de schéma OpenAPI, et la coercition de types du serializer. Le bénéfice visible est que les types qui généraient auparavant des schémas OpenAPI incorrects ou manquants — <code>Collection&lt;int, Book&gt;</code>, <code>list&lt;string&gt;</code>, types intersection — produisent maintenant des schémas précis sans annotations <code>@ApiProperty</code> manuelles.</p>
<h2 id="autoconfigure-apiresource">Autoconfigure <code>#[ApiResource]</code></h2>
<p>Avant la 4.2, ajouter <code>#[ApiResource]</code> à une classe suffisait pour qu&rsquo;Hugo la découvre uniquement si la classe était dans un chemin scanné par le chargeur de ressources d&rsquo;API Platform. En dehors de ce chemin, une configuration de service explicite était nécessaire.</p>
<p>La 4.2 se branche sur le système d&rsquo;autoconfigure de Symfony. Toute classe taguée avec <code>#[ApiResource]</code> est automatiquement enregistrée comme ressource indépendamment de son emplacement, tant qu&rsquo;elle est dans un répertoire couvert par le scan de composants de Symfony. Aucune entrée dans <code>config/services.yaml</code> nécessaire.</p>
<p>Pour Laravel, l&rsquo;équivalent utilise l&rsquo;autoloading du service provider de Laravel — les modèles Eloquent avec <code>#[ApiResource]</code> sont récupérés automatiquement sans enregistrement manuel.</p>
<h2 id="doctrine-existsfilter">Doctrine ExistsFilter</h2>
<p>L&rsquo;<code>ExistsFilter</code> contraint une collection selon qu&rsquo;une relation ou un champ nullable est défini :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[ApiFilter(ExistsFilter::class, properties: [&#39;publishedAt&#39;])]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p><code>GET /books?exists[publishedAt]=true</code> retourne les livres où <code>publishedAt</code> n&rsquo;est pas null. <code>exists[publishedAt]=false</code> retourne les livres où il l&rsquo;est.</p>
]]></content:encoded></item><item><title>API Platform 4.1 : paramètres de requête stricts, OpenAPI multi-spec, et limites GraphQL</title><link>https://guillaumedelre.github.io/fr/2025/02/28/api-platform-4.1-param%C3%A8tres-de-requ%C3%AAte-stricts-openapi-multi-spec-et-limites-graphql/</link><pubDate>Fri, 28 Feb 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2025/02/28/api-platform-4.1-param%C3%A8tres-de-requ%C3%AAte-stricts-openapi-multi-spec-et-limites-graphql/</guid><description>Part 7 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 4.1 formalise la validation stricte des paramètres de requête, introduit x-apiplatform-tag pour découper une API en plusieurs specs OpenAPI, et ajoute des limites de profondeur et complexité pour GraphQL.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.1 est arrivé en février 2025 avec un lot de fonctionnalités moins axées sur de nouvelles capacités que sur la mise en production des existantes. La validation stricte des paramètres de requête gagne une propriété de première classe. OpenAPI gagne un mécanisme pour découper les grandes APIs en specs séparées. GraphQL obtient les contrôles de prévention des abus qui lui manquaient.</p>
<h2 id="validation-stricte-des-paramètres-de-requête">Validation stricte des paramètres de requête</h2>
<p>La 3.3 avait introduit la validation des paramètres de requête en opt-in. La 3.4 avait déprécié le comportement permissif. La 4.1 la formalise avec une propriété native <code>strictQueryParameterValidation</code> sur les ressources et opérations : quand elle est à <code>true</code>, les paramètres de requête inconnus renvoient 400.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\QueryParameter</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">strictQueryParameterValidation</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">parameters</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">QueryParameter</span>(<span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;utm_source&#39;</span>, <span style="color:#a6e22e">required</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">schema</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;type&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;string&#39;</span>]),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">QueryParameter</span>(<span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;feature_flag&#39;</span>, <span style="color:#a6e22e">required</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">schema</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;type&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;string&#39;</span>]),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>Les paramètres déclarés passent ; les paramètres non déclarés sont rejetés. Pour désactiver la validation stricte sur une opération spécifique quand elle est activée au niveau de la ressource, passer <code>strictQueryParameterValidation: false</code> sur cette opération.</p>
<h2 id="x-apiplatform-tag-pour-openapi-multi-spec"><code>x-apiplatform-tag</code> pour OpenAPI multi-spec</h2>
<p>Les grandes APIs ont souvent besoin de plusieurs specs OpenAPI : une par équipe, une par version d&rsquo;API, une interne et une publique. Avant la 4.1, la spec générée était un seul document, et la diviser nécessitait un post-traitement ou des instances API Platform séparées.</p>
<p>La 4.1 ajoute une extension vendor <code>x-apiplatform-tag</code> (sans <code>s</code> final). On tague les opérations avec des noms de groupes logiques via les <code>extensionProperties</code> d&rsquo;un objet <code>Operation</code> OpenAPI, puis on demande la spec filtrée sur un ou plusieurs groupes :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Factory\OpenApiFactory</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\Operation</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">openapi</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Operation</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">extensionProperties</span><span style="color:#f92672">:</span> [<span style="color:#a6e22e">OpenApiFactory</span><span style="color:#f92672">::</span><span style="color:#a6e22e">API_PLATFORM_TAG</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;public&#39;</span>, <span style="color:#e6db74">&#39;v2&#39;</span>]]
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>Demander <code>/api/docs.json?filter_tags[]=public</code> retourne uniquement les opérations taguées <code>public</code>. La spec complète reste disponible sans filtre. Les groupes n&rsquo;affectent pas le comportement réel de l&rsquo;API — c&rsquo;est uniquement une préoccupation de couche documentation.</p>
<p>Cela rend faisable de maintenir une configuration API Platform unique tout en servant différentes vues de spec à différents consommateurs : un Swagger UI public, un portail partenaire, et un outil interne qui expose les endpoints d&rsquo;administration.</p>
<h2 id="authentification-http-dans-swagger-ui">Authentification HTTP dans Swagger UI</h2>
<p>Avant la 4.1, le Swagger UI fourni avec API Platform supportait l&rsquo;authentification par token Bearer via son dialogue &ldquo;Authorize&rdquo;. L&rsquo;authentification par clé API et HTTP Basic n&rsquo;étaient pas câblées.</p>
<p>La 4.1 ajoute le support de plusieurs schémas de sécurité dans le document OpenAPI généré. Les schémas de sécurité sont ajoutés en décorant l&rsquo;<code>OpenApiFactory</code> et en modifiant l&rsquo;objet <code>components.securitySchemes</code> de la spec. Chaque schéma déclaré apparaît ensuite dans le dialogue &ldquo;Authorize&rdquo; de Swagger UI et est appliqué aux requêtes faites depuis l&rsquo;UI. C&rsquo;est une amélioration de la documentation et de l&rsquo;expérience développeur — la logique d&rsquo;authentification réelle dans votre application n&rsquo;est pas affectée.</p>
<h2 id="limites-de-profondeur-et-complexité-des-requêtes-graphql">Limites de profondeur et complexité des requêtes GraphQL</h2>
<p>La structure de requête récursive de GraphQL rend triviale la construction d&rsquo;une requête petite en octets mais énorme en coût d&rsquo;exécution. Sans limites, une requête imbriquée sur quatre niveaux à travers une relation many-to-many peut frapper la base de données des centaines de fois.</p>
<p>La 4.1 ajoute des limites configurables de profondeur et complexité :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">api_platform</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">graphql</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">max_query_depth</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">max_query_complexity</span>: <span style="color:#ae81ff">100</span>
</span></span></code></pre></div><p><code>max_query_depth</code> est le niveau d&rsquo;imbrication maximum. <code>max_query_complexity</code> assigne un coût à chaque champ et rejette les requêtes dont le coût total dépasse le seuil. Les requêtes qui dépassent l&rsquo;une ou l&rsquo;autre limite sont rejetées avant exécution avec une réponse 400.</p>
<p>Il n&rsquo;y a pas de valeur universellement correcte pour ces limites — elles dépendent de la forme de votre schéma et des patterns de requête attendus. Les valeurs par défaut sont intentionnellement permissives pour éviter de casser des APIs existantes lors de la mise à jour. Les resserrer est un choix de configuration délibéré.</p>
<h2 id="formats-de-sortie-au-niveau-opération">Formats de sortie au niveau opération</h2>
<p>La 4.0 et les versions antérieures configuraient les types de contenu acceptés et retournés au niveau de l&rsquo;API. La 4.1 permet de les restreindre par opération :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">outputFormats</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;jsonld&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;application/ld+json&#39;</span>]],
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">inputFormats</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;json&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;application/json&#39;</span>]],
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>Les opérations qui ne spécifient pas de formats héritent de la configuration au niveau de l&rsquo;API. C&rsquo;est utile pour les endpoints qui doivent retourner un format spécifique (une export CSV, un flux binaire) sans changer les défauts pour le reste de l&rsquo;API.</p>
]]></content:encoded></item><item><title>API Platform 4.0 : support Laravel et PUT repensé</title><link>https://guillaumedelre.github.io/fr/2024/09/27/api-platform-4.0-support-laravel-et-put-repens%C3%A9/</link><pubDate>Fri, 27 Sep 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/09/27/api-platform-4.0-support-laravel-et-put-repens%C3%A9/</guid><description>Part 6 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 4.0 apporte le support Laravel de première classe avec Eloquent et les policies, et supprime PUT des opérations par défaut pour corriger une ambiguïté sémantique persistante.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.0 est sorti neuf jours après la 3.4, fin septembre 2024. Le numéro de version est honnête : il n&rsquo;y a pas de nouvelle architecture, et la migration depuis la 3.4 est courte si on a résolu les dépréciations. Ce qui en fait un majeur, c&rsquo;est le changement de scope — API Platform n&rsquo;est plus un framework uniquement Symfony — et un défaut d&rsquo;opinion qui inverse six ans de comportement PUT.</p>
<h2 id="laravel-comme-cible-de-première-classe">Laravel comme cible de première classe</h2>
<p>Depuis sa première release, API Platform était construit sur Symfony. La couche HTTP, les métadonnées, le serializer et le bridge Doctrine supposaient tous le container de Symfony, l&rsquo;event dispatcher et le cycle de vie des requêtes. Les utilisateurs Laravel pouvaient faire tourner API Platform via un adaptateur léger, mais les filtres, la sécurité et l&rsquo;intégration Doctrine ne fonctionnaient pas avec Eloquent.</p>
<p>La 4.0 livre un bridge Laravel dédié. Il mappe la couche d&rsquo;état d&rsquo;API Platform sur le cycle de vie des requêtes de Laravel, s&rsquo;intègre directement avec les modèles Eloquent, et se branche sur le système d&rsquo;autorisation de Laravel :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Illuminate\Database\Eloquent\Model</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(), <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Get</span>()]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Model</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> $fillable <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;title&#39;</span>, <span style="color:#e6db74">&#39;author&#39;</span>];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>L&rsquo;autorisation utilise les policies et gates de Laravel plutôt que les security voters de Symfony. Les opérations exposent un paramètre <code>policy</code> dédié qui correspond à un nom de méthode de policy :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[Get(policy: &#39;view&#39;)]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Model</span> {}
</span></span></code></pre></div><p>API Platform mappe la valeur de <code>policy</code> vers <code>Gate::allows()</code> de Laravel avec l&rsquo;instance du modèle. Les policies peuvent aussi être auto-détectées : si un modèle a une classe de policy enregistrée, API Platform infère la bonne méthode (<code>view</code>, <code>viewAny</code>, <code>create</code>, <code>update</code>, <code>delete</code>) selon le type d&rsquo;opération. Les filtres pour les collections Eloquent couvrent le même périmètre que leurs homologues Doctrine : <code>PartialSearchFilter</code>, <code>EqualsFilter</code>, <code>RangeFilter</code>, <code>OrderFilter</code>, <code>DateFilter</code>, et des variantes de recherche (<code>StartSearchFilter</code>, <code>EndSearchFilter</code>). La pagination, le tri et la validation fonctionnent via les mécanismes natifs de Laravel.</p>
<p>Ce n&rsquo;est pas un shim de compatibilité. Le bridge Laravel est maintenu aux côtés du bridge Symfony et est couvert par la même suite de tests. Les projets utilisant l&rsquo;un ou l&rsquo;autre framework ont la même API de définition des ressources.</p>
<h2 id="put-supprimé-des-opérations-par-défaut">PUT supprimé des opérations par défaut</h2>
<p>Depuis API Platform 1.0, <code>#[ApiResource]</code> sans tableau <code>operations</code> explicite générait des opérations CRUD incluant PUT. Le handler PUT mettait à jour les ressources existantes et, depuis la 3.1, pouvait aussi les créer via <code>allowCreate: true</code>.</p>
<p>La 4.0 supprime PUT de l&rsquo;ensemble par défaut. <code>#[ApiResource]</code> génère maintenant GET, POST, PATCH et DELETE. Pour utiliser PUT, il faut le déclarer explicitement :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Put</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ... autres opérations
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Put</span>(),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>La motivation est la clarté sémantique. PATCH remplace PUT pour la plupart des cas d&rsquo;usage de mise à jour partielle. La sémantique de PUT — remplacer la représentation entière de la ressource — est rarement ce qu&rsquo;une API implémente réellement, mais le défaut le faisait apparaître dans toutes les APIs à moins de l&rsquo;enlever activement. Rendre PUT opt-in aligne les défauts sur la façon dont la sémantique HTTP est réellement utilisée en pratique.</p>
<h2 id="php-82-minimum">PHP 8.2 minimum</h2>
<p>La 4.0 abandonne PHP 8.0 et 8.1. PHP 8.2 est le nouveau minimum. La syntaxe de classe readonly, <code>AllowDynamicProperties</code> et les types DNF<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> introduits en 8.2 sont disponibles dans toute la codebase. Aucune fonctionnalité spécifique de 8.2 n&rsquo;est structurante pour la 4.0 — le bump de version concerne principalement l&rsquo;abandon du fardeau de maintenance plus ancien.</p>
<h2 id="symfony-64-et-doctrine-orm-217-minimum">Symfony 6.4+ et Doctrine ORM 2.17+ minimum</h2>
<p>Côté Symfony, la 4.0 requiert Symfony 6.4 ou 7.x et Doctrine ORM 2.17 ou 3.x. Les deux étaient déjà supportés en 3.4. La migration de la 3.4 vers la 4.0 sur la piste Symfony est : résoudre les dépréciations 3.4, vérifier qu&rsquo;on est sur Symfony 6.4+ et ORM 2.17+, puis mettre à jour. Aucun travail de migration supplémentaire n&rsquo;est nécessaire si c&rsquo;est déjà en place.</p>
<h2 id="ce-que-la-40-nest-pas">Ce que la 4.0 n&rsquo;est pas</h2>
<p>La 4.0 n&rsquo;est pas une nouvelle architecture. Les state providers, processors et le modèle de métadonnées de ressources de la 3.0 sont inchangés. Le bridge Laravel ajoute un nouveau contexte d&rsquo;exécution mais ne change pas la façon dont les ressources ou les opérations sont déclarées. La séparation est intentionnelle : si la 3.0 était le &ldquo;quoi&rdquo;, la 4.0 est le &ldquo;où&rdquo;.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Types en Forme Normale Disjonctive : types intersection combinés avec union, comme <code>(A&amp;B)|null</code>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>API Platform 3.4 : BackedEnum comme ressources et support DBAL 4</title><link>https://guillaumedelre.github.io/fr/2024/09/18/api-platform-3.4-backedenum-comme-ressources-et-support-dbal-4/</link><pubDate>Wed, 18 Sep 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/09/18/api-platform-3.4-backedenum-comme-ressources-et-support-dbal-4/</guid><description>Part 5 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.4 fait des BackedEnum des ressources API complètes, ajoute un BackedEnumFilter, supporte les expressions de sécurité sur les paramètres, et ajoute le support DBAL 4.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.4 est arrivé en septembre 2024 comme dernier mineur avant le saut vers la 4.0. La fonctionnalité principale est le BackedEnum comme ressource complète — pas juste un champ typé, mais un enum qui est lui-même un endpoint API.</p>
<h2 id="backedenum-comme-ressources-api">BackedEnum comme ressources API</h2>
<p>Depuis PHP 8.1, les classes BackedEnum ont un ensemble fixe de cas avec des valeurs de support string ou integer. API Platform 3.4 permet de mettre <code>#[ApiResource]</code> directement sur un BackedEnum :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>()]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">enum</span> <span style="color:#a6e22e">BookStatus</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Draft</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;draft&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Published</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;published&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Archived</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;archived&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Un endpoint <code>GET /book_statuses</code> retourne la liste des cas. Chaque cas est sérialisé avec son nom et sa valeur. L&rsquo;endpoint est en lecture seule — les enums sont immuables par nature.</p>
<p>C&rsquo;est surtout utile pour les consommateurs frontend qui veulent une liste lisible par machine des valeurs valides sans les coder en dur. L&rsquo;alternative était un contrôleur personnalisé ou une ressource DTO dédiée listant manuellement les valeurs de l&rsquo;enum.</p>
<h2 id="backedenumfilter">BackedEnumFilter</h2>
<p>Le compagnon des ressources enum est <code>BackedEnumFilter</code>, un nouveau filtre pour les collections Doctrine qui contraint une requête par une propriété BackedEnum :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Doctrine\Orm\Filter\BackedEnumFilter</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiFilter</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiFilter(BackedEnumFilter::class, properties: [&#39;status&#39;])]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">BookStatus</span> $status;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>GET /books?status=published</code> filtre la collection aux livres où <code>status</code> est égal à <code>BookStatus::Published</code>. Les valeurs d&rsquo;enum invalides retournent une réponse 400. Avant ce filtre, on devait soit écrire un filtre personnalisé, soit utiliser <code>SearchFilter</code> et valider la valeur manuellement.</p>
<h2 id="expressions-de-sécurité-sur-les-paramètres">Expressions de sécurité sur les paramètres</h2>
<p>La 3.3 avait ajouté la sécurité aux liens et aux propriétés. La 3.4 étend cela aux paramètres de requête. Un paramètre peut déclarer une expression de sécurité qui contrôle s&rsquo;il est accepté :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\QueryParameter</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[GetCollection(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">parameters</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">QueryParameter</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">key</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;includeDeleted&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">security</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;is_granted(&#39;ROLE_ADMIN&#39;)&#34;</span>
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span> {}
</span></span></code></pre></div><p>Quand l&rsquo;expression de sécurité est false, le paramètre est rejeté avec un 403, pas silencieusement ignoré. C&rsquo;est plus explicite que vérifier le rôle de l&rsquo;utilisateur dans le provider après avoir reçu le paramètre.</p>
<h2 id="support-dbal-4-ajouté">Support DBAL 4 ajouté</h2>
<p>La 3.4 ajoute le support de Doctrine DBAL 4, qui apporte des changements au système de types qui affectent la façon dont les types personnalisés et le SQL spécifique à la plateforme fonctionnent. Les filtres Doctrine Orm et les extensions de requête dans API Platform ont été mis à jour pour fonctionner avec la nouvelle API de types DBAL 4.</p>
<p>DBAL 3 (<code>^3.4.0</code>) et DBAL 4 sont supportés simultanément en 3.4. C&rsquo;est la release à adopter pour migrer vers DBAL 4 tout en restant sur une branche stable API Platform 3.x.</p>
<h2 id="validateur-de-paramètres-de-requête-déprécié">Validateur de paramètres de requête déprécié</h2>
<p>La 3.3 avait ajouté le validateur strict de paramètres de requête en opt-in. La 3.4 déprécie l&rsquo;ancien comportement (paramètres inconnus silencieusement ignorés) en préparation pour rendre la validation stricte la valeur par défaut en 4.0. Les projets qui s&rsquo;appuient sur des paramètres de requête pass-through ont une release de plus pour les déclarer explicitement.</p>
<h2 id="dernier-arrêt-avant-la-40">Dernier arrêt avant la 4.0</h2>
<p>La 3.4 est la dernière release 3.x avec de nouvelles fonctionnalités. Tout ce qui était déprécié d&rsquo;ici la 3.4 disparaît en 4.0. Le chemin de migration de la 3.4 vers la 4.0 est intentionnellement court : résoudre les dépréciations, puis mettre à jour.</p>
]]></content:encoded></item><item><title>API Platform 3.3 : headers, sécurité des liens, et webhooks OpenAPI</title><link>https://guillaumedelre.github.io/fr/2024/04/29/api-platform-3.3-headers-s%C3%A9curit%C3%A9-des-liens-et-webhooks-openapi/</link><pubDate>Mon, 29 Apr 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2024/04/29/api-platform-3.3-headers-s%C3%A9curit%C3%A9-des-liens-et-webhooks-openapi/</guid><description>Part 4 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.3 ajoute la configuration déclarative des headers, la sécurité fine sur les liens de sous-ressources, et le support des webhooks OpenAPI.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.3 est sorti en avril 2024 avec un ensemble d&rsquo;ajouts ciblés. Aucun d&rsquo;eux ne remodèle l&rsquo;architecture — la 3.2 avait déjà clos ce chapitre. Ce que la 3.3 apporte, c&rsquo;est du contrôle sur des choses qui étaient soit codées en dur soit nécessitaient un contournement : les headers de réponse, la visibilité des liens sur les sous-ressources, et les webhooks dans la spec générée.</p>
<h2 id="configuration-déclarative-des-headers">Configuration déclarative des headers</h2>
<p>Avant la 3.3, définir des headers de réponse personnalisés nécessitait soit un processor personnalisé qui modifiait l&rsquo;objet réponse, soit un event listener Symfony sur <code>kernel.response</code>. Les deux approches fonctionnaient mais vivaient en dehors de la définition de la ressource.</p>
<p>La 3.3 ajoute un paramètre <code>parameters</code> aux métadonnées d&rsquo;opération :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\HeaderParameter</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Get(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">parameters</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;X-Custom-Header&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">HeaderParameter</span>(<span style="color:#a6e22e">description</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;A custom header&#39;</span>),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span></code></pre></div><p>Pour les headers qui varient par réponse (comme <code>Cache-Control</code> avec un max-age calculé), le processor peut encore les définir directement sur l&rsquo;objet réponse. Le paramètre <code>parameters</code> sert principalement à documenter les headers attendus dans la spec OpenAPI et pour les valeurs de headers statiques.</p>
<h2 id="sécurité-des-liens-sur-les-sous-ressources">Sécurité des liens sur les sous-ressources</h2>
<p>Quand une ressource expose des liens vers des ressources liées, ces liens apparaissent dans la sortie sérialisée indépendamment du fait que l&rsquo;utilisateur courant puisse accéder à la ressource liée. Cela crée un problème de divulgation : un utilisateur qui peut lire un livre mais pas le profil de son auteur voit quand même l&rsquo;URI de l&rsquo;auteur dans la réponse.</p>
<p>La 3.3 ajoute des expressions de sécurité au descripteur <code>Link</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Link</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[Get]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Link(
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">toClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Author</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">security</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;is_granted(&#39;ROLE_ADMIN&#39;)&#34;</span>
</span></span><span style="display:flex;"><span>    )]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">Author</span> $author;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le lien est omis de la réponse quand l&rsquo;expression de sécurité évalue à false. La ressource liée elle-même n&rsquo;est pas affectée — seulement le fait que la réponse courante inclue la référence à elle.</p>
<h2 id="apipropertysecurity"><code>ApiProperty::security</code></h2>
<p>Le même mécanisme d&rsquo;expression de sécurité est disponible au niveau propriété via <code>ApiProperty::security</code>. Cela permet de cacher des champs individuels selon l&rsquo;utilisateur courant sans écrire un normalizer personnalisé :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiProperty</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[ApiProperty(security: &#34;is_granted(&#39;ROLE_ADMIN&#39;)&#34;)]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $internalNote;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La propriété est exclue de la sérialisation quand l&rsquo;expression est false. C&rsquo;est plus propre qu&rsquo;un normalizer pour le cas courant de champs conditionnels par rôle.</p>
<h2 id="webhooks-openapi">Webhooks OpenAPI</h2>
<p><a href="https://spec.openapis.org/oas/v3.1.0" target="_blank" rel="noopener noreferrer">OpenAPI 3.1</a>
 supporte les webhooks — des appels HTTP sortants que votre API fait à des listeners enregistrés — dans le document spec lui-même. Avant la 3.3, il n&rsquo;y avait pas de moyen de les documenter dans la spec générée par API Platform.</p>
<p>La 3.3 ajoute une classe <code>Webhook</code> à passer au paramètre <code>openapi</code> d&rsquo;une opération. On déclare une classe PHP dédiée avec <code>#[ApiResource]</code> et on utilise <code>Webhook</code> sur chaque opération pour décrire la forme de l&rsquo;appel sortant :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Post</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Attributes\Webhook</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\Operation</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\PathItem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Post</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">openapi</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Webhook</span>(
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;bookCreated&#39;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">pathItem</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PathItem</span>(
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">post</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Operation</span>(<span style="color:#a6e22e">summary</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Un livre a été créé&#39;</span>),
</span></span><span style="display:flex;"><span>                ),
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookWebhook</span> {}
</span></span></code></pre></div><p>Les définitions de webhooks apparaissent dans la spec générée sous la clé <code>webhooks</code> aux côtés des paths réguliers. Swagger UI les affiche dans une section séparée.</p>
<h2 id="deep-linking-dans-swagger-ui">Deep linking dans Swagger UI</h2>
<p>Swagger UI supporte le deep linking — des URLs mémorisables qui ouvrent directement sur une opération spécifique dans l&rsquo;interface. Avant la 3.3, l&rsquo;intégration API Platform n&rsquo;activait pas cela. La 3.3 active l&rsquo;option <code>deepLinking</code> de Swagger UI, configurable via <code>swagger_ui_extra_configuration</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">api_platform</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">openapi</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">swagger_ui_extra_configuration</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">deepLinking</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Avec cette option activée, le fragment d&rsquo;URL se met à jour pendant la navigation dans l&rsquo;UI, et coller ou partager l&rsquo;URL ouvre la même opération. Utile quand on écrit de la doc qui pointe directement vers un endpoint spécifique.</p>
<h2 id="validation-stricte-des-paramètres-de-requête">Validation stricte des paramètres de requête</h2>
<p>La 3.3 renforce le validateur de paramètres de requête : les paramètres non déclarés sur l&rsquo;opération renvoient maintenant une réponse 400 au lieu d&rsquo;être silencieusement ignorés. Ce comportement est opt-in :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">api_platform</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">validator</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">query_parameter_validation</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>L&rsquo;intention est de détecter les fautes de frappe et les mauvaises utilisations de l&rsquo;API tôt. Si vous vous appuyez sur des paramètres de requête pass-through pour une logique personnalisée (logging, feature flags), vous devez les déclarer explicitement sur l&rsquo;opération avant d&rsquo;activer cela.</p>
]]></content:encoded></item><item><title>API Platform 3.2 : les erreurs comme ressources et le retour des sous-ressources</title><link>https://guillaumedelre.github.io/fr/2023/10/12/api-platform-3.2-les-erreurs-comme-ressources-et-le-retour-des-sous-ressources/</link><pubDate>Thu, 12 Oct 2023 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2023/10/12/api-platform-3.2-les-erreurs-comme-ressources-et-le-retour-des-sous-ressources/</guid><description>Part 3 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.2 fait des erreurs des ressources Problem Detail de première classe, rétablit les sous-ressources proprement, et rend les event listeners optionnels.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.2 est arrivé en octobre 2023 avec trois changements qui ont fait avancer le modèle d&rsquo;état : les erreurs sont devenues des ressources, les sous-ressources sont revenues sous une forme qui s&rsquo;intègre vraiment dans l&rsquo;architecture, et le dernier point d&rsquo;extension hérité — les event listeners — a été formellement remplacé.</p>
<h2 id="les-erreurs-comme-ressources">Les erreurs comme ressources</h2>
<p>Avant la 3.2, la gestion des erreurs était en dehors du modèle de ressources. Les exceptions étaient interceptées par un event listener Symfony et converties en réponse, avec un contrôle limité sur la forme de la sortie.</p>
<p>La 3.2 fait des erreurs des classes <code>ApiResource</code> de première classe conformes à la <a href="https://www.rfc-editor.org/rfc/rfc9457" target="_blank" rel="noopener noreferrer">RFC 9457</a>
 (Problem Details for HTTP APIs). La classe d&rsquo;erreur intégrée est <code>ApiPlatform\ApiResource\Error</code>, et on peut créer la sienne :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ErrorResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Exception\ProblemExceptionInterface</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#[ErrorResource]
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookNotFoundError</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">\RuntimeException</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ProblemExceptionInterface</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(<span style="color:#66d9ef">private</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $bookId)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">__construct</span>(<span style="color:#e6db74">&#34;Book </span><span style="color:#e6db74">$bookId</span><span style="color:#e6db74"> not found&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getType</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#39;/errors/book-not-found&#39;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Quand cette exception est levée n&rsquo;importe où dans la couche d&rsquo;état, API Platform l&rsquo;intercepte, la sérialise comme une réponse Problem Detail, et génère un schéma OpenAPI approprié pour elle. Le type d&rsquo;erreur, le titre, le détail et le statut font tous partie du contrat de la ressource — pas des chaînes codées en dur dans un listener.</p>
<h2 id="les-sous-ressources-sans-les-contournements">Les sous-ressources sans les contournements</h2>
<p>Les sous-ressources existaient en 2.x mais ont été supprimées en 3.0 parce qu&rsquo;elles étaient étroitement couplées à l&rsquo;ancien modèle de data provider et ne pouvaient pas être proprement mappées à la nouvelle architecture orientée opération. La 3.2 les réintroduit d&rsquo;une façon qui s&rsquo;intègre.</p>
<p>Une sous-ressource est une ressource accessible via l&rsquo;URI d&rsquo;une ressource parente. En 3.2, elle est déclarée directement sur la ressource enfant en utilisant <code>uriTemplate</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/books/{bookId}/reviews&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriVariables</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;bookId&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Link</span>(<span style="color:#a6e22e">fromClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Book</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>            ],
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Review</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le descripteur <code>Link</code> rend la relation explicite. Le provider reçoit <code>bookId</code> dans <code>$uriVariables</code> et peut l&rsquo;utiliser pour délimiter la requête. Pas d&rsquo;inférence magique, pas de jointures implicites — la structure d&rsquo;URI et l&rsquo;accès aux données sont tous les deux déclarés.</p>
<h2 id="canonical_uri_template-pour-plusieurs-chemins-daccès"><code>canonical_uri_template</code> pour plusieurs chemins d&rsquo;accès</h2>
<p>Quand une ressource est accessible via plusieurs URI (un endpoint direct et un endpoint de sous-ressource), OpenAPI doit savoir quelle URI est canonique pour les liens <code>$ref</code>. La 3.2 utilise le <code>uriTemplate</code> de niveau supérieur sur <code>ApiResource</code> comme URI canonique par défaut. Pour plus de contrôle, l&rsquo;option <code>canonical_uri_template</code> peut être passée via <code>extraProperties</code> sur n&rsquo;importe quelle opération pour la définir explicitement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/reviews/{id}&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Get</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/books/{bookId}/reviews&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">uriVariables</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;bookId&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Link</span>(<span style="color:#a6e22e">fromClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Book</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>)],
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Review</span> {}
</span></span></code></pre></div><p>La spec OpenAPI générée utilise l&rsquo;URI canonique pour les références de schéma, gardant le document cohérent quand une ressource apparaît sous plusieurs chemins.</p>
<h2 id="types-union-et-intersection">Types union et intersection</h2>
<p>La 3.2 ajoute le support des types union et intersection PHP dans la couche de métadonnées. Une propriété déclarée comme <code>Book|Magazine</code> génère un schéma <code>oneOf</code> approprié dans OpenAPI. C&rsquo;était auparavant non supporté — on devait tomber sur un <code>mixed</code> non typé ou annoter la propriété manuellement.</p>
<h2 id="les-event-listeners-rendus-optionnels">Les event listeners rendus optionnels</h2>
<p>Le dernier shim de compatibilité venu de la 2.x était la possibilité d&rsquo;utiliser les event listeners Symfony sur les événements <code>kernel.request</code> et <code>kernel.view</code> pour intercepter le flux de données d&rsquo;API Platform. La 3.2 ne les supprime pas, mais introduit un moyen de s&rsquo;en passer : passer <code>event_listeners_backward_compatibility_layer: false</code> dans la configuration d&rsquo;API Platform désactive entièrement les hooks basés sur les événements. Le remplacement est un provider ou processor décoré par un autre provider ou processor. Le hook basé sur les événements était avec état, dépendant de l&rsquo;ordre, et court-circuitait entièrement le contexte d&rsquo;opération. Les providers décorés reçoivent l&rsquo;objet opération et peuvent appeler le provider interne quand ils sont prêts.</p>
<h2 id="le-modèle-détat-est-maintenant-complet">Le modèle d&rsquo;état est maintenant complet</h2>
<p>La 3.0 a introduit l&rsquo;architecture. La 3.1 a ajouté la séparation ressource/entité. La 3.2 ferme les lacunes restantes : les erreurs ont un contrat de ressource, les sous-ressources ont un modèle de déclaration propre, et la couche d&rsquo;état couvre désormais tous les points d&rsquo;extension que les event listeners géraient autrefois. Les shims 2.x existent encore, mais s&rsquo;en passer n&rsquo;est plus qu&rsquo;une ligne de configuration.</p>
]]></content:encoded></item><item><title>API Platform 3.1 : votre ressource n'a pas à être votre entité</title><link>https://guillaumedelre.github.io/fr/2023/01/23/api-platform-3.1-votre-ressource-na-pas-%C3%A0-%C3%AAtre-votre-entit%C3%A9/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2023/01/23/api-platform-3.1-votre-ressource-na-pas-%C3%A0-%C3%AAtre-votre-entit%C3%A9/</guid><description>Part 2 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.1 découple les ressources API des entités Doctrine, introduit un PUT conforme à la spec, et collecte les erreurs de dénormalisation en liste.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>Quatre mois après la 3.0, API Platform 3.1 est arrivé avec le premier lot de fonctionnalités construites sur le nouveau modèle d&rsquo;état. Tous les changements ne sont pas spectaculaires, mais l&rsquo;un d&rsquo;eux résout un problème qui a engendré beaucoup de contournements alambiqués en 2.x : votre ressource API n&rsquo;a plus besoin d&rsquo;être votre entité Doctrine.</p>
<h2 id="la-séparation-ressourceentité">La séparation ressource/entité</h2>
<p>En 2.x, API Platform fonctionnait mieux quand votre ressource API et votre modèle de persistance étaient la même classe. Utiliser un DTO comme surface API était possible via le système Input/Output DTO, mais ce système a été supprimé en 3.0 — il compliquait le modèle d&rsquo;état sans apporter suffisamment de bénéfices.</p>
<p>La 3.1 le remplace par quelque chose de plus propre. Le paramètre <code>stateOptions</code> d&rsquo;une opération accepte un objet <code>DoctrineOrmOptions</code> qui pointe vers une entité différente :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Doctrine\Orm\State\Options</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">stateOptions</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Options</span>(<span style="color:#a6e22e">entityClass</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookEntity</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookDto</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $title;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $author;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Le provider reçoit les objets <code>BookEntity</code> de Doctrine et la couche de sérialisation les mappe sur <code>BookDto</code>. Les filtres Doctrine, la pagination et le tri fonctionnent tous sur <code>BookEntity</code>. La surface API expose <code>BookDto</code>. Les deux peuvent évoluer indépendamment.</p>
<p>Ça compte plus qu&rsquo;il n&rsquo;y paraît. Votre modèle de persistance accumule des champs internes, des relations et des colonnes qui n&rsquo;ont aucune raison d&rsquo;apparaître dans votre API. Avant la 3.1, on les exposait quand même ou on construisait un normaliseur élaboré pour les cacher. Maintenant on déclare ce que l&rsquo;API expose comme classe distincte et on laisse le framework gérer la correspondance.</p>
<h2 id="un-put-conforme-à-la-spec">Un PUT conforme à la spec</h2>
<p>Depuis la version 1.0, le handler PUT d&rsquo;API Platform mettait à jour des ressources existantes. Créer une ressource via PUT — ce que la spec HTTP autorise explicitement — n&rsquo;était pas supporté. La 3.1 ajoute la création basée sur l&rsquo;<code>uriTemplate</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[Put(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">uriTemplate</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/books/{id}&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">allowCreate</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>)]
</span></span></code></pre></div><p>Avec <code>allowCreate: true</code>, un PUT vers une URI qui n&rsquo;existe pas crée la ressource au lieu de retourner une 404. L&rsquo;identifiant vient de l&rsquo;URI, pas du corps de la requête. C&rsquo;est ce que la RFC 9110 décrit pour PUT : &ldquo;Si la ressource cible n&rsquo;a pas de représentation courante et que le PUT en crée une avec succès, le serveur d&rsquo;origine DOIT informer le user agent en envoyant une réponse 201 (Created).&rdquo;</p>
<p>C&rsquo;est un petit paramètre, mais il ouvre API Platform à des cas d&rsquo;usage — création idempotente, identifiants assignés par le client — qui nécessitaient auparavant un contrôleur personnalisé.</p>
<h2 id="les-erreurs-de-dénormalisation-collectées-pas-jetées">Les erreurs de dénormalisation collectées, pas jetées</h2>
<p>Avant la 3.1, les erreurs de désérialisation s&rsquo;arrêtaient au premier problème. Envoyer un corps de requête avec cinq champs invalides renvoyait une erreur sur le premier. On le corrigeait, on renvoyait, on trouvait le deuxième. À répéter cinq fois.</p>
<p>La 3.1 ajoute une option <code>collect_denormalization_errors</code> sur l&rsquo;opération qui change ce comportement :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">#[Post(collectDenormalizationErrors: true)]
</span></span></span></code></pre></div><p>Avec cette option activée, API Platform intercepte toutes les erreurs de type et les violations de contraintes pendant la désérialisation et les retourne sous forme de liste structurée dans la réponse, formatée de la même façon que les erreurs de validation. Un seul aller-retour, une vue complète.</p>
<h2 id="apiresourceopenapi-remplace-openapicontext"><code>ApiResource::openapi</code> remplace <code>openapiContext</code></h2>
<p>L&rsquo;ancien paramètre <code>openapiContext</code> acceptait un tableau brut fusionné dans le schéma OpenAPI généré — pratique mais non typé. La 3.1 introduit un paramètre <code>openapi</code> de premier ordre qui accepte un objet <code>OpenApiOperation</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\Operation</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\OpenApi\Model\RequestBody</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[Post(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">openapi</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Operation</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">requestBody</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">RequestBody</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">description</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Créer un livre&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">required</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">summary</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Créer une nouvelle entrée livre&#39;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>)]
</span></span></code></pre></div><p>L&rsquo;ancien tableau <code>openapiContext</code> fonctionne encore mais est déprécié. La nouvelle approche est typée, compatible avec l&rsquo;autocomplétion IDE, et se valide à la construction plutôt qu&rsquo;à la génération du schéma. Les backed enums PHP 8.1 obtiennent également une génération de schéma OpenAPI correcte en 3.1 — un champ typé comme backed enum produit un schéma avec des valeurs <code>enum</code> et le type correct, sans annotation supplémentaire.</p>
<h2 id="le-pattern-est-clair">Le pattern est clair</h2>
<p>La 3.0 a établi l&rsquo;architecture. La 3.1 montre ce que cette architecture permet : une séparation propre ressource/entité sans système DTO parallèle, une sémantique HTTP correcte selon la RFC, un meilleur rapport d&rsquo;erreurs. Aucune de ces fonctionnalités n&rsquo;aurait été aussi propre à implémenter sur le modèle data provider de la 2.x. Les features de la 3.1 sont la première preuve que la réécriture était la bonne décision.</p>
]]></content:encoded></item><item><title>API Platform 3.0 : un nouveau modèle d'état et la fin des DataProviders</title><link>https://guillaumedelre.github.io/fr/2022/11/18/api-platform-3.0-un-nouveau-mod%C3%A8le-d%C3%A9tat-et-la-fin-des-dataproviders/</link><pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/11/18/api-platform-3.0-un-nouveau-mod%C3%A8le-d%C3%A9tat-et-la-fin-des-dataproviders/</guid><description>Part 1 of 8 in &amp;quot;Sorties API Platform&amp;quot;: API Platform 3.0 a remplacé les DataProviders et DataPersisters par un modèle d&amp;#39;état qui rend les opérations HTTP explicites — et a exigé PHP 8.1 et Symfony 6 pour y arriver.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.0 est arrivé en septembre 2022 avec Symfony 6.1 comme prérequis strict et une architecture de base qui ne ressemblait en rien à la 2.x. Le guide de migration est long. La raison pour laquelle il est long est intéressante.</p>
<p>L&rsquo;ancien modèle avait une fuite conceptuelle. <code>DataProviderInterface</code> et <code>DataPersisterInterface</code> étaient appelés pour chaque requête HTTP, mais le provider recevait le contexte de l&rsquo;opération comme un indice — pas comme un contrat. Un provider de collection et un provider d&rsquo;item étaient des interfaces distinctes, mais les deux vivaient dans le même seau mental : &ldquo;choses qui retournent des données.&rdquo; La couche HTTP savait ce qui était demandé ; le provider devait reconstruire cette connaissance à partir d&rsquo;indices passés dans le tableau <code>$context</code>.</p>
<p>La 3.0 inverse le modèle. Les opérations sont déclarées en premier. L&rsquo;accès aux données est câblé aux opérations.</p>
<h2 id="les-state-providers-remplacent-les-data-providers">Les state providers remplacent les data providers</h2>
<p>L&rsquo;ancien <code>DataProviderInterface</code> a disparu. Le remplaçant est <code>ProviderInterface</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\State\ProviderInterface</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Operation</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookProvider</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ProviderInterface</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">provide</span>(<span style="color:#a6e22e">Operation</span> $operation, <span style="color:#66d9ef">array</span> $uriVariables <span style="color:#f92672">=</span> [], <span style="color:#66d9ef">array</span> $context <span style="color:#f92672">=</span> [])<span style="color:#f92672">:</span> <span style="color:#a6e22e">object</span><span style="color:#f92672">|</span><span style="color:#66d9ef">array</span><span style="color:#f92672">|</span><span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> ($operation <span style="color:#a6e22e">instanceof</span> <span style="color:#a6e22e">CollectionOperationInterface</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">repository</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">findAll</span>();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">repository</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">find</span>($uriVariables[<span style="color:#e6db74">&#39;id&#39;</span>]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>La différence n&rsquo;est pas syntaxique. En 2.x, on enregistrait un provider et API Platform l&rsquo;appelait pour toute ressource correspondante. En 3.0, on lie un provider à une opération spécifique. Le provider n&rsquo;a plus à deviner ce qui l&rsquo;a déclenché — l&rsquo;objet opération qu&rsquo;il reçoit est le contrat.</p>
<h2 id="les-state-processors-remplacent-les-data-persisters">Les state processors remplacent les data persisters</h2>
<p><code>DataPersisterInterface</code> avait le même problème côté écriture : une seule classe gérant la création, la mise à jour et la suppression, les distinguant en inspectant la méthode HTTP ou l&rsquo;état de l&rsquo;objet. <code>ProcessorInterface</code> reçoit l&rsquo;opération comme argument typé :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\State\ProcessorInterface</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Operation</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BookProcessor</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">ProcessorInterface</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">mixed</span> $data, <span style="color:#a6e22e">Operation</span> $operation, <span style="color:#66d9ef">array</span> $uriVariables <span style="color:#f92672">=</span> [], <span style="color:#66d9ef">array</span> $context <span style="color:#f92672">=</span> [])
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">entityManager</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">persist</span>($data);
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">entityManager</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">flush</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $data;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Plus utile encore : on peut lier un processor différent par opération. L&rsquo;opération de suppression en reçoit un qui supprime. L&rsquo;opération de création en reçoit un qui valide et stocke. Pas de switch, pas d&rsquo;inspection de méthode, pas de classe partagée qui essaie d&rsquo;être trois choses à la fois.</p>
<h2 id="les-opérations-déclarées-explicitement-en-attributs-php-81">Les opérations déclarées explicitement en attributs PHP 8.1</h2>
<p>L&rsquo;autre moitié de la 3.0 est la couche de métadonnées. Les annotations Doctrine sont remplacées par des attributs natifs PHP 8.1, et chaque opération est déclarée explicitement sur la classe ressource :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\ApiResource</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Get</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\GetCollection</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">ApiPlatform\Metadata\Post</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[ApiResource(
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">operations</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GetCollection</span>(<span style="color:#a6e22e">provider</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookProvider</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">provider</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookProvider</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Post</span>(<span style="color:#a6e22e">processor</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">BookProcessor</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span>),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Book</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>C&rsquo;est plus verbeux que <code>@ApiResource</code> avec ses valeurs par défaut magiques. C&rsquo;est aussi explicite. On sait exactement quelles opérations HTTP existent pour cette ressource, ce qui récupère les données, ce qui les écrit, et où vit la logique. Les valeurs par défaut de la 2.x étaient pratiques jusqu&rsquo;au jour où il fallait en surcharger une sans réussir à déterminer quel service décorer sans lire le code source.</p>
<h2 id="php-81-nétait-pas-un-hasard">PHP 8.1 n&rsquo;était pas un hasard</h2>
<p>L&rsquo;exigence stricte pour PHP 8.1 est structurante. Les callables de première classe rendent l&rsquo;enregistrement des filtres plus propre. L&rsquo;immuabilité des métadonnées d&rsquo;opération est assurée par un pattern de clonage (méthodes <code>withX()</code>) qui s&rsquo;appuie sur les arguments nommés et les propriétés de constructeur promues — des fondations PHP 8.0 sur lesquelles l&rsquo;architecture s&rsquo;appuie massivement.</p>
<p>Plus concrètement : l&rsquo;expression complète de l&rsquo;architecture 3.0 — opérations typées, providers scopés à l&rsquo;opération, métadonnées explicites — avait besoin de la 8.1 pour ne pas ressembler à des contournements. Abandonner PHP 7.x et 8.0 n&rsquo;était pas une décision de nettoyage.</p>
<h2 id="la-migration-est-un-vrai-travail">La migration est un vrai travail</h2>
<p>Le passage de 2.x à 3.0 n&rsquo;est pas un simple bump de version. Chaque <code>DataProvider</code> devient un <code>ProviderInterface</code>. Chaque <code>DataPersister</code> devient un <code>ProcessorInterface</code>. Les annotations deviennent des attributs. Les normaliseurs et filtres personnalisés peuvent nécessiter une restructuration. Le guide de mise à jour documente tout cela, mais &ldquo;documenté&rdquo; ne veut pas dire &ldquo;rapide.&rdquo;</p>
<p>Ce qu&rsquo;on obtient de l&rsquo;autre côté est une architecture qui passe à l&rsquo;échelle sans la complexité ambiante de la 2.x : plus besoin de deviner quelle interface implémenter, plus de chaînes <code>$this-&gt;supports()</code>, plus de valeurs par défaut invisibles qui surchargent silencieusement la config explicite.</p>
<p>La 3.0 est l&rsquo;API Platform qu&rsquo;on concevrait de zéro en sachant ce qu&rsquo;on sait après des années de 2.x. Le prix est la migration. Le numéro de version est honnête là-dessus.</p>
]]></content:encoded></item></channel></rss>