<?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>Openapi on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/openapi/</link><description>Recent content in Openapi on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Thu, 18 Sep 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/openapi/index.xml" rel="self" type="application/rss+xml"/><item><title>API Platform 4.2: JSON streamer, ObjectMapper, and autoconfigure</title><link>https://guillaumedelre.github.io/2025/09/18/api-platform-4.2-json-streamer-objectmapper-and-autoconfigure/</link><pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2025/09/18/api-platform-4.2-json-streamer-objectmapper-and-autoconfigure/</guid><description>Part 8 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 4.2 streams large JSON collections without buffering, replaces manual DTO mapping with ObjectMapper, and autoconfigures resources from their class attributes.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.2 arrived in September 2025. Three changes stand out: a JSON streamer for large collections that avoids buffering the entire response in memory, an ObjectMapper that replaces the manual wiring in <code>stateOptions</code>-based DTO flows, and autoconfiguration of <code>#[ApiResource]</code> without explicit service registration.</p>
<h2 id="json-streamer-for-large-collections">JSON streamer for large collections</h2>
<p>The default Symfony serializer builds the full response in memory before writing it to the output. For a collection of 10,000 items, this means allocating a PHP array, serializing it to a string, and keeping both in memory until the response is flushed. At scale, this is the source of the OOM errors that force people to add pagination everywhere.</p>
<p>4.2 adds a streaming JSON encoder that writes the response incrementally:</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>With streaming enabled, the response is written directly to the output buffer as each item is serialized. Memory usage stays roughly constant regardless of collection size. The trade-off: you cannot set response headers after streaming starts, and the HTTP status code must be committed before the first byte is written.</p>
<h2 id="objectmapper-replaces-manual-dto-wiring">ObjectMapper replaces manual DTO wiring</h2>
<p>3.1 introduced <code>stateOptions</code> with <code>DoctrineOrmOptions</code> for separating the API resource from the Doctrine entity. The provider received entity objects and the serializer mapped them to the DTO. This worked, but the mapping was implicit — the serializer used property names to match fields, and anything that did not match was either ignored or caused a normalization error.</p>
<p>4.2 introduces <code>ObjectMapper</code>, a declarative mapping layer between entities and 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>The <code>#[Map]</code> attribute tells ObjectMapper that <code>BookDto</code> can be populated from <code>BookEntity</code>. Field names are matched by convention; mismatches are declared explicitly at the property level:</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>The dot notation traverses nested objects. The mapping runs before serialization and replaces the implicit property-matching behavior of the serializer. Unmapped fields raise an error at configuration time, not at runtime.</p>
<p>ObjectMapper works with <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>The provider fetches <code>BookEntity</code> objects from Doctrine. ObjectMapper converts them to <code>BookDto</code> instances. The serializer writes the DTO. Three distinct steps, each with a clear contract.</p>
<h2 id="typeinfo-integration-throughout-the-stack">TypeInfo integration throughout the stack</h2>
<p>Symfony 7.1 introduced the <a href="https://symfony.com/doc/current/components/type_info.html" target="_blank" rel="noopener noreferrer">TypeInfo component</a>
, a unified type introspection layer that understands union types, intersection types, generic collections, and nullable types across reflection, PHPDoc, and PHP 8.x syntax.</p>
<p>4.2 replaces API Platform&rsquo;s internal type resolution with TypeInfo. This affects filter schema generation, OpenAPI schema inference, and the serializer&rsquo;s type coercion. The visible benefit is that types that previously generated incorrect or missing OpenAPI schemas — <code>Collection&lt;int, Book&gt;</code>, <code>list&lt;string&gt;</code>, intersection types — now produce accurate schemas without manual <code>@ApiProperty</code> annotations.</p>
<h2 id="autoconfigure-apiresource">Autoconfigure <code>#[ApiResource]</code></h2>
<p>Before 4.2, adding <code>#[ApiResource]</code> to a class was sufficient for Hugo to discover it only if the class was in a path scanned by API Platform&rsquo;s resource loader. Outside that path, you needed explicit service configuration.</p>
<p>4.2 hooks into Symfony&rsquo;s autoconfigure system. Any class tagged with <code>#[ApiResource]</code> is automatically registered as a resource regardless of its location, as long as it is in a directory covered by Symfony&rsquo;s component scan. No <code>config/services.yaml</code> entry needed.</p>
<p>For Laravel, the equivalent uses Laravel&rsquo;s service provider autoloading — Eloquent models with <code>#[ApiResource]</code> are picked up automatically without manual registration.</p>
<h2 id="doctrine-existsfilter">Doctrine ExistsFilter</h2>
<p>The <code>ExistsFilter</code> constrains a collection by whether a nullable relation or field is set:</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> returns books where <code>publishedAt</code> is not null. <code>exists[publishedAt]=false</code> returns books where it is null.</p>
]]></content:encoded></item><item><title>API Platform 4.1: strict query params, multi-spec OpenAPI, and GraphQL limits</title><link>https://guillaumedelre.github.io/2025/02/28/api-platform-4.1-strict-query-params-multi-spec-openapi-and-graphql-limits/</link><pubDate>Fri, 28 Feb 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2025/02/28/api-platform-4.1-strict-query-params-multi-spec-openapi-and-graphql-limits/</guid><description>Part 7 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 4.1 formalizes strict query parameter validation, introduces x-apiplatform-tag for splitting one API into multiple OpenAPI specs, and adds depth and complexity limits for GraphQL.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.1 arrived in February 2025 with a batch of features that are less about new capabilities and more about making the existing ones production-ready. Strict query param validation gets a first-class property. OpenAPI gains a mechanism for splitting large APIs into separate specs. GraphQL gets the abuse prevention controls it was missing.</p>
<h2 id="strict-query-parameter-validation">Strict query parameter validation</h2>
<p>3.3 introduced query parameter validation as opt-in. 3.4 deprecated the loose behavior. 4.1 formalizes it with a native <code>strictQueryParameterValidation</code> property on resources and operations: when set to <code>true</code>, unknown query parameters return 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>Declared parameters pass through; undeclared parameters are rejected. To disable strict validation on a specific operation when it is enabled at the resource level, set <code>strictQueryParameterValidation: false</code> on that operation.</p>
<h2 id="x-apiplatform-tag-for-multi-spec-openapi"><code>x-apiplatform-tag</code> for multi-spec OpenAPI</h2>
<p>Large APIs often need multiple OpenAPI specs: one per team, one per API version, one internal and one public. Before 4.1, the generated spec was one document, and splitting it required post-processing or separate API Platform instances.</p>
<p>4.1 adds an <code>x-apiplatform-tag</code> vendor extension (no trailing <code>s</code>). You tag operations with logical group names via the <code>extensionProperties</code> of an OpenAPI <code>Operation</code> object, then request the spec filtered to one or more groups:</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>Requesting <code>/api/docs.json?filter_tags[]=public</code> returns only the operations tagged <code>public</code>. The full spec is still available without a filter. Groups do not affect the actual API behavior — they are a documentation-layer concern only.</p>
<p>This makes it feasible to maintain one API Platform configuration while serving different spec views to different consumers: a public Swagger UI, a partner portal, and an internal tool that exposes admin endpoints.</p>
<h2 id="http-authentication-in-swagger-ui">HTTP authentication in Swagger UI</h2>
<p>Before 4.1, the Swagger UI bundled with API Platform supported Bearer token authentication via its &ldquo;Authorize&rdquo; dialog. API Key and HTTP Basic authentication were not wired in.</p>
<p>4.1 adds support for multiple security schemes in the generated OpenAPI document. Security schemes are added by decorating the <code>OpenApiFactory</code> and modifying the <code>components.securitySchemes</code> object of the spec. Each declared scheme then appears in Swagger UI&rsquo;s &ldquo;Authorize&rdquo; dialog and is applied to requests made from the UI. This is a documentation and developer experience improvement — the actual authentication logic in your application is not affected.</p>
<h2 id="graphql-query-depth-and-complexity-limits">GraphQL query depth and complexity limits</h2>
<p>GraphQL&rsquo;s recursive query structure makes it trivial to craft a query that is small in bytes but enormous in execution cost. Without limits, a nested query four levels deep across a many-to-many relation can hit the database hundreds of times.</p>
<p>4.1 adds configurable depth and complexity limits:</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> is the maximum nesting level. <code>max_query_complexity</code> assigns a cost to each field and rejects queries whose total cost exceeds the threshold. Queries that exceed either limit are rejected before execution with a 400 response.</p>
<p>There is no universally correct value for these limits — they depend on your schema shape and expected query patterns. The defaults are intentionally permissive to avoid breaking existing APIs on upgrade. Tightening them is a deliberate configuration choice.</p>
<h2 id="operation-level-output-formats">Operation-level output formats</h2>
<p>4.0 and earlier configured accepted and returned content types at the API level. 4.1 lets you narrow this per operation:</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>Operations that do not specify formats inherit the API-level configuration. This is useful for endpoints that need to return a specific format (a CSV export, a binary stream) without changing the defaults for the rest of the API.</p>
]]></content:encoded></item><item><title>API Platform 4.0: Laravel support and PUT rethought</title><link>https://guillaumedelre.github.io/2024/09/27/api-platform-4.0-laravel-support-and-put-rethought/</link><pubDate>Fri, 27 Sep 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2024/09/27/api-platform-4.0-laravel-support-and-put-rethought/</guid><description>Part 6 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 4.0 brings first-class Laravel support with Eloquent and policies, and removes PUT from default operations to fix a long-standing semantic ambiguity.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 4.0 shipped nine days after 3.4, in late September 2024. The version number is honest: there is no new architecture, and the migration from 3.4 is short if you resolved the deprecations. What makes this a major is the scope change — API Platform is no longer a Symfony-only framework — and one opinionated default that reverses six years of PUT behavior.</p>
<h2 id="laravel-as-a-first-class-target">Laravel as a first-class target</h2>
<p>Since its first release, API Platform was built on Symfony. The HTTP layer, metadata, serializer, and Doctrine bridge all assumed Symfony&rsquo;s container, event dispatcher, and request lifecycle. Laravel users could run API Platform through a thin adapter, but filters, security, and Doctrine integration did not work on Eloquent.</p>
<p>4.0 ships a dedicated Laravel bridge. It maps API Platform&rsquo;s state layer onto Laravel&rsquo;s request lifecycle, integrates with Eloquent models directly, and wires into Laravel&rsquo;s authorization system:</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>Authorization uses Laravel policies and gates rather than Symfony&rsquo;s security voters. Operations expose a dedicated <code>policy</code> parameter that maps to a policy method name:</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 maps the <code>policy</code> value to Laravel&rsquo;s <code>Gate::allows()</code> with the model instance. Policies can also be auto-detected: if a model has a registered policy class, API Platform infers the correct method (<code>view</code>, <code>viewAny</code>, <code>create</code>, <code>update</code>, <code>delete</code>) based on the operation type. Filters for Eloquent collections cover the same ground as their Doctrine counterparts: <code>PartialSearchFilter</code>, <code>EqualsFilter</code>, <code>RangeFilter</code>, <code>OrderFilter</code>, <code>DateFilter</code>, and search variants (<code>StartSearchFilter</code>, <code>EndSearchFilter</code>). Pagination, sorting, and validation work through Laravel&rsquo;s native mechanisms.</p>
<p>This is not a compatibility shim. The Laravel bridge is maintained alongside the Symfony bridge and is covered by the same test suite. Projects using either framework get the same resource definition API.</p>
<h2 id="put-removed-from-default-operations">PUT removed from default operations</h2>
<p>Since API Platform 1.0, <code>#[ApiResource]</code> without an explicit <code>operations</code> array generated CRUD operations including PUT. The PUT handler updated existing resources and, after 3.1, could also create them via <code>allowCreate: true</code>.</p>
<p>4.0 removes PUT from the default set. <code>#[ApiResource]</code> now generates GET, POST, PATCH, and DELETE. To use PUT, you must declare it explicitly:</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">// ... other operations
</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>The motivation is semantic clarity. PATCH replaces PUT for most partial-update use cases. PUT&rsquo;s semantics — replace the entire resource representation — are rarely what an API actually implements, but the default made it appear in every API unless actively removed. Making PUT opt-in aligns the defaults with how HTTP semantics are actually used in practice.</p>
<h2 id="php-82-minimum">PHP 8.2 minimum</h2>
<p>4.0 drops PHP 8.0 and 8.1. PHP 8.2 is the new minimum. The readonly class syntax, <code>AllowDynamicProperties</code>, and DNF<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> types introduced in 8.2 are available throughout the codebase. No specific 8.2 feature is load-bearing for 4.0 — the version bump is primarily about dropping the older maintenance burden.</p>
<h2 id="symfony-64-and-doctrine-orm-217-minimum">Symfony 6.4+ and Doctrine ORM 2.17+ minimum</h2>
<p>On the Symfony side, 4.0 requires Symfony 6.4 or 7.x and Doctrine ORM 2.17 or 3.x. Both were already supported in 3.4. The migration from 3.4 to 4.0 on the Symfony track is: resolve 3.4 deprecations, verify you are on Symfony 6.4+ and ORM 2.17+, then upgrade. No new migration work is required if those are already in place.</p>
<h2 id="what-40-is-not">What 4.0 is not</h2>
<p>4.0 is not a new architecture. The state providers, processors, and resource metadata model from 3.0 are unchanged. The Laravel bridge adds a new execution context but does not change how resources or operations are declared. The split is intentional: if 3.0 was the &ldquo;what&rdquo;, 4.0 is the &ldquo;where&rdquo;.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Disjunctive Normal Form types: intersection types combined with union, like <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 as resources and DBAL 4 support</title><link>https://guillaumedelre.github.io/2024/09/18/api-platform-3.4-backedenum-as-resources-and-dbal-4-support/</link><pubDate>Wed, 18 Sep 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2024/09/18/api-platform-3.4-backedenum-as-resources-and-dbal-4-support/</guid><description>Part 5 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 3.4 makes BackedEnum classes full API resources, adds a BackedEnumFilter, supports security expressions on parameters, and adds DBAL 4 support.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.4 landed in September 2024 as the last minor before the 4.0 jump. The headline feature is BackedEnum as full resources — not just a typed field, but an enum that is itself an API endpoint.</p>
<h2 id="backedenum-as-api-resources">BackedEnum as API resources</h2>
<p>Since PHP 8.1, BackedEnum classes have a fixed set of cases with string or integer backing values. API Platform 3.4 lets you put <code>#[ApiResource]</code> directly on a 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>A <code>GET /book_statuses</code> endpoint returns the list of cases. Each case is serialized with its name and value. The endpoint is read-only — enums are immutable by nature.</p>
<p>This is mostly useful for frontend consumers that want a machine-readable list of valid values without hardcoding them. The alternative was a custom controller or a dedicated DTO resource listing the enum values manually.</p>
<h2 id="backedenumfilter">BackedEnumFilter</h2>
<p>The companion to enum resources is <code>BackedEnumFilter</code>, a new filter for Doctrine collections that constrains a query by a BackedEnum property:</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> filters the collection to books where <code>status</code> equals <code>BookStatus::Published</code>. Invalid enum values return a 400 response. Before this filter, you had to either write a custom filter or use <code>SearchFilter</code> and validate the value manually.</p>
<h2 id="security-expressions-on-parameters">Security expressions on parameters</h2>
<p>3.3 added security to links and properties. 3.4 extends this to query parameters. A parameter can declare a security expression that controls whether it is accepted at all:</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>When the security expression is false, the parameter is rejected with a 403, not silently ignored. This is more explicit than checking the user&rsquo;s role inside the provider after receiving the parameter.</p>
<h2 id="dbal-4-support-added">DBAL 4 support added</h2>
<p>3.4 adds support for Doctrine DBAL 4, which ships with type system changes that affect how custom types and platform-specific SQL work. The Doctrine Orm filters and query extensions in API Platform were updated to work with the new DBAL 4 type API.</p>
<p>Both DBAL 3 (<code>^3.4.0</code>) and DBAL 4 are supported simultaneously in 3.4. This is the release to upgrade to if you want to adopt DBAL 4 while staying on a stable API Platform 3.x branch.</p>
<h2 id="query-parameter-validator-deprecated">Query parameter validator deprecated</h2>
<p>3.3 added the strict query parameter validator as an opt-in. 3.4 deprecates the old behavior (unknown parameters silently ignored) in preparation for making strict validation the default in 4.0. Projects that relied on pass-through query parameters have one more release to declare them explicitly.</p>
<h2 id="last-stop-before-40">Last stop before 4.0</h2>
<p>3.4 is the last 3.x release with new features. Anything 3.x that was deprecated by 3.4 is gone in 4.0. The migration path from 3.4 to 4.0 is intentionally short: resolve the deprecations, then upgrade.</p>
]]></content:encoded></item><item><title>API Platform 3.3: headers, link security, and OpenAPI webhooks</title><link>https://guillaumedelre.github.io/2024/04/29/api-platform-3.3-headers-link-security-and-openapi-webhooks/</link><pubDate>Mon, 29 Apr 2024 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2024/04/29/api-platform-3.3-headers-link-security-and-openapi-webhooks/</guid><description>Part 4 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 3.3 adds declarative header configuration, fine-grained link security on sub-resources, and OpenAPI webhook support.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.3 shipped in April 2024 with a set of targeted additions. None of them reshape the architecture — 3.2 already closed that chapter. What 3.3 adds is control over things that were previously either hardcoded or required a workaround: response headers, link visibility on sub-resources, and webhooks in the generated spec.</p>
<h2 id="declarative-header-configuration">Declarative header configuration</h2>
<p>Before 3.3, setting custom response headers required either a custom processor that modified the response object or a Symfony event listener on <code>kernel.response</code>. Both approaches worked but lived outside the resource definition.</p>
<p>3.3 adds a <code>parameters</code> parameter to operation metadata:</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>For headers that vary per response (like <code>Cache-Control</code> with a computed max-age), the processor can still set them directly on the response. The <code>headers</code> parameter is primarily for documenting expected headers in the OpenAPI spec and for static header values.</p>
<h2 id="link-security-on-sub-resources">Link security on sub-resources</h2>
<p>When a resource exposes links to related resources, those links appear in the serialized output regardless of whether the current user can access the linked resource. This creates a disclosure problem: a user who can read a book but not its author profile still sees the author&rsquo;s URI in the response.</p>
<p>3.3 adds security expressions to the <code>Link</code> descriptor:</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>The link is omitted from the response when the security expression evaluates to false. The linked resource itself is not affected — only whether the current response includes the reference to it.</p>
<h2 id="apipropertysecurity"><code>ApiProperty::security</code></h2>
<p>The same security expression mechanism is available at the property level via <code>ApiProperty::security</code>. This lets you hide individual fields based on the current user without writing a custom normalizer:</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>The property is excluded from serialization when the expression is false. This is cleaner than a normalizer for the common case of role-gated fields.</p>
<h2 id="openapi-webhooks">OpenAPI webhooks</h2>
<p><a href="https://spec.openapis.org/oas/v3.1.0" target="_blank" rel="noopener noreferrer">OpenAPI 3.1</a>
 supports webhooks — outbound HTTP calls that your API makes to registered listeners — in the spec document itself. Before 3.3, there was no way to document these in API Platform&rsquo;s generated spec.</p>
