main Operations

API Platform relies on the concept of operations. Operations can be applied to a resource exposed by the API. From an implementation point of view, an operation is a link between a resource, a route and its related controller.

Operations screencast
Watch the Operations screencast

API Platform automatically registers typical CRUD operations and describes them in the exposed documentation (Hydra and Swagger). It also creates and registers routes for these operations in the Symfony routing system, if available, or in the Laravel routing system, should that be the case.

The behavior of built-in operations is briefly presented in the Getting started guide.

The list of enabled operations can be configured on a per-resource basis. Creating custom operations on specific routes is also possible.

There are two types of operations: collection operations and item operations.

Collection operations act on a collection of resources. By default two operations are implemented: POST and GET. Item operations act on an individual resource. Three default operation are defined: GET, DELETE and PATCH. PATCH is supported with JSON Merge Patch (RFC 7396), or using the JSON:API format, as required by the specification.

The PUT operation is also supported, but is not registered by default.

When the ApiPlatform\Metadata\ApiResource annotation is applied to an entity class, the following built-in CRUD operations are automatically enabled:

Collection operations:

MethodMandatoryDescriptionRegistered by default
GETyesRetrieve the (paginated) list of elementsyes
POSTnoCreate a new elementyes

Item operations:

MethodMandatoryDescriptionRegistered by default
GETyesRetrieve an elementyes
PUTnoReplace an elementno
PATCHnoApply a partial modification to an elementyes
DELETEnoDelete an elementyes

# The HTTP QUERY Operation

HTTP QUERY is a safe, idempotent collection operation whose criteria are sent in the request body instead of the URI. It is useful when a collection query is too large or too structured for a URL. API Platform does not enable it by default; add a Query operation explicitly.

Unlike GET, a QUERY request must include a Content-Type header, including when its body is empty. API Platform supports application/json and application/x-www-form-urlencoded request bodies for this operation.

The following operation uses a parameter-driven filter. Although it is declared with QueryParameter, the name criterion is sent in the QUERY request body, not as ?name=... in the URL:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Query;
use ApiPlatform\Metadata\QueryParameter;

#[ApiResource(operations: [
    new GetCollection(),
    new Query(parameters: [
        'name' => new QueryParameter(
            filter: new PartialSearchFilter(),
            property: 'name',
        ),
    ]),
])]
class Book
{
    // ...
}

Call the QUERY operation with the same collection URI:

curl -X QUERY https://example.com/books \
    -H 'Accept: application/ld+json' \
    -H 'Content-Type: application/json' \
    --data '{"name":"Dune"}'

The parsed values are processed by the same parameter and filter system as URL query parameters. This lets existing QueryParameter filters describe and apply body criteria without a custom provider.

# Criteria DTOs

For a structured query, set an input class on the operation and put QueryParameter attributes on its properties. API Platform uses that class as the request-body schema and discovers its parameters to apply their filters:

<?php
// api/src/Dto/BookCriteria.php
namespace App\Dto;

use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
use ApiPlatform\Metadata\QueryParameter;

final class BookCriteria
{
    #[QueryParameter(filter: new PartialSearchFilter())]
    public ?string $name = null;
}
<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Query;
use App\Dto\BookCriteria;

#[ApiResource(operations: [new Query(input: BookCriteria::class)])]
class Book
{
    // ...
}

The input class is not deserialized and passed to a state provider by default. With the default Query settings, it documents the body and declares the filter criteria; providers continue to use the usual provider arguments and request context.

When the query itself is a command-like operation and a processor needs a typed criteria object, disable the read stage and explicitly enable deserialization and writing. The processor then receives the deserialized BookCriteria object as its $data argument:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Query;
use App\Dto\BookCriteria;
use App\State\BookCriteriaProcessor;

#[ApiResource(operations: [
    new Query(
        input: BookCriteria::class,
        read: false,
        deserialize: true,
        write: true,
        processor: BookCriteriaProcessor::class,
    ),
])]
class Book
{
    // ...
}
<?php
// api/src/State/BookCriteriaProcessor.php
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Dto\BookCriteria;
use App\Entity\Book;

