Skip to content

Commit cce80a2

Browse files
committed
Change the recommended way to create custom operations
1 parent 6c1125b commit cce80a2

File tree

3 files changed

+204
-35
lines changed

3 files changed

+204
-35
lines changed

core/events.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ Constant | Event | Priority |
105105
`POST_RESPOND` | `kernel.response` | 0 |
106106

107107
Some of those built-in listeners can be enabled/disabled by setting request attributes ([for instance in the `defaults`
108-
attribute of an operation](operations.md#creating-custom-operations-and-controllers)):
108+
attribute of an operation](operations.md#recommended-method)):
109109

110110
Listener | Parameter | Values | Default | Description |
111111
----------------------|----------------|----------------|---------|----------------------------------------|

core/operations.md

Lines changed: 201 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,8 @@ Symfony controllers extending the [`Symfony\Bundle\FrameworkBundle\Controller\Co
471471
helper class.
472472

473473
However, API Platform recommends to use **action classes** instead of typical Symfony controllers. Internally, API Platform
474-
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).
474+
implements the [Action-Domain-Responder](https://github.com/pmjones/adr) pattern (ADR), a web-specific refinement of
475+
[MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller).
475476

476477
Note: [the event system](events.md) should be preferred over custom controllers when applicable.
477478

@@ -482,10 +483,199 @@ Thanks to the [autowiring](http://symfony.com/doc/current/components/dependency_
482483
Symfony Dependency Injection container, services required by an action can be type-hinted in its constructor, it will be
483484
automatically instantiated and injected, without having to declare it explicitly.
484485

485-
In the following example, the built-in `GET` operation is registered as well as a custom operation called `special`.
486+
In the following examples, the built-in `GET` operation is registered as well as a custom operation called `special`.
486487
The `special` operation reference the Symfony route named `book_special`.
487488

488-
Note: API Platform uses the first operation (with `GET` method) defined in `collectionOperations` to generate the IRI for this resource class. This means that as long as you dont want to overwrite the IRI for this resource class by intention, the default collection operation associated with the `GET` method should be the first operation defined inside collection operations.
489+
Note: By default, API Platform uses the first `GET` operation defined in `collectionOperations` to generate the IRI for
490+
a resource class.
491+
492+
### Recommended Method
493+
494+
First, let's create your custom operation:
495+
496+
```php
497+
<?php
498+
// api/src/Controller/BookSpecial.php
499+
500+
namespace App\Controller;
501+
502+
use App\Entity\Book;
503+
504+
class BookSpecial
505+
{
506+
private $myService;
507+
508+
public function __construct(MyService $myService)
509+
{
510+
$this->myService = $myService;
511+
}
512+
513+
public function __invoke(Book $data): Book
514+
{
515+
$this->myService->doSomething($data);
516+
517+
return $data;
518+
}
519+
}
520+
```
521+
522+
This custom operation behaves exactly like the built-in operation: it returns a JSON-LD document corresponding to the id
523+
passed in the URL.
524+
525+
Here we consider that [autowiring](https://symfony.com/doc/current/service_container/autowiring.html) is enabled for
526+
controller classes (the default when using the API Platform distribution).
527+
This action will be automatically registered as a service (the service name is the same as the class name:
528+
`App\Controller\BookSpecial`).
529+
530+
API Platform automatically retrieves the appropriate PHP entity using the data provider then deserializes user data in it,
531+
and for `POST` and `PUT` requests updates the entity with data provided by the user.
532+
By convention, the action's parameter must be called `$data`.
533+
534+
Services (`$myService` here) are automatically injected thanks to the autowiring feature. You can type-hint any service
535+
you need and it will be autowired too.
536+
537+
The `__invoke` method of the action is called when the matching route is hit. It can return either an instance of
538+
`Symfony\Component\HttpFoundation\Response` (that will be displayed to the client immediately by the Symfony kernel) or,
539+
like in this example, an instance of an entity mapped as a resource (or a collection of instances for collection operations).
540+
In this case, the entity will pass through [all built-in event listeners](events.md) of API Platform. It will be
541+
automatically validated, persisted and serialized in JSON-LD. Then the Symfony kernel will send the resulting document to
542+
the client.
543+
544+
The routing has not been configured yet because we will add it at the resource configuration level:
545+
546+
```php
547+
<?php
548+
// src/Entity/Book.php
549+
550+
use ApiPlatform\Core\Annotation\ApiResource;
551+
use App\Controller\BookSpecial;
552+
553+
/**
554+
* @ApiResource(itemOperations={
555+
* "get",
556+
* "special"={
557+
* "path"="/books/{id}/special",
558+
* "controller"=BookSpecial::class
559+
* }
560+
* })
561+
*/
562+
class Book
563+
{
564+
//...
565+
}
566+
```
567+
568+
Or in YAML:
569+
570+
```yaml
571+
# api/config/api_platform/resources.yaml
572+
App\Entity\Book:
573+
itemOperations:
574+
get: ~
575+
special:
576+
path: '/books/{id}/special'
577+
controller: 'App\Controller\BookSpecial'
578+
```
579+
580+
Or in XML:
581+
582+
```xml
583+
<?xml version="1.0" encoding="UTF-8" ?>
584+
<!-- api/config/api_platform/resources.xml -->
585+
586+
<resources xmlns="https://api-platform.com/schema/metadata"
587+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
588+
xsi:schemaLocation="https://api-platform.com/schema/metadata
589+
https://api-platform.com/schema/metadata/metadata-2.0.xsd">
590+
<resource class="App\Entity\Book">
591+
<itemOperations>
592+
<itemOperation name="get" />
593+
<itemOperation name="special">
594+
<attribute name="path">/books/{id}/special</attribute>
595+
<attribute name="controller">App\Controller\BookSpecial</attribute>
596+
</itemOperation>
597+
</itemOperations>
598+
</resource>
599+
</resources>
600+
```
601+
602+
It is mandatory to set the `path` and `controller` attributes. They allow API platform to configure the routing path and
603+
the associated controller respectively.
604+
605+
If you want to bypass the automatic retrieval of the entity in your custom operation, you can set the parameter
606+
`_api_receive` to `false` in the `default` attribute:
607+
608+
```php
609+
<?php
610+
// src/Entity/Book.php
611+
612+
use ApiPlatform\Core\Annotation\ApiResource;
613+
use App\Controller\BookSpecial;
614+
615+
/**
616+
* @ApiResource(itemOperations={
617+
* "get",
618+
* "special"={
619+
* "path"="/books/{id}/special",
620+
* "controller"=BookSpecial::class,
621+
* "defaults"={"_api_receive"=false}
622+
* }
623+
* })
624+
*/
625+
class Book
626+
{
627+
//...
628+
}
629+
```
630+
631+
Or in YAML:
632+
633+
```yaml
634+
# api/config/api_platform/resources.yaml
635+
App\Entity\Book:
636+
itemOperations:
637+
get: ~
638+
special:
639+
path: '/books/{id}/special'
640+
controller: 'App\Controller\BookSpecial'
641+
defaults:
642+
_api_receive: false
643+
```
644+
645+
Or in XML:
646+
647+
```xml
648+
<?xml version="1.0" encoding="UTF-8" ?>
649+
<!-- api/config/api_platform/resources.xml -->
650+
651+
<resources xmlns="https://api-platform.com/schema/metadata"
652+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
653+
xsi:schemaLocation="https://api-platform.com/schema/metadata
654+
https://api-platform.com/schema/metadata/metadata-2.0.xsd">
655+
<resource class="App\Entity\Book">
656+
<itemOperations>
657+
<itemOperation name="get" />
658+
<itemOperation name="special">
659+
<attribute name="path">/books/{id}/special</attribute>
660+
<attribute name="controller">App\Controller\BookSpecial</attribute>
661+
<attribute name="defaults">
662+
<attribute name="_api_receive">false</attribute>
663+
</attribute>
664+
</itemOperation>
665+
</itemOperations>
666+
</resource>
667+
</resources>
668+
```
669+
670+
This way, it will skip the `Read`, `Deserialize` and `Validate` listeners (see [the event system](events.md) for more
671+
information).
672+
673+
### Alternative Method
674+
675+
There is another way to create a custom operation. However, we do not encourage its use. Indeed, this one disperses
676+
the configuration at the same time in the routing and the resource configuration.
677+
678+
First, let's create your resource configuration:
489679

490680
```php
491681
<?php
@@ -563,48 +753,25 @@ class BookSpecial
563753
* name="book_special",
564754
* path="/books/{id}/special",
565755
* methods={"PUT"},
566-
* defaults={"_api_resource_class"=Book::class, "_api_item_operation_name"="special"}
756+
* defaults={
757+
* "_api_resource_class"=Book::class,
758+
* "_api_item_operation_name"="special"
759+
* }
567760
* )
568761
*/
569-
public function __invoke(Book $data): object // API Platform retrieves the PHP entity using the data provider then (for POST and
570-
// PUT method) deserializes user data in it. Then passes it to the action. Here $data
571-
// is an instance of Book having the given ID. By convention, the action's parameter
572-
// must be called $data.
762+
public function __invoke(Book $data): Book
573763
{
574764
$this->myService->doSomething($data);
575765
576-
return $data; // API Platform will automatically validate, persist (if you use Doctrine) and serialize an entity
577-
// for you. If you prefer to do it yourself, return an instance of Symfony\Component\HttpFoundation\Response
766+
return $data;
578767
}
579768
}
580769
```
581770

582-
This custom operation behaves exactly like the built-in operation: it returns a JSON-LD document corresponding to the id
583-
passed in the URL.
584-
585-
It is mandatory to set the `_api_resource_class` and `_api_item_operation_name` (or `_api_collection_operation_name` for a collection
771+
It is mandatory to set `_api_resource_class` and `_api_item_operation_name` (or `_api_collection_operation_name` for a collection
586772
operation) in the parameters of the route (`defaults` key). It allows API Platform and the Symfony routing system to hook
587773
together.
588774

589-
Here we consider that the autowiring enabled for controller classes (the default when using the API Platform distribution).
590-
This action will be automatically registered as a service (the service name is the same as the class name: `App\Controller\BookSpecial`).
591-
592-
API Platform automatically retrieves the appropriate PHP entity then deserializes it, and for `POST` and `PUT` requests
593-
updates the entity with data provided by the user.
594-
595-
If you want to bypass the automatic retrieval of the entity, you can set the parameter `_api_receive` to `false`.
596-
This way, it will skip the `Read`, `Deserialize` and `Validate` listeners (see [the event system](events.md) for more information).
597-
598-
Services (`$myService` here) are automatically injected thanks to the autowiring feature. You can type-hint any service
599-
you need and it will be autowired too.
600-
601-
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`
602-
(that will be displayed to the client immediately by the Symfony kernel) or, like in this example, an instance of an entity
603-
mapped as a resource (or a collection of instances for collection operations).
604-
In this case, the entity will pass through [all built-in event listeners](events.md) of API Platform. It will be
605-
automatically validated, persisted and serialized in JSON-LD. Then the Symfony kernel will send the resulting document to
606-
the client.
607-
608775
Alternatively, you can also use standard Symfony controller and YAML or XML route declarations. The following example does
609776
exactly the same thing than the previous example in a more Symfony-like fashion:
610777

@@ -619,7 +786,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
619786
620787
class BookController extends Controller
621788
{
622-
public function specialAction(Book $data, MyService $service): object
789+
public function specialAction(Book $data, MyService $service): Book
623790
{
624791
return $service->doSomething($data);
625792
}

index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
2. [Configuring Operations](core/operations.md#configuring-operations)
1919
3. [Subresources](core/operations.md#subresources)
2020
4. [Creating Custom Operations and Controllers](core/operations.md#creating-custom-operations-and-controllers)
21+
1. [Recommended Method](core/operations.md#recommended-method)
22+
2. [Alternative Method](core/operations.md#alternative-method)
2123
5. [Overriding Default Order](core/default-order.md)
2224
6. [Filters](core/filters.md)
2325
1. [Doctrine ORM Filters](core/filters.md#doctrine-orm-filters)

0 commit comments

Comments
 (0)