<p>3.3 adds a <code>Webhook</code> class you pass to the <code>openapi</code> parameter of an operation. Declare a dedicated PHP class with <code>#[ApiResource]</code> and use <code>Webhook</code> on each operation to describe the outbound call shape:</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;A book was created&#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>The webhook definitions appear in the generated spec under the <code>webhooks</code> key alongside regular paths. Swagger UI renders them in a separate section.</p>
<h2 id="swagger-ui-deep-linking">Swagger UI deep linking</h2>
<p>Swagger UI supports deep linking — bookmarkable URLs that open directly to a specific operation in the interface. Before 3.3, the API Platform integration did not enable this. 3.3 turns on the Swagger UI <code>deepLinking</code> option, 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>With this enabled, the URL fragment updates as you navigate the UI, and pasting or sharing the URL opens the same operation. Useful when writing docs that link directly to a specific endpoint.</p>
<h2 id="strict-query-parameter-validation">Strict query parameter validation</h2>
<p>3.3 tightens the query parameter validator: parameters not declared on the operation now return a 400 response instead of being silently ignored. This behavior is 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>The intent is to catch typos and API misuse early. If you rely on pass-through query parameters for custom logic (logging, feature flags), you need to declare them explicitly on the operation before enabling this.</p>
]]></content:encoded></item><item><title>API Platform 3.2: errors as resources and sub-resources come back</title><link>https://guillaumedelre.github.io/2023/10/12/api-platform-3.2-errors-as-resources-and-sub-resources-come-back/</link><pubDate>Thu, 12 Oct 2023 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2023/10/12/api-platform-3.2-errors-as-resources-and-sub-resources-come-back/</guid><description>Part 3 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 3.2 makes errors first-class Problem Detail resources, brings back sub-resources cleanly, and makes event listeners optional.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.2 arrived in October 2023 with three changes that pushed the state model further: errors became resources, sub-resources came back in a form that actually fits the architecture, and the last legacy extension point — event listeners — was formally replaced.</p>
<h2 id="errors-as-resources">Errors as resources</h2>
<p>Before 3.2, error handling was outside the resource model. Exceptions were caught by a Symfony event listener and converted to a response, with limited control over the shape of the output.</p>
<p>3.2 makes errors first-class <code>ApiResource</code> classes compliant with <a href="https://www.rfc-editor.org/rfc/rfc9457" target="_blank" rel="noopener noreferrer">RFC 9457</a>
 (Problem Details for HTTP APIs). The built-in error class is <code>ApiPlatform\ApiResource\Error</code>, and you can create your own:</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>When this exception is thrown anywhere in the state layer, API Platform catches it, serializes it as a Problem Detail response, and generates a proper OpenAPI schema for it. The error type, title, detail, and status are all part of the resource contract — not hardcoded strings in a listener.</p>
