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.

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:
| Method | Mandatory | Description | Registered by default |
|---|---|---|---|
GET | yes | Retrieve the (paginated) list of elements | yes |
POST | no | Create a new element | yes |
Item operations:
| Method | Mandatory | Description | Registered by default |
|---|---|---|---|
GET | yes | Retrieve an element | yes |
PUT | no | Replace an element | no |
PATCH | no | Apply a partial modification to an element | yes |
DELETE | no | Delete an element | yes |
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.
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.
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
PATCHmethod must be enabled explicitly in the configuration, refer to theContent Negotiation section for more information.
With JSON Merge Patch, the
null values will be skipped in the response.
Current
PUTimplementation behaves more or less like thePATCHmethod. Existingproperties 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.
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>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:
POST operation never throws: creating a resource does not require one to already exist;PUT operation with allowCreate enabled never throws
either, since a missing item is exactly the “create it” case of the upsert behavior;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.
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.
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>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_statusattribute always wins over the operation’sstatusoption, souse it only when the code truly depends on runtime conditions. When the status is fixed per operation, the static
statusoption 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.
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
routePrioritywith the pre-existingpriorityoption:priorityonlyorders 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.
routePrioritycontrols 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.
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>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.
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!
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.
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);
}
}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