# Creating Custom Operations and Symfony Controllers
> [!NOTE] Using custom Symfony controllers with API Platform is **discouraged**. Also, GraphQL is
> **not supported**.
> [For most use cases, better extension points, working both with REST and GraphQL, are available](../core/design.md).
> We recommend to use
> [System providers and processors](../core/extending.md#system-providers-and-processors) to extend
> API Platform internals.
API Platform can leverage the Symfony routing system to register custom operations related to custom
controllers. Such custom controllers can be any valid
[Symfony controller](https://symfony.com/doc/current/controller.html), including standard Symfony
controllers extending the
[`Symfony\Bundle\FrameworkBundle\Controller\AbstractController`](https://symfony.com/doc/current/controller.html#the-base-controller-class-services)
helper class.
To enable this feature use `use_symfony_listeners: true` in your `api_platform` configuration file:
```yaml
api_platform:
title: "My Dummy API"
description: |
This is a test API.
Made with love
use_symfony_listeners: true
```
However, API Platform recommends to use **action classes** instead of typical Symfony controllers.
Internally, API Platform implements the [Action-Domain-Responder](https://github.com/pmjones/adr)
pattern (ADR), a web-specific refinement of
[MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller).
The distribution of API Platform also eases the implementation of the ADR pattern: it automatically
registers action classes stored in `api/src/Controller` as autowired services.
Thanks to the [autowiring](https://symfony.com/doc/current/service_container/autowiring.html)
feature of the Symfony Dependency Injection container, services required by an action can be
type-hinted in its constructor, it will be automatically instantiated and injected, without having
to declare it explicitly.
In the following examples, the built-in `GET` operation is registered as well as a custom operation
called `post_publication`.
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 Symfony routing and always return a HTTP 404.
If you create a custom operation, you will probably want to properly document it. See the
[OpenAPI](../core/openapi.md) part of the documentation to do so.
First, let's create your custom operation:
```php
bookPublishingHandler->handle($book);
return $book;
}
}
```
This custom operation behaves exactly like the built-in operation: it returns a JSON-LD document
corresponding to the ID passed in the URL.
Here we consider that
[autowiring](https://symfony.com/doc/current/service_container/autowiring.html) is enabled for
controller classes (the default when using the API Platform distribution). This action will be
automatically registered as a service (the service name is the same as the class name:
`App\Controller\CreateBookPublication`).
API Platform automatically retrieves the appropriate PHP entity using the state provider then
deserializes user data in it, and for `POST`, `PUT` and `PATCH` requests updates the entity with
state provided by the user.
The entity is retrieved in the `__invoke` method thanks to a dedicated argument resolver.
When using `GET`, the `__invoke()` method parameter will receive the identifier and should be called
the same as the resource identifier. So for the path `/user/{uuid}/bookmarks`, you must use
`__invoke(string $uuid)`.
Services (`$bookPublishingHandler` here) are automatically injected thanks to the autowiring
feature. You can type-hint any service you need and it will be autowired too.
The `__invoke` method of the action is called when the matching route is hit. It can return either
an instance of `Symfony\Component\HttpFoundation\Response` (that will be displayed to the client
immediately by the Symfony kernel) or, like in this example, an instance of an entity mapped as a
resource (or a collection of instances for collection operations). In this case, the entity will
pass through [all built-in event listeners](../core/events.md#built-in-event-listeners) of API
Platform. It will be automatically validated, persisted and serialized in JSON-LD. Then the Symfony
kernel will send the resulting document to the client.
The routing has not been configured yet because we will add it at the resource configuration level:
```php
```
It is mandatory to set the `method`, `uriTemplate` and `controller` attributes. They allow API
Platform to configure the routing path and the associated controller respectively.
## Using the PlaceholderAction
Complex use cases may lead you to create multiple custom operations.
In such a case, you will probably create the same amount of custom controllers while you may not
need to perform custom logic inside.
To avoid that, API Platform provides the `ApiPlatform\Action\PlaceholderAction` which behaves the
same when using the [built-in operations](../core/operations.md#operations).
You just need to set the `controller` attribute with this class. Here, the previous example updated:
```php
// api/src/Entity/Book.php
namespace App\Entity;
use ApiPlatform\Action\PlaceholderAction;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
#[ApiResource(operations: [
new Get(),
new Post(
name: 'publication',
uriTemplate: '/books/{id}/publication',
controller: PlaceholderAction::class
)
])]
class Book
{
// ...
}
```
```yaml
# api/config/api_platform/resources.yaml
resources:
App\Entity\Book:
operations:
ApiPlatform\Metadata\Get: ~
post_publication:
class: ApiPlatform\Metadata\Post
method: POST
uriTemplate: /books/{id}/publication
controller: ApiPlatform\Action\PlaceholderAction
```
```xml
```
## Using Serialization Groups
You may want different serialization groups for your custom operations. Just configure the proper
`normalizationContext` and/or `denormalizationContext` in your operation:
```php
['publication']],
)
])]
class Book
{
// ...
#[Groups(['publication'])]
public $isbn;
// ...
}
```
```yaml
# api/config/api_platform/resources.yaml
resources:
App\Entity\Book:
operations:
ApiPlatform\Metadata\Get: ~
post_publication:
class: ApiPlatform\Metadata\Get
uriTemplate: /books/{id}/publication
controller: App\Controller\CreateBookPublication
normalizationContext:
groups: ["publication"]
```
```xml
publication
```
## Retrieving the Entity
If you want to bypass the automatic retrieval of the entity in your custom operation, you can set
`read: false` in the operation attribute:
```php
```
This way, it will skip the `ReadListener`. You can do the same for some other built-in listeners.
See [Built-in Event Listeners](../core/events.md#built-in-event-listeners) for more information.
In your custom controller, the `__invoke()` method parameter should be called the same as the entity
identifier. So for the path `/user/{uuid}/bookmarks`, you must use `__invoke(string $uuid)`.
## Alternative Method
There is another way to create a custom operation. However, we do not encourage its use. Indeed,
this one disperses the configuration at the same time in the routing and the resource configuration.
The `post_publication` operation references the Symfony route named `book_post_publication`.
Since version 2.3, you can also use the route name as operation name by convention, as shown in the
following example for `book_post_discontinuation` when neither `method` nor `routeName` attributes
are specified.
First, let's create your resource configuration:
```php
```
API Platform will automatically map this `post_publication` operation to the route
`book_post_publication`. Let's create a custom action and its related route using attributes:
```php
Book::class,
'_api_operation_name' => '_api_/books/{id}/publication_post',
],
)]
public function __invoke(Book $book): Book
{
$this->bookPublishingHandler->handle($book);
return $book;
}
}
```
It is mandatory to set `_api_resource_class` and `_api_operation_name` in the parameters of the
route (`defaults` key). It allows API Platform to work with the Symfony routing system.
Alternatively, you can also use a traditional Symfony controller and YAML or XML route declarations.
The following example does the same thing as the previous example:
```php
handle($book);
}
}
```
```yaml
# api/config/routes.yaml
book_post_publication:
path: /books/{id}/publication
methods: ["POST"]
defaults:
_controller: App\Controller\BookController::createPublication
_api_resource_class: App\Entity\Book
_api_operation_name: post_publication
```