<h2 id="sub-resources-without-the-workarounds">Sub-resources without the workarounds</h2>
<p>Sub-resources existed in 2.x but were removed in 3.0 because they were tightly coupled to the old data provider model and couldn&rsquo;t be cleanly mapped to the new operation-first architecture. 3.2 reintroduces them in a way that fits.</p>
<p>A sub-resource is a resource accessible through a parent resource&rsquo;s URI. In 3.2, it is declared directly on the child resource using <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>The <code>Link</code> descriptor makes the relationship explicit. The provider receives <code>bookId</code> in <code>$uriVariables</code> and can use it to scope the query. No magic inference, no implicit joins — the URI structure and the data access are both declared.</p>
<h2 id="canonical_uri_template-for-multiple-access-paths"><code>canonical_uri_template</code> for multiple access paths</h2>
<p>When a resource is accessible through multiple URIs (a direct endpoint and a sub-resource endpoint), OpenAPI needs to know which URI is canonical for <code>$ref</code> links. 3.2 uses the top-level <code>uriTemplate</code> on <code>ApiResource</code> as the default canonical URI. For finer control, the <code>canonical_uri_template</code> option can be passed via <code>extraProperties</code> on any operation to override it explicitly.</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>The generated OpenAPI spec uses the canonical URI for schema references, keeping the document consistent when a resource appears under several paths.</p>
<h2 id="union-and-intersection-types">Union and intersection types</h2>
<p>3.2 adds support for PHP union and intersection types in the metadata layer. A property declared as <code>Book|Magazine</code> generates a proper <code>oneOf</code> schema in OpenAPI. This was previously unsupported — you had to fall back to an untyped <code>mixed</code> or annotate the property manually.</p>
<h2 id="event-listeners-made-optional">Event listeners made optional</h2>
<p>The last compatibility shim from 2.x was the ability to use Symfony event listeners on the <code>kernel.request</code> and <code>kernel.view</code> events to intercept API Platform&rsquo;s data flow. 3.2 does not remove them, but introduces an opt-out: setting <code>event_listeners_backward_compatibility_layer: false</code> in the API Platform configuration disables the event-based hooks entirely. The replacement is a provider or processor decorated with another provider or processor. The event-based hook was stateful, order-dependent, and bypassed the operation context entirely. Decorated providers get the operation object and can call the inner provider when ready.</p>
<h2 id="the-state-model-is-now-complete">The state model is now complete</h2>
<p>3.0 introduced the architecture. 3.1 added resource/entity separation. 3.2 closes the remaining gaps: errors have a resource contract, sub-resources have a clean declaration model, and the state layer now covers every extension point that event listeners once handled. The 2.x shims still exist, but opting out of them is now a single config flag.</p>
]]></content:encoded></item><item><title>API Platform 3.1: your resource doesn't have to be your entity</title><link>https://guillaumedelre.github.io/2023/01/23/api-platform-3.1-your-resource-doesnt-have-to-be-your-entity/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2023/01/23/api-platform-3.1-your-resource-doesnt-have-to-be-your-entity/</guid><description>Part 2 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 3.1 decouples API resources from Doctrine entities, ships a spec-compliant PUT, and collects denormalization errors as a list.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>Four months after 3.0, API Platform 3.1 arrived with the first batch of features built on the new state model. Not every change is dramatic, but one of them solves a problem that drove a lot of convoluted workarounds in 2.x: your API resource no longer needs to be your Doctrine entity.</p>
<h2 id="the-resourceentity-split">The resource/entity split</h2>
<p>In 2.x, API Platform worked best when your API resource and your persistence model were the same class. Using a DTO as the API surface was possible through the Input/Output DTO system, but that system was removed in 3.0 — it complicated the state model without enough benefit.</p>
<p>3.1 replaces it with something cleaner. The <code>stateOptions</code> parameter on an operation accepts a <code>DoctrineOrmOptions</code> object that points to a different entity:</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>The provider receives the <code>BookEntity</code> objects from Doctrine and the serialization layer maps them to <code>BookDto</code>. The Doctrine filters, pagination, and ordering all work on <code>BookEntity</code>. The API surface exposes <code>BookDto</code>. The two can evolve independently.</p>
<p>This matters more than it looks. Your persistence model accumulates internal fields, relations, and columns that have no business being in your API. Before 3.1, you either exposed them anyway or built an elaborate normalizer to hide them. Now you declare what the API looks like as a separate class and let the framework handle the mapping.</p>
<h2 id="put-that-follows-the-spec">PUT that follows the spec</h2>
<p>Since version 1.0, API Platform&rsquo;s PUT handler updated existing resources. Creating a resource via PUT — which the HTTP spec explicitly allows — was not supported. 3.1 adds <code>uriTemplate</code>-based creation:</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>With <code>allowCreate: true</code>, a PUT to a URI that does not exist creates the resource instead of returning 404. The identifier comes from the URI, not from the request body. This is what RFC 9110 describes for PUT: &ldquo;If the target resource does not have a current representation and the PUT successfully creates one, then the origin server MUST inform the user agent by sending a 201 (Created) response.&rdquo;</p>
<p>It is a small flag, but it opens API Platform to use cases — idempotent resource creation, client-assigned identifiers — that previously required a custom controller.</p>
<h2 id="denormalization-errors-collected-not-thrown">Denormalization errors collected, not thrown</h2>
<p>Before 3.1, deserialization errors stopped at the first problem. Send a request body with five invalid fields and get an error about the first one. Fix it, send again, find the second. Repeat five times.</p>
<p>3.1 adds a <code>collect_denormalization_errors</code> option on the operation that changes this behavior:</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>With this enabled, API Platform catches all type errors and constraint violations during deserialization and returns them as a structured list in the response, formatted the same way as validation errors. One round-trip, full picture.</p>
<h2 id="apiresourceopenapi-replaces-openapicontext"><code>ApiResource::openapi</code> replaces <code>openapiContext</code></h2>
<p>The old <code>openapiContext</code> parameter accepted a raw array that was merged into the generated OpenAPI schema — convenient but untyped. 3.1 introduces a first-class <code>openapi</code> parameter that accepts an <code>OpenApiOperation</code> object:</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;Create a book&#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;Create a new book entry&#39;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>)]
</span></span></code></pre></div><p>The old <code>openapiContext</code> array still works but is deprecated. The new approach is typed, IDE-friendly, and validates at construction time rather than at schema generation time. PHP 8.1 backed enums also get proper OpenAPI schema generation in 3.1 — a field typed as a backed enum produces a schema with <code>enum</code> values and the correct type, without any annotation.</p>
<h2 id="the-pattern-is-clear">The pattern is clear</h2>
<p>3.0 established the architecture. 3.1 shows what that architecture enables: clean resource/entity separation without a parallel DTO system, RFC-correct HTTP semantics, better error reporting. None of these would have been as clean to implement on the 2.x data provider model. The features in 3.1 are the first proof that the rewrite was the right call.</p>
]]></content:encoded></item><item><title>API Platform 3.0: a new state model and the end of DataProviders</title><link>https://guillaumedelre.github.io/2022/11/18/api-platform-3.0-a-new-state-model-and-the-end-of-dataproviders/</link><pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2022/11/18/api-platform-3.0-a-new-state-model-and-the-end-of-dataproviders/</guid><description>Part 1 of 8 in &amp;quot;API Platform Releases&amp;quot;: API Platform 3.0 replaced DataProviders and DataPersisters with a state model that makes HTTP operations explicit — and required PHP 8.1 and Symfony 6 to do it.</description><category>api-platform-releases</category><content:encoded><![CDATA[<p>API Platform 3.0 arrived in September 2022 with Symfony 6.1 as a hard minimum and a core architecture that looked nothing like 2.x. The migration guide is long. The reason it&rsquo;s long is interesting.</p>
<p>The old model had a conceptual leak. <code>DataProviderInterface</code> and <code>DataPersisterInterface</code> were called for every HTTP request, but the provider received the operation context as a hint — not as a contract. A collection provider and an item provider were separate interfaces, but both lived in the same mental bucket: &ldquo;things that return data.&rdquo; The HTTP layer knew what was being requested; the provider had to reconstruct that knowledge from context clues passed in the <code>$context</code> array.</p>
<p>3.0 inverts the model. Operations are declared first. Data access is wired to operations.</p>
<h2 id="state-providers-replaced-data-providers">State providers replaced data providers</h2>
<p>The old <code>DataProviderInterface</code> is gone. The replacement is <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>The difference is not syntactic. In 2.x, you registered a provider and API Platform called it for any matching resource. In 3.0, you bind a provider to a specific operation. The provider no longer guesses what triggered it — the operation object it receives is the contract.</p>
<h2 id="state-processors-replaced-data-persisters">State processors replaced data persisters</h2>
<p><code>DataPersisterInterface</code> had the same problem on the write side: one class handling create, update, and delete, distinguishing them by inspecting the HTTP method or the object state. <code>ProcessorInterface</code> receives the operation as a typed argument:</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>More usefully: you can bind a different processor per operation. The delete operation gets one that removes. The post operation gets one that validates and stores. No switch statement, no method inspection, no shared class trying to be three things at once.</p>
<h2 id="operations-declared-explicitly-in-php-81-attributes">Operations declared explicitly in PHP 8.1 attributes</h2>
<p>The other half of 3.0 is the metadata layer. Doctrine annotations are replaced by PHP 8.1 native attributes, and each operation is declared explicitly on the resource class:</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>This is more verbose than <code>@ApiResource</code> with magic defaults. It is also explicit. You know exactly what HTTP operations exist for this resource, what retrieves data, what writes it, and where the logic lives. The defaults of 2.x were convenient until the day you needed to override one and couldn&rsquo;t figure out which service to decorate without reading the source.</p>
<h2 id="php-81-was-not-a-coincidence">PHP 8.1 was not a coincidence</h2>
<p>The hard requirement for PHP 8.1 is load-bearing. First-class callables make filter registration cleaner. The immutability of operation metadata is enforced through cloning patterns (<code>withX()</code> methods) that rely on named arguments and promoted constructor properties — PHP 8.0 foundations the architecture builds on heavily.</p>
<p>More practically: the full expression of 3.0&rsquo;s architecture — typed operations, operation-scoped providers, explicit metadata — needed 8.1 to not feel like workarounds. Dropping PHP 7.x and 8.0 was not a housekeeping decision.</p>
<h2 id="the-migration-is-real-work">The migration is real work</h2>
<p>The jump from 2.x to 3.0 is not a version bump. Every <code>DataProvider</code> becomes a <code>ProviderInterface</code>. Every <code>DataPersister</code> becomes a <code>ProcessorInterface</code>. Annotations become attributes. Custom normalizers and filters may need restructuring. The upgrade guide documents all of it, but &ldquo;documented&rdquo; does not mean &ldquo;fast.&rdquo;</p>
<p>What you get on the other side is an architecture that scales without the ambient complexity of 2.x: no more guessing which interface to implement, no more <code>$this-&gt;supports()</code> chains, no more invisible defaults quietly overriding explicit config.</p>
<p>3.0 is the API Platform you&rsquo;d design from scratch knowing what you know after years of 2.x. The price is the migration. The version number is honest about that.</p>
]]></content:encoded></item></channel></rss>