/** @implements ProcessorInterface<BookCriteria, iterable<Book>> */
final readonly class BookCriteriaProcessor implements ProcessorInterface
{
    public function __construct(private BookSearch $bookSearch) {}

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): iterable
    {
        if (!$data instanceof BookCriteria) {
            throw new \RutimeException('Expected BookCriteria.');
        }

        return $this->bookSearch->search($data);
    }
}

This is the processor path: a processor is only called for a safe operation when write is set to true. See State Processors for implementing the processor.

# OpenAPI

When exporting an OpenAPI 3.2 document, API Platform represents the operation in the Path Item Object’s query field. Its request body lists application/json and application/x-www-form-urlencoded; parameter-driven criteria are represented as body properties. For an input criteria class, the request body references that class’s input schema. Path and header parameters remain OpenAPI parameters.

The PATCH method must be enabled explicitly in the configuration, refer to the

Content Negotiation section for more information.


With JSON Merge Patch, the

null values will be skipped in the response.


Current PUT implementation behaves more or less like the PATCH method. Existing

properties not included in the payload are not removed, their current values are preserved. To remove an existing property, its value must be explicitly set to null.

# Upsert: Creating a Resource With PUT

By default, sending a PUT request to an item that does not exist returns a 404 Not Found. To enable an “upsert” behavior (update the resource if it exists, create it otherwise), set the allowCreate property to true on the PUT operation. The identifier provided in the URI is then used for the new resource and a 201 Created response is returned when the item did not exist.

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Put;

#[ApiResource(
    operations: [
        new Put(allowCreate: true),
    ]
)]
class Book
{
    // ...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        operations:
            ApiPlatform\Metadata\Put:
                allowCreate: true
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->
<resources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns="https://api-platform.com/schema/metadata/resources-3.0"
           xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
           https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <operations>
            <operation class="ApiPlatform\Metadata\Put" allowCreate="true" />
        </operations>
    </resource>
</resources>

# Controlling the 404 Response When Data Is Missing

Available since API Platform 4.4.

When the provider returns null (no matching entity, or a provider that has nothing to give back for this request), API Platform’s ReadProvider decides whether to throw a 404 Not Found or to let the request through with null data. By default, that decision depends on the HTTP method:

  • a POST operation never throws: creating a resource does not require one to already exist;
  • a PUT operation with allowCreate enabled never throws either, since a missing item is exactly the “create it” case of the upsert behavior;
  • every other operation (GET, GetCollection, PATCH, DELETE, or a PUT without allowCreate) throws a 404 Not Found when the provider returns null.

Set the throwOnNotFound property to false to opt out of this default and let the operation proceed with null data, or to true to force the 404 even on an operation that would not throw by default (for instance a PUT with allowCreate: true for which you still want a strict “must already exist” semantics).

A common use case for throwOnNotFound: false is an operation whose provider legitimately returns null as valid data, for example a “current user” or “current cart” endpoint that returns null when none is set instead of failing:

<?php
// api/src/Entity/Cart.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;

#[ApiResource(
    operations: [
        new Get(
            uriTemplate: '/cart/current',
            provider: CurrentCartProvider::class,
            throwOnNotFound: false
        ),
    ]
)]
class Cart
{
    // ...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Cart:
        operations:
            ApiPlatform\Metadata\Get:
                uriTemplate: "/cart/current"
                provider: App\State\CurrentCartProvider
                throwOnNotFound: false
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->
<resources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns="https://api-platform.com/schema/metadata/resources-3.0"
           xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
           https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Cart">
        <operations>
            <operation class="ApiPlatform\Metadata\Get" uriTemplate="/cart/current"
                       provider="App\State\CurrentCartProvider" throwOnNotFound="false" />
        </operations>
    </resource>
</resources>

With throwOnNotFound: false, the null value reaches the rest of the pipeline (normalization, custom processors, and so on) instead of interrupting the request with an exception, so the controller and later stages must be prepared to handle a null resource.

# Enabling and Disabling Operations

If no operation is specified, all default CRUD operations are automatically registered. It is also possible - and recommended for large projects - to define operations explicitly.

Keep in mind that once you explicitly set up an operation, the automatically registered CRUD will no longer be. If you declare even one operation manually, such as #[GET], you must declare the others manually as well if you need them.

Operations can be configured using attributes, XML or YAML. In the following examples, we enable only the built-in operation for the GET method for both collection and item to create a readonly endpoint.

If the operation’s name matches a supported HTTP method (GET, POST, PUT, PATCH or DELETE), the corresponding method property will be automatically added.


In Symfony we use the term “entities”, while the following documentation is mostly for

Laravel “models”.

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
    operations: [
        new Get(),
        new GetCollection()
    ]
)]
class Book
{
    // ...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        operations:
            ApiPlatform\Metadata\GetCollection: ~ # nothing more to add if we want to keep the default controller
            ApiPlatform\Metadata\Get: ~
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <operations>
            <operation class="ApiPlatform\Metadata\Get" />
            <operation class="ApiPlatform\Metadata\GetCollection" />
        </operations>
    </resource>
</resources>

The previous example can also be written with an explicit method definition:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(
    operations: [
        new Get(),
        new GetCollection()
    ]
)]
class Book
{
    // ...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        operations:
            ApiPlatform\Metadata\GetCollection:
                method: GET
            ApiPlatform\Metadata\Get:
                method: GET
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <operations>
            <operation class="ApiPlatform\Metadata\GetCollection" />
            <operation class="ApiPlatform\Metadata\Get" method="GET" />
        </operations>
    </resource>
</resources>

API Platform is smart enough to automatically register the applicable Symfony route referencing a built-in CRUD action just by specifying the method name as key, or by checking the explicitly configured HTTP method.

By default, API Platform uses the first Get operation defined to generate the IRI of an item and the first GetCollection operation to generate the IRI of a collection.

If your resource does not have any Get operation, API Platform automatically adds an operation to help generating this IRI. If your resource has any identifier, this operation will look like /books/{id}. But if your resource doesn’t have any identifier, API Platform will use the Skolem format /.well-known/genid/{id}. Those routes are not exposed from any documentation (for instance OpenAPI), but are anyway declared on the routing system and always return a HTTP 404.

# Configuring Operations

The URL, the method and the default status code (among other options) can be configured per operation.

In the next example, both GET and POST operations are registered with custom URLs. Those will override the URLs generated by default. In addition to that, we require the id parameter in the URL of the GET operation to be an integer, and we configure the status code generated after successful POST request to be 301:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;

#[ApiResource(operations: [
    new Get(
        uriTemplate: '/grimoire/{id}',
        requirements: ['id' => '\d+'],
        defaults: ['color' => 'brown'],
        options: ['my_option' => 'my_option_value'],
        schemes: ['https'],
        host: '{subdomain}.api-platform.com'
    ),
    new Post(
        uriTemplate: '/grimoire',
        status: 301
    )
])]
class Book
{
    //...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        operations:
            ApiPlatform\Metadata\Post:
                uriTemplate: "/grimoire"
                status: 301
            ApiPlatform\Metadata\Get:
                uriTemplate: "/grimoire/{id}"
                requirements:
                    id: '\d+'
                defaults:
                    color: "brown"
                host: "{subdomain}.api-platform.com"
                schemes: ["https"]
                options:
                    my_option: "my_option_value"
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <operations>
            <operation class="ApiPlatform\Metadata\Post" uriTemplate="/grimoire" status="301" />
            <operation class="ApiPlatform\Metadata\Get" uriTemplate="/grimoire/{id}" host="{subdomain}.api-platform.com">
                <requirements>
                    <requirement property="id">\d+</requirement>
                </requirements>
                <defaults>
                    <values>
                        <value name="color">brown</value>
                    </values>
                </defaults>
                <schemes>
                    <scheme>https</scheme>
                </schemes>
                <options>
                    <values>
                        <value name="color">brown</value>
                    </values>
                </options>
            </operation>
        </operations>
    </resource>
</resources>

When you do not want to allow access to the resource item (i.e. you don’t want a GET item operation), instead of omitting the resource item altogether, you can explicitly specify the IRI of the resource item by declaring a GET item operation that returns HTTP 404 (Not Found).

For Laravel applications, the same behavior can be implemented using the ApiPlatform\Laravel\Controller\NotExposedController.

For example:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Action\NotFoundAction;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;

#[ApiResource(operations: [
    new Get(
        uriTemplate: '/grimoire/{id}',
        controller: NotFoundAction::class,
        read: false,
        output: false
    ),
    new Post(
        uriTemplate: '/grimoire',
        status: 301
    )
])]
class Book
{
    // ...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        operations:
            ApiPlatform\Metadata\Post:
                uriTemplate: "/grimoire"
                status: 301
            ApiPlatform\Metadata\Get:
                uriTemplate: "/grimoire/{id}"
                controller: ApiPlatform\Action\NotFoundAction
                read: false
                output: false
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <operations>
            <operation class="ApiPlatform\Metadata\Post" uriTemplate="/grimoire" status="301" />
            <operation class="ApiPlatform\Metadata\Get" uriTemplate="/grimoire/{id}"
                       controller="ApiPlatform\Action\NotFoundAction" read="false" output="false" />
        </operations>
    </resource>
</resources>

# Setting the Response Status Code at Runtime

Available since API Platform 4.4.

The status option shown above is static: it is the right tool when the response code for an operation is fixed and known when you configure it. Sometimes, though, the status code can only be decided while the request is being handled, for example a state processor that returns 202 Accepted when a task is queued for later processing but 200 OK when it completes synchronously.

For this case, RespondProcessor reads a _api_response_status request attribute before falling back to the operation’s static status (or to the framework default). Set it from a custom state processor to override the status code for the current request only:

<?php
// api/src/State/ImportBookProcessor.php
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Symfony\Component\HttpFoundation\Response;

final class ImportBookProcessor implements ProcessorInterface
{
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        $request = $context['request'];

