# 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 ```