# 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](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 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](getting-started.md#mapping-the-entities) 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)](https://www.rfc-editor.org/rfc/rfc7386), or [using the JSON:API format](https://jsonapi.org/format/#crud-updating), 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 | ## The HTTP QUERY Operation [HTTP QUERY](https://www.rfc-editor.org/rfc/rfc10008.html) 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 new QueryParameter( filter: new PartialSearchFilter(), property: 'name', ), ]), ])] class Book { // ... } ``` Call the `QUERY` operation with the same collection URI: ```console 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](filters.md) 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 > */ 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](state-processors.md) 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. > [!NOTE] The `PATCH` method must be enabled explicitly in the configuration, refer to the > [Content Negotiation](content-negotiation.md) section for more information. --- > [!NOTE] With JSON Merge Patch, the > [null values will be skipped](https://symfony.com/doc/current/components/serializer.html#skipping-null-values) > in the response. --- > [!NOTE] 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 ``` ## 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`](#upsert-creating-a-resource-with-put) 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 ``` 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. --- > [!NOTE] In Symfony we use the term “entities”, while the following documentation is mostly for > Laravel “models”. ```php ``` The previous example can also be written with an explicit method definition: ```php ``` 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 '\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 { //... } ``` ```yaml # 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 \d+ brown https brown ``` 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 ``` ## 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 isQueuedForAsyncImport($data)) { $request->attributes->set('_api_response_status', Response::HTTP_ACCEPTED); } // ... persist $data, return it or a DTO return $data; } } ``` > [!NOTE] 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 ``` 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`. > [!NOTE] 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 ``` ## 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 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 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 getNormalizationContext() ?? []; // add another group to normalization group $context['groups'][] = 'review:list:read'; return $operation->withNormalizationContext($context); } } ``` > [!NOTE] Operation mutators are executed during metadata loading, the result is stored in cache so > runtime logic is prohibited. ---