        if ($this->isQueuedForAsyncImport($data)) {
            $request->attributes->set('_api_response_status', Response::HTTP_ACCEPTED);
        }

        // ... persist $data, return it or a DTO

        return $data;
    }
}

The _api_response_status attribute always wins over the operation’s status option, so

use it only when the code truly depends on runtime conditions. When the status is fixed per operation, the static status option documented above remains the right tool: it is visible in the resource metadata and in the generated OpenAPI/Hydra documentation, while a request attribute set at runtime is not.

# Setting the Route Matching Priority

Symfony’s router matches an incoming URL against every registered route, in order, and stops at the first one that fits. When a resource combines a static, custom URI template with the default, parameterized one, the static route must be tried first, or it never gets a chance to match. Take a Book resource that has a default Get item operation on /books/{id} and a custom GetCollection operation exposing the “featured” books at /books/featured: because /books/featured also fits the /books/{id} pattern (id becomes the string featured), whichever route is registered first wins. If the item operation happens to load before the featured one, requests to /books/featured are routed to Get with id: 'featured' instead of reaching the intended operation.

The routePriority option is available on the standard CRUD HTTP operations: Get, GetCollection, Post, Put, Patch, and Delete. It tells the Symfony router which route to try first: the higher the value, the earlier the route is checked, regardless of the order in which operations are declared. It accepts any integer (negative values are allowed to deprioritize a route) and defaults to 0 when omitted.

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;

