Creating Custom Operations and Controllers

Note: using custom 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.

注: API プラットフォームでカスタム コントローラーを使用することはお勧めしません。また、GraphQL はサポートされていません。ほとんどのユース ケースでは、REST と GraphQL の両方で動作する、より優れた拡張ポイントを利用できます。

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, including standard Symfony controllers extending the Symfony\Bundle\FrameworkBundle\Controller\AbstractController helper class.

API プラットフォームは、Symfony ルーティング システムを利用して、カスタム コントローラーに関連するカスタム操作を登録できます。そのような customcontrollers は、Symfony\Bundle\FrameworkBundle\Controller\AbstractControllerhelper クラスを拡張する標準 Symfony コントローラーを含む、任意の有効な Symfony コントローラーにすることができます。

However, API Platform recommends to use action classes instead of typical Symfony controllers. Internally, API Platform implements the Action-Domain-Responder pattern (ADR), a web-specific refinement of MVC.

ただし、API プラットフォームでは、通常の Symfony コントローラーの代わりにアクション クラスを使用することをお勧めします。内部的には、API プラットフォームは、MVC の Web 固有の改良である Action-Domain-Responder パターン (ADR) を実装します。

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.

API プラットフォームの配布により、ADR パターンの実装も容易になります。api/src/Controller に格納されたアクション クラスが自動配線されたサービスとして自動的に登録されます。

Thanks to the autowiring 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.

Symfony 依存性注入コンテナーの自動配線機能のおかげで、アクションに必要なサービスはそのコンストラクターでタイプヒントにすることができ、明示的に宣言する必要なく、自動的にインスタンス化および注入されます。

In the following examples, the built-in GET operation is registered as well as a custom operation called post_publication.

次の例では、組み込みの GET 操作と、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.

デフォルトでは、API プラットフォームは定義された最初の Get オペレーションを使用してアイテムの IRI を生成し、最初の GetCollection オペレーションを使用してコレクションの IRI を生成します。

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.

リソースに Get 操作がない場合、API プラットフォームは自動的に操作を追加して、この IRI の生成を支援します。リソースに識別子がある場合、この操作は /books/{id} のようになります。ただし、リソースに識別子がない場合、API プラットフォームは Skolem 形式 /.well-known/genid/{id} を使用します。これらのルートはドキュメント (OpenAPI など) から公開されていませんが、とにかくsymfony ルーティングであり、常に HTTP 404 を返します。

If you create a custom operation, you will probably want to properly document it. See the OpenAPI part of the documentation to do so.

カスタム オペレーションを作成する場合は、適切にドキュメント化する必要があります。ドキュメントの OpenAPI 部分を参照してください。

First, let's create your custom operation:

まず、カスタム操作を作成しましょう。

<?php
// api/src/Controller/CreateBookPublication.php
namespace App\Controller;

use App\Entity\Book;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;

#[AsController]
class CreateBookPublication extends AbstractController
{
    private $bookPublishingHandler;

    public function __construct(BookPublishingHandler $bookPublishingHandler)
    {
        $this->bookPublishingHandler = $bookPublishingHandler;
    }