#[ApiResource(operations: [
    new Get(uriTemplate: '/books/{id}'),
    new GetCollection(
        uriTemplate: '/books/featured',
        routePriority: 1,
    ),
])]
class Book
{
    //...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        operations:
            ApiPlatform\Metadata\Get:
                uriTemplate: "/books/{id}"
            ApiPlatform\Metadata\GetCollection:
                uriTemplate: "/books/featured"
                routePriority: 1
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <operations>
            <operation class="ApiPlatform\Metadata\Get" uriTemplate="/books/{id}" />
            <operation class="ApiPlatform\Metadata\GetCollection" uriTemplate="/books/featured" routePriority="1" />
        </operations>
    </resource>
</resources>

With routePriority: 1 set on the /books/featured operation, its route is now checked before /books/{id}, so GET /books/featured reaches the intended GetCollection operation, and every other /books/{id} request still falls through to Get.

Do not confuse routePriority with the pre-existing priority option: priority only

orders operations within a resource’s own operation list (used, for instance, to determine which operation generates a resource’s IRI) and sorts ascending — a lower value comes first. routePriority controls Symfony route matching order and sorts descending — a higher value is matched first. The two options are unrelated and are intentionally kept separate to avoid this confusion.

# Prefixing All Routes of All Operations

Sometimes it’s also useful to put a whole resource into its own “namespace” regarding the URI. Let’s say you want to put everything that’s related to a Book into the library so that URIs become library/book/{id}. In that case you don’t need to override all the operations to set the path but configure the routePrefix attribute for the whole entity instead:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(routePrefix: '/library')]
class Book
{
    //...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        routePrefix: /library
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book" routePrefix="/library" />
</resources>

# Defining Which Operation to Use to Generate the IRI

Using multiple operations on your resource, you may want to specify which operation to use to generate the IRI, instead of letting API Platform use the first one it finds.

Let’s say you have 2 resources in relationship: Company and User, where a company has multiple users. You can declare the following routes:

  • /users
  • /users/{id}
  • /companies/{companyId}/users
  • /companies/{companyId}/users/{id}

The first routes (/users...) are only accessible by the admin, and the others by regular users. Calling /companies/{companyId}/users should return IRIs matching /companies/{companyId}/users/{id} to not expose an admin route to regular users.

To do so, use the itemUriTemplate option only available on GetCollection and Post operations:

<?php
// api/src/Entity/User.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;

#[GetCollection] // auto-generated path will be /users
#[Get] // auto-generated path will be /users/{id}
#[GetCollection(uriTemplate: '/companies/{companyId}/users', itemUriTemplate: '/companies/{companyId}/users/{id}'/*, ... */)]
#[Post(uriTemplate: '/companies/{companyId}/users', itemUriTemplate: '/companies/{companyId}/users/{id}'/*, ... */)]
#[Get(uriTemplate: '/companies/{companyId}/users/{id}'/*, ... */)]
class User
{
    //...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\User:
        - operations:
              ApiPlatform\Metadata\GetCollection: ~
              ApiPlatform\Metadata\Get: ~
        - operations:
              ApiPlatform\Metadata\GetCollection:
                  uriTemplate: /companies/{companyId}/users
                  itemUriTemplate: /companies/{companyId}/users/{id}
                  # ...
              ApiPlatform\Metadata\Post:
                  uriTemplate: /companies/{companyId}/users
                  itemUriTemplate: /companies/{companyId}/users/{id}
                  # ...
              ApiPlatform\Metadata\Get:
                  uriTemplate: /companies/{companyId}/users/{id}
                  # ...
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/api_platform/resources.xml -->

<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
        https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\User">
        <operations>
            <operation class="ApiPlatform\Metadata\GetCollection" />
            <operation class="ApiPlatform\Metadata\Get" />
        </operations>
    </resource>

    <resource class="App\Entity\User">
        <operations>
            <operation class="ApiPlatform\Metadata\GetCollection" uriTemplate="/companies/{companyId}/users" itemUriTemplate="/companies/{companyId}/users/{id}" />
            <operation class="ApiPlatform\Metadata\Post" uriTemplate="/companies/{companyId}/users" itemUriTemplate="/companies/{companyId}/users/{id}" />
            <operation class="ApiPlatform\Metadata\Get" uriTemplate="/companies/{companyId}/users/{id}" />
        </operations>
    </resource>
</resources>

API Platform will find the operation matching this itemUriTemplate and use it to generate the IRI.

If this option is not set, the first Get operation is used to generate the IRI.

# Expose a Model Without Any Routes

Sometimes, you may want to expose a model, but want it to be used through subrequests only, and never through item or collection operations. Because the OpenAPI standard requires at least one route to be exposed to make your models consumable, let’s see how you can manage this kind of issue.

Let’s say you have the following entities in your project:

<?php
// api/src/Entity/Place.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Place
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    #[ORM\Column]
    private string $name = '';