    public function __invoke(Book $book): Book
    {
        $this->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.

このカスタム操作は、組み込み操作とまったく同じように動作します。URL で渡された ID に対応する JSON-LD ドキュメントを返します。

Here we consider that autowiring 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 プラットフォーム ディストリビューションを使用する場合のデフォルト) に対してオートワイヤリングが有効になっているとします。このアクションは、サービスとして自動的に登録されます (サービス名はクラス名と同じです: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.

API プラットフォームは、状態プロバイダーを使用して適切な PHP エンティティを自動的に取得し、その中のユーザー データを逆シリアル化します。POST、PUT、および PATCH リクエストの場合、ユーザーが提供した状態でエンティティを更新します。

The entity is retrieved in the __invoke method thanks to a dedicated argument resolver.

エンティティは、専用の引数リゾルバーのおかげで __invoke メソッドで取得されます。

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). Warning: the __invoke() method parameter MUST be called $data, otherwise, it will not be filled correctly!

GET を使用する場合、__invoke() メソッド パラメータは識別子を受け取り、リソース識別子と同じように呼び出す必要があります。そのため、パス /user/{uuid}/bookmarks には __invoke(string $uuid) を使用する必要があります。警告: __invoke() メソッドのパラメータは $data である必要があります。そうしないと、正しく入力されません!

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.

自動配線機能により、サービス (ここでは $bookPublishingHandler) が自動的に挿入されます。必要なサービスをヒントとして入力すると、自動配線されます。

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 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.

一致するルートがヒットすると、アクションの __invoke メソッドが呼び出されます。 Symfony\Component\HttpFoundation\Response のインスタンス (Symfony カーネルによってすぐにクライアントに表示されます) を返すか、この例のように、リソースとしてマップされたエンティティのインスタンス (またはインスタンスのコレクション) を返すことができます。この場合、エンティティは API プラットフォームのすべての組み込みイベント リスナーを通過します。 JSON-LD で自動的に検証、永続化、シリアル化されます。次に、Symfony カーネルは結果のドキュメントをクライアントに送信します。

The routing has not been configured yet because we will add it at the resource configuration level:

リソース構成レベルで追加するため、ルーティングはまだ構成されていません。

[codeSelector]

[コードセレクター]

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use App\Controller\CreateBookPublication;

#[ApiResource(operations: [
    new Get(),
    new Post(
        name: 'publication', 
        uriTemplate: '/books/{id}/publication', 
        controller: CreateBookPublication::class
    )
])]
class Book
{
    // ...
}
# api/config/api_platform/resources.yaml
App\Entity\Book:
    operations:
        ApiPlatform\Metadata\Get: ~
        post_publication:
            class: ApiPlatform\Metadata\Post
            method: POST
            uriTemplate: /books/{id}/publication
            controller: App\Controller\CreateBookPublication
<?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\Post" name="post_publication" uriTemplate="/books/{id}/publication"
                       controller="App\Controller\CreateBookPublication" />
        </operations>
    </resource>
</resources>

[/codeSelector]

[/コードセレクター]

It is mandatory to set the method, path and controller attributes. They allow API Platform to configure the routing path and the associated controller respectively.

メソッド、パス、およびコントローラーの属性を設定することは必須です。これらにより、API プラットフォームはルーティング パスと関連するコントローラーをそれぞれ構成できます。

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.

これを回避するために、API プラットフォームは、組み込み操作を使用する場合と同じように動作する ApiPlatform\Action\PlaceholderAction を提供します。

You just need to set the controller attribute with this class. Here, the previous example updated:

このクラスでコントローラー属性を設定するだけです。ここで、前の例が更新されました。

[codeSelector]

[コードセレクター]

// 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
{
    // ...
}
# api/config/api_platform/resources.yaml
App\Entity\Book:
    operations:
        ApiPlatform\Metadata\Get: ~
        post_publication:
            class: ApiPlatform\Metadata\Post
            method: POST
            uriTemplate: /books/{id}/publication
            controller: ApiPlatform\Action\PlaceholderAction
<?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\Post" name="post_publication" uriTemplate="/books/{id}/publication"
                       controller="ApiPlatform\Action\PlaceholderAction" />
        </operations>
    </resource>
</resources>

[/codeSelector]

[/コードセレクター]

Using Serialization Groups

You may want different serialization groups for your custom operations. Just configure the proper normalizationContext and/or denormalizationContext in your operation:

カスタム操作に異なるシリアライゼーション グループが必要になる場合があります。操作で適切な normalizationContext および/または denormalizationContext を構成するだけです。

[codeSelector]

[コードセレクター]

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use App\Controller\CreateBookPublication;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(operations: [
    new Get(),
    new Post(
        name: 'publication', 
        uriTemplate: '/books/{id}/publication', 
        controller: CreateBookPublication::class, 
        normalizationContext: ['groups' => 'publication']
    )
])]
class Book
{
    // ...

    #[Groups(['publication'])]
    public $isbn;

    // ...
}
# api/config/api_platform/resources.yaml
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 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\Post" name="post_publication" uriTemplate="/books/{id}/publication"
                controller="App\Controller\CreateBookPublication">
                <normalizationContext>
                    <values>
                        <value name="groups">publication</value>
                    </values>
                </normalizationContext>
            </operation>
        </operations>
    </resource>
</resources>

[/codeSelector]

[/コードセレクター]

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:

カスタム操作でエンティティの自動取得をバイパスする場合は、read: false を operation 属性に設定できます。

[codeSelector]

[コードセレクター]

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use App\Controller\CreateBookPublication;

#[ApiResource(operations: [
    new Get(),
    new Post(
        name: 'publication', 
        uriTemplate: '/books/{id}/publication', 
        controller: CreateBookPublication::class, 
        read: false
    )
])]
class Book
{
    // ...
}
# api/config/api_platform/resources.yaml
App\Entity\Book:
    operations:
        ApiPlatform\Metadata\Get: ~
        post_publication:
            class: ApiPlatform\Metadata\Post
            uriTemplate: /books/{id}/publication
            controller: App\Controller\CreateBookPublication
            read: 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\Get" />
            <operation class="ApiPlatform\Metadata\Post" name="post_publication" uriTemplate="/books/{id}/publication"
                controller="App\Controller\CreateBookPublication" read="false" />
        </operations>
    </resource>
</resources>

[/codeSelector]

[/コードセレクター]

This way, it will skip the ReadListener. You can do the same for some other built-in listeners. See Built-in Event Listeners for more information.

このようにして、ReadListener をスキップします。他の組み込みリスナーについても同じことができます。詳細については、組み込みのイベント リスナーを参照してください。

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).

カスタム コントローラでは、__invoke() メソッド パラメータはエンティティ識別子と同じように呼び出す必要があります。そのため、パス /user/{uuid}/bookmarks には __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.

post_publication 操作は、book_post_publication という名前の Symfony ルートを参照します。

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.

バージョン 2.3 以降では、慣習によりルート名を操作名として使用することもできます。次の例に示すように、method 属性も routeName 属性も指定されていない場合の book_post_discontinuation について説明します。

First, let's create your resource configuration:

まず、リソース構成を作成しましょう。

[codeSelector]

[コードセレクター]

<?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(),
    new Post(name: 'publication', routeName: 'book_post_publication'),
    new Post(name: 'book_post_discontinuation')
])]
class Book
{
    // ...
}
# api/config/api_platform/resources.yaml
App\Entity\Book:
    operations:
        ApiPlatform\Metadata\Get: ~
        post_publication:
            class: ApiPlatform\Metadata\Post
            routeName: book_post_publication
        book_post_discontinuation:
          class: ApiPlatform\Metadata\Post
<?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\Post" name="post_publication" routeName="book_post_publication" />
            <operation class="ApiPlatform\Metadata\Post" name="book_post_discontinuation" />
        </operations>
    </resource>
</resources>

[/codeSelector]

[/コードセレクター]

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 annotations:

API プラットフォームは、この post_publication オペレーションをルート book_post_publication に自動的にマッピングします。注釈を使用して、カスタム アクションとそれに関連するルートを作成しましょう。

<?php
// api/src/Controller/CreateBookPublication.php
namespace App\Controller;

use App\Entity\Book;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;

#[AsController]
class CreateBookPublication extends AbstractController
{
    public function __construct(
        private BookPublishingHandler $bookPublishingHandler
    ) {}

    #[Route(
        name: 'book_post_publication',
        path: '/books/{id}/publication',
        methods: ['POST'],
        defaults: [
            '_api_resource_class' => 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.

ルートのパラメーター (デフォルト キー) に _api_resource_class と _api_operation_name を設定することは必須です。これにより、API プラットフォームが Symfony ルーティング システムと連携できるようになります。

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:

または、従来の Symfony コントローラーと YAML または XML ルート宣言を使用することもできます。次の例は、前の例と同じことを行います。

<?php
// api/src/Controller/BookController.php
namespace App\Controller;

use App\Entity\Book;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;

#[AsController]
class BookController extends AbstractController
{
    public function createPublication(Book $book, BookPublishingHandler $bookPublishingHandler): Book
    {
        return $bookPublishingHandler->handle($book);
    }
}
# 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_item_operation_name: post_publication