    #[ORM\Column(type: 'float')]
    private float $latitude = 0;

    #[ORM\Column(type: 'float')]
    private float $longitude = 0;

    // ...
}
<?php
// api/src/Entity/Weather.php
namespace App\Entity;

class Weather
{
    private float $temperature;

    private float $pressure;

    // ...
}

We don’t save the Weather entity in the database, since we want to return the weather in real time when it is queried. Because we want to get the weather for a known place, it is more reasonable to query it through a subresource of the Place entity, so let’s do this:

<?php
// api/src/Entity/Place.php
namespace App\Entity;

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\ApiResource;
use App\Controller\GetWeather;
use Doctrine\ORM\Mapping as ORM;

#[ApiResource(
    operations: [
        new Get(),
        new Put(),
        new Delete(),
        new Get(name: 'weather', uriTemplate: '/places/{id}/weather', controller: GetWeather::class),
        new GetCollection(),
        new Post(),
    ]
)]
#[ORM\Entity]
class Place
{
    // ...

The GetWeather controller fetches the weather for the given city and returns an instance of the Weather entity. This implies that API Platform has to know about this entity, so we will need to make it an API resource too:

<?php
// api/src/Entity/Weather.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(operations: [])]
class Weather
{
    // ...

That’s it!

# Customize Operation and Resource Metadata

Metadata mutators allow a dynamic control over resources and operations, by programmatically altering metadata before they are exposed as endpoints. Providing a way to modify, add or remove operations, adjust serialization groups or pagination settings.

It also makes it possible to customize built-in endpoints from a third-party API, such as Sylius.

# Resource Mutator

Use the resource mutator to modify the entire resource metadata by adding the attribute and target resource class as argument:

<?php

namespace App;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\AsResourceMutator;
use ApiPlatform\Metadata\ResourceMutatorInterface;
use App\Entity\Book;
use App\Entity\Comic;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

// Metadata mutators are repeatable
#[AsResourceMutator(resourceClass: Book::class)]
#[AsResourceMutator(resourceClass: Comic::class)]
final readonly class ApiPrefixMutator implements ResourceMutatorInterface
{
    public function __construct(#[Autowire(env: 'API_PREFIX')] private string $prefix) {
    }

    public function __invoke(ApiResource $resource): ApiResource
    {
        $operations = $resource->getOperations();

        if (null === $operations) {
            return $resource;
        }

        foreach ($operations as $name => $operation) {
            // add route prefix to each resource operation
            $prefixedOperation = $operation->withRoutePrefix($this->prefix);
            $operations->add($name, $prefixedOperation);
        }

        return $resource->withOperations($operations);
    }
}

# Operation Mutator

The operation mutator will modify a specific operation’s metadata, by using the attribute and passing the operation name:

<?php

namespace App\Mutator;

use ApiPlatform\Metadata\AsOperationMutator;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\OperationMutatorInterface;

#[AsOperationMutator(operationName: '_api_Book_get_collection')]
final class BookOperationMutator implements OperationMutatorInterface
{
    public function __invoke(Operation $operation): Operation
    {
        $context = $operation->getNormalizationContext() ?? [];
        // add another group to normalization group
        $context['groups'][] = 'review:list:read';

        return $operation->withNormalizationContext($context);
    }
}

Operation mutators are executed during metadata loading, the result is stored in cache so

runtime logic is prohibited.


You can also help us improve the documentation of this page.

Using an AI coding agent? See the documentation index for LLMs at /docs/llms.txt.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop