GraphQL Support

GraphQL is a query language made to communicate with an API and therefore is an alternative to REST.

GraphQL は、API と通信するために作成されたクエリ言語であるため、REST に代わるものです。

It has some advantages compared to REST: it solves the over-fetching or under-fetching of data, is strongly typed, and is capable of retrieving multiple and nested data in one go, but it also comes with drawbacks. For example it creates overhead depending on the request.

REST と比較していくつかの利点があります。データのオーバーフェッチまたはアンダーフェッチを解決し、厳密に型指定され、複数のネストされたデータを一度に取得できますが、欠点もあります。たとえば、リクエストに応じてオーバーヘッドが発生します。

API Platform creates a REST API by default. But you can choose to enable GraphQL as well.

API プラットフォームは、デフォルトで REST API を作成します。ただし、GraphQL を有効にすることも選択できます。

Once enabled, you have nothing to do: your schema describing your API is automatically built and your GraphQL endpoint is ready to go!

API を記述したスキーマが自動的に構築され、GraphQL エンドポイントの準備が整います。

Enabling GraphQL

To enable GraphQL and its IDE (GraphiQL and GraphQL Playground) in your API, simply require the graphql-php package using Composer and clear the cache one more time:

API で GraphQL とその IDE (GraphiQL および GraphQL Playground) を有効にするには、Composer を使用して graphql-php パッケージを要求し、もう一度キャッシュをクリアします。

docker compose exec php sh -c '
    composer require webonyx/graphql-php
    bin/console cache:clear
'

You can now use GraphQL at the endpoint: https://localhost:8443/graphql.

エンドポイントで GraphQL を使用できるようになりました: https://localhost:8443/graphql。

Note: If you used Symfony Flex to install API Platform, URLs will be prefixed with /api by default. For example, the GraphQL endpoint will be: https://localhost:8443/api/graphql.

注: Symfony Flex を使用して API プラットフォームをインストールした場合、URL にはデフォルトで /api というプレフィックスが付きます。たとえば、GraphQL エンドポイントは https://localhost:8443/api/graphql になります。

Changing Location of the GraphQL Endpoint

Sometimes you may want to have the GraphQL endpoint at a different location. This can be done by manually configuring the GraphQL controller.

GraphQL エンドポイントを別の場所に配置したい場合があります。これは、GraphQL コントローラーを手動で構成することで実行できます。

# api/config/routes.yaml
api_graphql_entrypoint:
    path: /api/graphql
    controller: api_platform.graphql.action.entrypoint
# ...

Change /api/graphql to the URI you wish the GraphQL endpoint to be accessible on.

/api/graphql を、GraphQL エンドポイントにアクセスできるようにする URI に変更します。

GraphiQL

If Twig is installed in your project, go to the GraphQL endpoint with your browser. You will see a nice interface provided by GraphiQL to interact with your API.

プロジェクトに Twig がインストールされている場合は、ブラウザーで GraphQL エンドポイントに移動します。 API と対話するために GraphiQL によって提供される優れたインターフェイスが表示されます。

The GraphiQL IDE can also be found at /graphql/graphiql.

GraphiQL IDE は、/graphql/graphiql にもあります。

If you need to disable it, it can be done in the configuration:

無効にする必要がある場合は、構成で行うことができます。

# api/config/packages/api_platform.yaml
api_platform:
    graphql:
        graphiql:
            enabled: false
# ...

Add another Location for GraphiQL

If you want to add a different location besides /graphql/graphiql, you can do it like this:

/graphql/graphiql 以外に別の場所を追加する場合は、次のようにします。

# app/config/routes.yaml
graphiql:
    path: /docs/graphiql
    controller: api_platform.graphql.action.graphiql

GraphQL Playground

Another IDE is by default included in API Platform: GraphQL Playground.

別の IDE がデフォルトで API プラットフォームに含まれています: GraphQL Playground。

It can be found at /graphql/graphql_playground.

/graphql/graphql_playground にあります。

You can disable it if you want in the configuration:

構成で必要に応じて無効にすることができます。

# api/config/packages/api_platform.yaml
api_platform:
    graphql:
        graphql_playground:
            enabled: false
# ...

Add another Location for GraphQL Playground

You can add a different location besides /graphql/graphql_playground:

/graphql/graphql_playground 以外に別の場所を追加できます。

# app/config/routes.yaml
graphql_playground:
    path: /docs/graphql_playground
    controller: api_platform.graphql.action.graphql_playground

Modifying or Disabling the Default IDE

When going to the GraphQL endpoint, you can choose to launch the IDE you want.

GraphQL エンドポイントに移動するときに、必要な IDE を起動することを選択できます。

# api/config/packages/api_platform.yaml
api_platform:
    graphql:
        # Choose between graphiql or graphql-playground
        default_ide: graphql-playground
# ...

You can also disable this feature by setting the configuration value to false.

構成値を false に設定して、この機能を無効にすることもできます。

# api/config/packages/api_platform.yaml
api_platform:
    graphql:
        default_ide: false
# ...

Request with application/graphql Content-Type

If you wish to send a POST request using the application/graphql Content-Type, you need to enable it in the allowed formats of API Platform:

application/graphql Content-Type を使用して POST リクエストを送信する場合は、API プラットフォームの許可された形式で有効にする必要があります。

# api/config/packages/api_platform.yaml
api_platform:
    formats:
        # ...
        graphql: ['application/graphql']

Operations

To understand what an operation is, please refer to the operations documentation.

オペレーションとは何かを理解するには、オペレーションのドキュメントを参照してください。

For GraphQL, the operations are defined by using the Query, QueryCollection, Mutation, DeleteMutation and Subscription attributes.

GraphQL の場合、操作は Query、QueryCollection、Mutation、DeleteMutation、および Subscription 属性を使用して定義されます。

By default, the following operations are enabled:

デフォルトでは、次の操作が有効になっています。

  • Query
    クエリ
  • QueryCollection
    クエリコレクション
  • Mutation(name: 'create')
    突然変異(名前: '作成')
  • Mutation(name: 'update')
    ミューテーション(名前: 'update')
  • DeleteMutation(name: 'delete')
    DeleteMutation(名前: 'delete')

You can of course disable or configure these operations.

もちろん、これらの操作を無効にしたり構成したりできます。

For instance, in the following example, only the query of an item and the create mutation are enabled:

たとえば、次の例では、アイテムのクエリと create ミューテーションのみが有効になっています。

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

use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\ApiResource;

#[ApiResource(graphQlOperations: [
    new Query(),
    new Mutation(name: 'create')
])]
class Book
{
    // ...
}

Queries

If you don't know what queries are yet, please read the documentation about them.

クエリがまだわからない場合は、クエリに関するドキュメントを読んでください。

For each resource, two queries are available: one for retrieving an item and the other one for the collection. For example, if you have a Book resource, the queries book and books can be used.

リソースごとに 2 つのクエリを使用できます。1 つはアイテムの取得用で、もう 1 つはコレクション用です。たとえば、Book リソースがある場合、クエリ book と books を使用できます。

Global Object Identifier

When querying an item, you need to pass an identifier as argument. Following the GraphQL Global Object Identification Specification, the identifier needs to be globally unique. In API Platform, this argument is represented as an IRI (Internationalized Resource Identifier).

アイテムをクエリするときは、識別子を引数として渡す必要があります。 GraphQL グローバル オブジェクト識別仕様に従って、識別子はグローバルに一意である必要があります。 API Platform では、この引数は IRI (Internationalized Resource Identifier) として表されます。

For example, to query a book having as identifier 89, you have to run the following:

たとえば、識別子が 89 の本を照​​会するには、次のコマンドを実行する必要があります。

{
  book(id: "/books/89") {
    title
    isbn
  }
}

Note that in this example, we're retrieving two fields: title and isbn.

この例では、title と isbn の 2 つのフィールドを取得していることに注意してください。

Custom Queries

To create a custom query, first of all you need to create its resolver.

カスタム クエリを作成するには、まずそのリゾルバーを作成する必要があります。

If you want a custom query for a collection, create a class like this:

コレクションのカスタム クエリが必要な場合は、次のようなクラスを作成します。

<?php
// api/src/Resolver/BookCollectionResolver.php
namespace App\Resolver;

use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface;
use App\Entity\Book;

final class BookCollectionResolver implements QueryCollectionResolverInterface
{
    /**
     * @param iterable<Book> $collection
     *
     * @return iterable<Book>
     */
    public function __invoke(iterable $collection, array $context): iterable
    {
        // Query arguments are in $context['args'].

        foreach ($collection as $book) {
            // Do something with the book.
        }

        return $collection;
    }
}

If you use autoconfiguration (the default Symfony configuration) in your application, then you are done!

アプリケーションで自動構成 (デフォルトの Symfony 構成) を使用する場合は、完了です!

Else, you need to tag your resolver like this:

それ以外の場合は、次のようにリゾルバーにタグを付ける必要があります。

# api/config/services.yaml
services:
    # ...
    App\Resolver\BookCollectionResolver:
        tags:
            - { name: api_platform.graphql.query_resolver }

The resolver for an item is very similar:

アイテムのリゾルバは非常に似ています:

<?php
// api/src/Resolver/BookResolver.php
namespace App\Resolver;

use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface;
use App\Entity\Book;

final class BookResolver implements QueryItemResolverInterface
{
    /**
     * @param Book|null $item
     *
     * @return Book
     */
    public function __invoke($item, array $context)
    {
        // Query arguments are in $context['args'].

        // Do something with the book.
        // Or fetch the book if it has not been retrieved.

        return $item;
    }
}

Note that you will receive the retrieved item or not in this resolver depending on how you configure your query in your resource.

リソースでのクエリの構成方法に応じて、取得したアイテムをこのリゾルバーで受け取るかどうかに注意してください。

Since the resolver is a service, you can inject some dependencies and fetch your item in the resolver if you want.

リゾルバーはサービスであるため、必要に応じていくつかの依存関係を注入し、リゾルバーでアイテムを取得できます。

If you don't use autoconfiguration, don't forget to tag your resolver with api_platform.graphql.query_resolver.

自動構成を使用しない場合は、リゾルバーに api_platform.graphql.query_resolver のタグを付けることを忘れないでください。

Now that your resolver is created and registered, you can configure your custom query and link its resolver.

リゾルバーが作成されて登録されたので、カスタム クエリを構成してそのリゾルバーをリンクできます。

In your resource, add the following:

リソースに、次を追加します。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use App\Resolver\BookCollectionResolver;
use App\Resolver\BookResolver;

#[ApiResource(graphQlOperations: [
    new Query(),
    new QueryCollection(),
    new Mutation(name: 'create'),
    new Mutation(name: 'update'),
    new DeleteMutation(name: 'delete'),

    new Query(name: 'retrievedQuery', resolver: BookResolver::class),
    new Query(
        name: 'notRetrievedQuery',
        resolver: BookResolver::class,
        args: []
    ),
    new Query(
        name: 'withDefaultArgsNotRetrievedQuery',
        resolver: BookResolver::class,
        read: false
    ),
    new Query(
        name: 'withCustomArgsQuery',
        resolver: BookResolver::class,
        args: [
            'id' => ['type' => 'ID!'], 
            'log' => ['type' => 'Boolean!', 'description' => 'Is logging activated?'], 
            'logDate' => ['type' => 'DateTime']
        ]
    ),
    new QueryCollection(name: 'collectionQuery', resolver: BookCollectionResolver::class),
])]
class Book
{
    // ...
}

Note that you need to explicitly add the auto-generated queries and mutations if they are needed when configuring custom queries, like it's done for the operations.

自動生成されたクエリとミューテーションが必要な場合は、操作の場合と同様に、カスタム クエリを構成するときに明示的に追加する必要があることに注意してください。

As you can see, it's possible to define your own arguments for your custom queries. They are following the GraphQL type system. If you don't define the args property, it will be the default ones (for example id for an item).

ご覧のとおり、カスタム クエリに独自の引数を定義することができます。それらは GraphQL 型システムに従っています。args プロパティを定義しない場合は、デフォルトのものになります (たとえば、アイテムの id)。

If you don't want API Platform to retrieve the item for you, disable the read stage like in withDefaultArgsNotRetrievedQuery. Some other stages can be disabled. Another option would be to make sure there is no id argument. This is the case for notRetrievedQuery (empty args). Conversely, if you need to add custom arguments, make sure id is added among the arguments if you need the item to be retrieved automatically.

API プラットフォームに項目を取得させたくない場合は、withDefaultArgsNotRetrievedQuery のように読み取りステージを無効にします。他のステージを無効にすることもできます。別のオプションとして、id 引数がないことを確認することもできます。これは、notRetrievedQuery の場合です (空の引数).逆に、カスタム引数を追加する必要がある場合、アイテムを自動的に取得する必要がある場合は、引数に id が追加されていることを確認してください。

Note also that:

次の点にも注意してください。

  • If you have added your own custom types, you can use them directly for your arguments types (it's the case here for DateTime).
    独自のカスタム型を追加した場合は、それらを引数の型に直接使用できます (ここでは DateTime の場合です)。
  • You can also add a custom description for your custom arguments. You can see the field arguments documentation for more options.
    カスタム引数のカスタム説明を追加することもできます。その他のオプションについては、フィールド引数のドキュメントを参照してください。

The arguments you have defined or the default ones and their value will be in $context['args'] of your resolvers.

定義した引数またはデフォルトの引数とその値は、リゾルバーの $context['args'] にあります。

You custom queries will be available like this:

カスタム クエリは次のように利用できます。

{
  retrievedQueryBook(id: "/books/56") {
    title
  }

  notRetrievedQueryBook {
    title
  }

  withDefaultArgsNotRetrievedQueryBook(id: "/books/56") {
    title
  }

  withCustomArgsQueryBook(id: "/books/23", log: true, logDate: "2019-12-20") {
    title
  }

  collectionQueryBooks {
    edges {
      node {
        title
      }
    }
  }
}

Mutations

If you don't know what mutations are yet, the documentation about them is here.

ミューテーションとは何かをまだ知らない場合は、ミューテーションに関するドキュメントがここにあります。

For each resource, three mutations are available: * Mutation(name: 'create') for creating a new resource * Mutation(name: 'update') for updating an existing resource * DeleteMutation(name: 'delete') for deleting an existing resource

リソースごとに、次の 3 つのミューテーションを使用できます。* 新しいリソースを作成するための Mutation(name: 'create')* 既存のリソースを更新するための Mutation(name: 'update')* 既存のリソースを削除するための DeleteMutation(name: 'delete')資源

When updating or deleting a resource, you need to pass the IRI of the resource as argument. See Global Object Identifier for more information.

リソースを更新または削除するときは、リソースの IRI を引数として渡す必要があります。詳細については、グローバル オブジェクト識別子を参照してください。

Client Mutation ID

Following the Relay Input Object Mutations Specification, you can pass a clientMutationId as argument and can ask its value as a field.

Relay Input Object Mutations Specification に従って、clientMutationId を引数として渡し、その値をフィールドとして尋ねることができます。

For example, if you delete a book:

たとえば、本を削除する場合:

mutation DeleteBook($id: ID!, $clientMutationId: String!) {
  deleteBook(input: {id: $id, clientMutationId: $clientMutationId}) {
    clientMutationId
  }
}

Custom Mutations

Creating custom mutations is comparable to creating custom queries.

カスタム ミューテーションの作成は、カスタム クエリの作成に相当します。

Create your resolver:

リゾルバーを作成します。

<?php
// api/src/Resolver/BookMutationResolver.php
namespace App\Resolver;

use ApiPlatform\GraphQl\Resolver\MutationResolverInterface;
use App\Entity\Book;

final class BookMutationResolver implements MutationResolverInterface
{
    /**
     * @param Book|null $item
     *
     * @return Book
     */
    public function __invoke($item, array $context)
    {
        // Mutation input arguments are in $context['args']['input'].

        // Do something with the book.
        // Or fetch the book if it has not been retrieved.

        // The returned item will pe persisted.
        return $item;
    }
}

As you can see, depending on how you configure your custom mutation in the resource, the item is retrieved or not. For instance, if you don't set an id argument or if you disable the read or the deserialize stage (other stages can also be disabled), the received item will be null.

ご覧のとおり、リソースでカスタム ミューテーションをどのように構成するかによって、アイテムが取得されるかどうかが決まります。無効にすることもできます)、受け取ったアイテムは null になります。

Likewise, if you don't want your item to be persisted by API Platform, you can return null instead of the mutated item (be careful: the response will also be null) or disable the write stage.

同様に、項目を API プラットフォームで保持したくない場合は、変更された項目の代わりに null を返す (注意: 応答も null になる) か、書き込みステージを無効にすることができます。

Don't forget the resolver is a service and you can inject the dependencies you want.

リゾルバーはサービスであり、必要な依存関係を注入できることを忘れないでください。

If you don't use autoconfiguration, add the tag api_platform.graphql.mutation_resolver to the resolver service.

自動構成を使用しない場合は、タグ api_platform.graphql.mutation_resolver をリゾルバー サービスに追加します。

Now in your resource:

今あなたのリソースで:

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use App\Resolver\BookMutationResolver;

#[ApiResource(
    graphQlOperations: [
        new Query(),
        new QueryCollection(),
        new Mutation(name: 'create'),
        new Mutation(name: 'update'),
        new DeleteMutation(name: 'delete'),

        new Mutation(name: 'mutation', resolver: BookMutationResolver::class),
        new Mutation(
            name: 'withCustomArgsMutation',
            resolver: BookMutationResolver::class,
            args: [
                'sendMail' => [
                    'type' => 'Boolean!', 
                    'description' => 'Send a mail?'
                ]
            ]
        ),
        new Mutation(
            name: 'disabledStagesMutation',
            resolver: BookMutationResolver::class,
            deserialize: false, 
            write: false
        )
    ]
)]
class Book
{
    // ...
}

Note that you need to explicitly add the auto-generated queries and mutations if they are needed when configuring custom mutations, like it's done for the operations.

カスタム ミューテーションを構成するときに必要な場合は、自動生成されたクエリとミューテーションを明示的に追加する必要があることに注意してください。操作の場合と同様です。

As the custom queries, you can define your own arguments if you don't want to use the default ones (extracted from your resource). The only difference with them is that, even if you define your own arguments, the clientMutationId will always be set.

カスタム クエリとして、(リソースから抽出された) デフォルトのものを使用したくない場合は、独自の引数を定義できます。それらとの唯一の違いは、独自の引数を定義した場合でも、clientMutationId は常に設定。

The arguments will be in $context['args']['input'] of your resolvers.

引数はリゾルバの $context['args']['input'] にあります。

Your custom mutations will be available like this:

カスタム ミューテーションは次のように利用できます。

{
  mutation {
    mutationBook(input: {id: "/books/18", title: "The Fitz and the Fool"}) {
      book {
        title
      }
    }
  }

  mutation {
    withCustomArgsMutationBook(input: {sendMail: true, clientMutationId: "myId"}) {
      book {
        title
      }
      clientMutationId
    }
  }

  mutation {
    disabledStagesMutationBook(input: {id: "/books/18", title: "The Fitz and the Fool"}) {
      book {
        title
      }
      clientMutationId
    }
  }
}

Subscriptions

Subscriptions are an RFC to allow a client to receive pushed realtime data from the server.

サブスクリプションは、クライアントがサーバーからプッシュされたリアルタイム データを受信できるようにする RFC です。

In API Platform, the built-in subscription support is handled by using Mercure as its underlying protocol.

API プラットフォームでは、組み込みのサブスクリプション サポートは、その基になるプロトコルとして Mercure を使用して処理されます。

Enable Update Subscriptions for a Resource

To enable update subscriptions for a resource, these conditions have to be met:

リソースの更新サブスクリプションを有効にするには、次の条件を満たす必要があります。

  • the Mercure hub and bundle need to be installed and configured.
    Mercure ハブとバンドルをインストールして構成する必要があります。
  • Mercure needs to be enabled for the resource.
    リソースに対して Mercure を有効にする必要があります。
  • the update mutation needs to be enabled for the resource.
    リソースに対して update ミューテーションを有効にする必要があります。
  • the subscription needs to be enabled for the resource.
    リソースに対してサブスクリプションを有効にする必要があります。

For instance, your resource should look like this:

たとえば、リソースは次のようになります。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Subscription;

#[ApiResource(mercure: true, graphQlOperations: [
    new Mutation(name: 'update'),
    new Subscription()
])]
class Book
{
    // ...
}

Subscribe

Doing a subscription is very similar to doing a query:

サブスクリプションの実行は、クエリの実行と非常に似ています。

{
  subscription {
    updateBookSubscribe(input: {id: "/books/1", clientSubscriptionId: "myId"}) {
      book {
        title
        isbn
      }
      mercureUrl
      clientSubscriptionId
    }
  }
}

As you can see, you need to pass the IRI of the resource as argument. See Global Object Identifier for more information.

ご覧のとおり、リソースの IRI を引数として渡す必要があります。詳細については、グローバル オブジェクト識別子を参照してください。

You can also pass clientSubscriptionId as argument and can ask its value as a field.

clientSubscriptionId を引数として渡し、その値をフィールドとして尋ねることもできます。

In the payload of the subscription, the given fields of the resource will be the fields you subscribe to: if any of these fields is updated, you will be pushed their updated values.

サブスクリプションのペイロードでは、リソースの指定されたフィールドがサブスクライブするフィールドになります。これらのフィールドのいずれかが更新されると、更新された値がプッシュされます。

The mercureUrl field is the Mercure URL you need to use to subscribe to the updates on the client-side.

mercureUrl フィールドは、クライアント側で更新を購読するために使用する必要がある Mercure URL です。

Receiving an Update

On the client-side, you will receive the pushed updated data like you would receive the updated data if you did an update mutation.

クライアント側では、更新ミューテーションを行った場合に更新されたデータを受け取るのと同じように、プッシュされた更新されたデータを受け取ります。

For instance, you could receive a JSON payload like this:

たとえば、次のような JSON ペイロードを受け取ることができます。

{
  "book": {
    "title": "Updated title",
    "isbn": "978-6-6344-4051-1"
  }
}

Subscriptions Cache

Internally, API Platform stores the subscriptions in a cache, using the Symfony Cache.

内部的には、API プラットフォームは Symfony キャッシュを使用してサブスクリプションをキャッシュに保存します。

The cache is named api_platform.graphql.cache.subscription and the subscription keys are generated from the subscription payload by using a SHA-256 hash.

キャッシュの名前は api_platform.graphql.cache.subscription で、サブスクリプション キーは SHA-256 ハッシュを使用してサブスクリプション ペイロードから生成されます。

It's recommended to use an adapter like Redis for this cache.

このキャッシュには Redis などのアダプターを使用することをお勧めします。

Workflow of the Resolvers

API Platform resolves the queries and mutations by using its own resolvers.

API プラットフォームは、独自のリゾルバーを使用してクエリとミューテーションを解決します。

Even if you create your custom queries or your custom mutations, these resolvers will be used and yours will be called at the right time.

カスタム クエリまたはカスタム ミューテーションを作成した場合でも、これらのリゾルバーが使用され、適切なタイミングで呼び出されます。

Each resolver follows a workflow composed of stages.

各リゾルバーは、ステージで構成されるワークフローに従います。

The schema below describes them:

以下のスキーマでそれらについて説明します。

Resolvers Workflow

Each stage corresponds to a service. It means you can take control of the workflow wherever you want by decorating them!

各ステージはサービスに対応しています。これは、ワークフローを好きな場所で装飾することで制御できることを意味します。

Here is an example of the decoration of the write stage, for instance if you want to persist your data as you want.

たとえば、必要に応じてデータを永続化する場合など、書き込みステージの装飾の例を次に示します。

Create your WriteStage:

WriteStage を作成します。

<?php
namespace App\Stage;

use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface;

final class WriteStage implements WriteStageInterface
{
    private $writeStage;

    public function __construct(WriteStageInterface $writeStage)
    {
        $this->writeStage = $writeStage;
    }

    /**
     * {@inheritdoc}
     */
    public function __invoke($data, string $resourceClass, string $operationName, array $context)
    {
        // You can add pre-write code here.

        // Call the decorated write stage (this syntax calls the __invoke method).
        $writtenObject = ($this->writeStage)($data, $resourceClass, $operationName, $context);

        // You can add post-write code here.

        return $writtenObject;
    }
}

Decorate the API Platform stage service:

API Platform ステージ サービスを装飾します。

# api/config/services.yaml
services:
    # ...
    'App\Stage\WriteStage':
        decorates: api_platform.graphql.resolver.stage.write

Disabling Resolver Stages

If you need to, you can disable some stages done by the resolvers, for instance if you don't want your data to be validated.

必要に応じて、たとえばデータを検証したくない場合など、リゾルバーによって実行されるいくつかの段階を無効にすることができます。

The following table lists the stages you can disable in your resource configuration.

次の表に、リソース構成で無効にできるステージを示します。

Attribute Type Default Description
read bool true Enables or disables the reading of data
deserialize bool true Enables or disables the deserialization of data (mutation only)
validate bool true Enables or disables the validation of the denormalized data (mutation only)
write bool true Enables or disables the writing of data into the persistence system (mutation only)
serialize bool true Enables or disables the serialization of data

A stage can be disabled at the operation level:

操作レベルでステージを無効にすることができます。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;

#[ApiResource(graphQlOperations: [
    new Query(),
    new QueryCollection(),
    new Mutation(name: 'create', write: false)
])]
class Book
{
    // ...
}

Or at the resource attributes level (will be also applied in REST and for all operations):

または、リソース属性レベル (REST およびすべての操作にも適用されます):

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;

#[ApiResource(write: false, graphQlOperations: [
    new Query(),
    new QueryCollection(),
    new Mutation(name: 'create')
])]
class Book
{
    // ...
}

Events

No events are sent by the resolvers in API Platform. If you want to add your custom logic, decorating the stages is the recommended way to do it.

API Platform のリゾルバーによってイベントが送信されることはありません。カスタム ロジックを追加する場合は、ステージを装飾することをお勧めします。

However, if you really want to use events, you can by installing a bundle dispatching events before and after the stages.

ただし、本当にイベントを使用したい場合は、ステージの前後にイベントをディスパッチするバンドルをインストールすることで使用できます。

Filters

Filters are supported out-of-the-box. Follow the filters documentation and your filters will be available as arguments of queries.

フィルターはすぐに使用できるようにサポートされています。フィルターのドキュメントに従ってください。フィルターはクエリの引数として使用できます。

However you don't necessarily have the same needs for your GraphQL endpoint as for your REST one.

ただし、GraphQL エンドポイントに対するニーズが REST エンドポイントと同じであるとは限りません。

In the QueryCollection attribute, you can choose to decorrelate the GraphQL filters. In order to keep the default behavior (possibility to fetch, delete, update or create), define all the auto-generated operations (Query ,QueryCollection, DeleteMutation, and the update and create Mutation).

QueryCollection 属性では、GraphQL フィルターの関連付けを解除することを選択できます。デフォルトの動作 (フェッチ、削除、更新、または作成の可能性) を維持するために、すべての自動生成操作 (Query 、QueryCollection、DeleteMutation、および更新) を定義します。ミューテーションを作成します)。

For example, this entity will have a search filter for REST and a date filter for GraphQL:

たとえば、このエンティティには REST の検索フィルターと GraphQL の日付フィルターがあります。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;

#[ApiResource(filters: ['offer.search_filter'], graphQlOperations: [
    new Query(),
    new QueryCollection(filters: ['offer.date_filter']),
    new Mutation(name: 'create'),
    new Mutation(name: 'update'),
    new DeleteMutation(name: 'delete')
])]
class Offer
{
    // ...
}

Syntax for Filters with a List of Key / Value Arguments

Some filters like the exists filter or the order filter take a list of key / value as arguments.

exists フィルターや order フィルターなどの一部のフィルターは、キー/値のリストを引数として受け取ります。

The first syntax coming to mind to use them is to write:

それらを使用するために頭に浮かぶ最初の構文は、次のように書くことです。

{
  offers(order: {id: "ASC", name: "DESC"}) {
    edges {
      node {
        id
        name
      }
    }
  }
}

However this syntax has a problematic issue: it doesn't keep the order of the arguments. These filters usually need a proper order to give results as expected.

ただし、この構文には問題があります。引数の順序が保持されません。通常、これらのフィルタは、期待どおりの結果を得るために適切な順序を必要とします。

That's why this syntax needs to be used instead:

そのため、代わりに次の構文を使用する必要があります。

{
  offers(order: [{id: "ASC"}, {name: "DESC"}]) {
    edges {
      node {
        id
        name
      }
    }
  }
}

Since a list is used for the arguments, the order is preserved.

引数にはリストが使用されるため、順序は保持されます。

Filtering on Nested Properties

Unlike for REST, all built-in filters support nested properties using the underscore (_) syntax instead of the dot (.) syntax, e.g.:

REST とは異なり、すべての組み込みフィルターは、ドット (.) 構文の代わりにアンダースコア (_) 構文を使用してネストされたプロパティをサポートします。

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

use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;

#[ApiResource]
#[ApiFilter(OrderFilter::class, properties: ['product.releaseDate'])]
#[ApiFilter(SearchFilter::class, properties: ['product.color' => 'exact'])]
class Offer
{
    // ...
}

The above allows you to find offers by their respective product's color like for the REST Api. You can then filter using the following syntax:

上記により、REST Api の場合と同様に、それぞれの製品の色でオファーを見つけることができます。その後、次の構文を使用してフィルター処理できます。

{
  offers(product_color: "red") {
    edges {
      node {
        id
        product {
          name
          color
        }
      }
    }
  }
}

Or order your results like:

または、次のように結果を並べ替えます。

{
  offers(order: [{product_releaseDate: "DESC"}]) {
    edges {
      node {
        id
        product {
          name
          color
        }
      }
    }
  }
}

Another difference with the REST API filters is that the keyword _list must be used instead of the traditional [] to filter over multiple values.

REST API フィルターとのもう 1 つの違いは、複数の値をフィルター処理するために、従来の [] の代わりにキーワード _list を使用する必要があることです。

For example, if you want to search the offers with a green or a red product you can use the following syntax:

たとえば、緑または赤の製品でオファーを検索する場合は、次の構文を使用できます。

{
  offers(product_color_list: ["red", "green"]) {
    edges {
      node {
        id
        product {
          name
          color
        }
      }
    }
  }
}

Pagination

API Platform natively enables a cursor-based pagination for collections. It supports GraphQL's Complete Connection Model and is compatible with GraphQL Cursor Connections Specification.

API プラットフォームは、コレクションのカーソルベースのページネーションをネイティブに有効にします。GraphQL の完全な接続モデルをサポートし、GraphQL Cursor Connections Specification と互換性があります。

A page-based pagination can also be enabled per resource or per operation.

ページベースのページネーションは、リソースごとまたは操作ごとに有効にすることもできます。

Using the Cursor-based Pagination

Here is an example query leveraging the pagination system:

ページネーション システムを利用したクエリの例を次に示します。

{
  offers(first: 10, after: "cursor") {
    totalCount
    pageInfo {
      endCursor
      hasNextPage
    }
    edges {
      cursor
      node {
        id
      }
    }
  }
}

Two pairs of parameters work with the query:

2 組のパラメーターがクエリで機能します。

  • first and after;
    最初とその後;
  • last and before.
    最後と前。

More precisely:

より正確に:

  • first corresponds to the items per page starting from the beginning;
    first は、最初から始まるページごとの項目に対応します。
  • after corresponds to the cursor from which the items are returned.

    after は、アイテムが返されるカーソルに対応します。

  • last corresponds to the items per page starting from the end;

    last は、最後から始まるページごとの項目に対応します。

  • before corresponds to the cursor from which the items are returned, from a backwards point of view.
    before は、後方の観点から、アイテムが返されるカーソルに対応します。

The current page always has a startCursor and an endCursor, present in the pageInfo field.

現在のページには、pageInfo フィールドにある startCursor と endCursor が常にあります。

To get the next page, you would add the endCursor from the current page as the after parameter.

次のページを取得するには、現在のページの endCursor を after パラメータとして追加します。

{
  offers(first: 10, after: "endCursor") {
  }
}

For the previous page, you would add the startCursor from the current page as the before parameter.

前のページの場合、現在のページの startCursor を before パラメータとして追加します。

{
  offers(last: 10, before: "startCursor") {
  }
}

How do you know when you have reached the last page? It is the aim of the property hasNextPage or hasPreviousPage in pageInfo. When it is false, you know it is the last page and moving forward or backward will give you an empty result.

最後のページに到達したことをどのように知ることができますか?これは、pageInfo の hasNextPage または hasPreviousPage プロパティの目的です。false の場合、最後のページであることがわかり、前後に移動すると空の結果が得られます。

Using the Page-based Pagination

In order to use the page-based pagination, you need to enable it in the resource.

ページベースのページネーションを使用するには、リソースでそれを有効にする必要があります。

For instance at the operation level:

たとえば、操作レベルでは次のようになります。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;

#[ApiResource(graphQlOperations: [
    new Query(),
    new QueryCollection(paginationType: 'page'),
    new Mutation(name: 'create'),
    new Mutation(name: 'update'),
    new DeleteMutation(name: 'delete')
])]
class Offer
{
    // ...
}

Or if you want to do it at the resource level:

または、リソース レベルで実行する場合は、次のようにします。

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

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(paginationType: 'page')]
class Offer
{
    // ...
}

Once enabled, a page filter will be available in the collection query (its name can be changed in the configuration) and an itemsPerPage filter will be available too if client-side-pagination is enabled.

有効にすると、コレクション クエリでページ フィルターを使用できるようになり (その名前は構成で変更できます)、クライアント側のページネーションが有効になっている場合は、itemsPerPage フィルターも使用できるようになります。

A paginationInfo field can be queried to obtain the following information:

paginationInfo フィールドを照会して、次の情報を取得できます。

  • itemsPerPage: the number of items per page. To change it, follow the pagination documentation.
    itemsPerPage: ページあたりのアイテム数。変更するには、ページネーションのドキュメントに従ってください。
  • lastPage: the last page of the collection.
    lastPage: コレクションの最後のページ。
  • totalCount: the total number of items in the collection.
    totalCount: コレクション内のアイテムの総数。

The collection items data are available in the collection field.

コレクション項目のデータは、コレクション フィールドで使用できます。

An example of a query:

クエリの例:

{
  offers(page: 3, itemsPerPage: 15) {
    collection {
      id
    }
    paginationInfo {
      itemsPerPage
      lastPage
      totalCount
    }
  }
}

Disabling the Pagination

See also the pagination documentation.

ページネーションのドキュメントも参照してください。

Globally

The pagination can be disabled for all GraphQL resources using this configuration:

この構成を使用して、すべての GraphQL リソースに対してページネーションを無効にすることができます。

# api/config/packages/api_platform.yaml
api_platform:
    graphql:
        collection:
            pagination:
                enabled: false

For a Specific Resource

It can also be disabled for a specific resource (REST and GraphQL):

特定のリソース (REST および GraphQL) に対して無効にすることもできます。

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

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(paginationEnabled: false)]
class Book
{
    // ...
}

For a Specific Resource Collection Operation

You can also disable the pagination for a specific collection operation:

特定のコレクション操作のページネーションを無効にすることもできます。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\QueryCollection;

#[ApiResource(graphQlOperations: [new QueryCollection(paginationEnabled: false)])]
class Book
{
    // ...
}

Partial Pagination

Partial pagination is possible with GraphQL.

GraphQL では部分的なページネーションが可能です。

When enabled, backwards pagination will not be possible, and the hasNextPage information will be always false.

有効にすると、逆方向のページ付けができなくなり、hasNextPage 情報は常に false になります。

Security

To add a security layer to your queries and mutations, follow the security documentation.

クエリとミューテーションにセキュリティ レイヤーを追加するには、セキュリティ ドキュメントに従ってください。

The REST security configuration and the GraphQL one are not correlated.

REST セキュリティ構成と GraphQL 構成は関連付けられていません。

If you have only some parts differing between REST and GraphQL, you have to redefine the common parts anyway.

REST と GraphQL で一部の部分だけが異なる場合は、共通部分を再定義する必要があります。

In the example below, we want the same security rules as we have in REST, but we also want to allow an admin to delete a book only in GraphQL. Please note that, it's not possible to update a book in GraphQL because the update operation is not defined.

以下の例では、REST と同じセキュリティ ルールが必要ですが、管理者が GraphQL でのみ本を削除できるようにしたいと考えています。定義されていません。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\Post;

#[ApiResource(
    security: "is_granted('ROLE_USER')", 
    operations: [
        new Get(security: "is_granted('ROLE_USER') and object.owner == user", securityMessage: 'Sorry, but you are not the book owner.'),
        new Post(security: "is_granted('ROLE_ADMIN')", securityMessage: 'Only admins can add books.')
    ],
    graphQlOperations: [
        new Query(security: "is_granted('ROLE_USER') and object.owner == user"),
        new QueryCollection(security: "is_granted('ROLE_ADMIN')"),
        new DeleteMutation(name: 'delete', security: "is_granted('ROLE_ADMIN')"),
        new Mutation(name: 'create', security: "is_granted('ROLE_ADMIN')")
    ]
)]
class Book
{
    // ...
}

Securing Properties (Including Associations)

You may want to limit access to certain resource properties with a security expression. This can be done with the ApiProperty security attribute.

セキュリティ式を使用して、特定のリソース プロパティへのアクセスを制限したい場合があります。これは、ApiProperty セキュリティ属性を使用して実行できます。

Note: adding the ApiProperty security expression to a GraphQL property will automatically make the GraphQL property type nullable (if it wasn't already). This is because null is returned as the property value if access is denied via the security expression.

注: ApiProperty セキュリティ式を GraphQL プロパティに追加すると、GraphQL プロパティ タイプが自動的に nullable になります (まだ設定されていない場合)。これは、セキュリティ式によってアクセスが拒否された場合、プロパティ値として null が返されるためです。

In GraphQL, it's possible to expose associations - allowing nested querying. For example, associations can be made with Doctrine ORM's OneToMany, ManyToOne, ManyToMany, etc.

GraphQL では、関連付けを公開して、ネストされたクエリを許可できます。たとえば、Doctrine ORM の OneToMany、ManyToOne、ManyToMany などで関連付けを作成できます。

It's important to note that the security defined on resource operations applies only to the exposed query/mutation endpoints (e.g. Query.users, Mutation.updateUser, etc.). Resource operation security is defined via the security attribute for each operation defined on the resource. This security is not applied to exposed associations.

リソース操作で定義されたセキュリティは、公開されたクエリ/ミューテーション エンドポイント (例: Query.users、Mutation.updateUser など) にのみ適用されることに注意することが重要です。リソース操作のセキュリティは、リソースで定義された各操作の security 属性を介して定義されます。 .このセキュリティは、公開された関連付けには適用されません。

Associations can instead be secured with the ApiProperty security attribute. This provides the flexibility to have different security depending on where an association is exposed.

関連付けは、代わりに ApiProperty セキュリティ属性で保護できます。これにより、アソシエーションが公開される場所に応じて異なるセキュリティを持つ柔軟性が提供されます。

To prevent traversal attacks, you should ensure that any exposed associations are secured appropriately. A traversal attack is where a user can gain unintended access to a resource by querying nested associations, gaining access to a resource that prevents direct access (via the query endpoint). For example, a user may be denied using Query.getUser to get a user, but is able to access the user through an association on an object that they do have access to (e.g. document.createdBy).

トラバーサル攻撃を防ぐには、公開されたアソシエーションが適切に保護されていることを確認する必要があります。トラバーサル攻撃とは、ユーザーが、ネストされたアソシエーションをクエリすることでリソースへの意図しないアクセスを取得し、(クエリ エンドポイントを介して) 直接アクセスを防止するリソースへのアクセスを取得できる場合です。たとえば、ユーザーは、Query.getUser を使用してユーザーを取得することを拒否される場合がありますが、アクセス権を持つオブジェクト (例: document.createdBy) の関連付けを介してユーザーにアクセスできます。

The following example shows how associations can be secured:

次の例は、関連付けを保護する方法を示しています。

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

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
 #[ApiResource(graphQlOperations: [
    new Query(security: 'is_granted("VIEW", object)'),
    new QueryCollection(security: 'is_granted("ROLE_ADMIN")')
 ])]
class User
{
    // ...

    /**
     * @ORM\ManyToMany(targetEntity=Document::class, mappedBy="viewers")
     */
    #[ApiProperty(security: 'is_granted("VIEW", object)')]
    private Collection $viewableDocuments;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    #[ApiProperty(security: 'is_granted("ROLE_ADMIN")')]
    private string $email;
}
<?php
// api/src/Entity/Document.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
 #[ApiResource(graphQlOperations: [
    new Query(security: 'is_granted("VIEW", object)'),
    new QueryCollection(security: 'is_granted("ROLE_ADMIN")')
 ])]
class Document
{
    // ...

    /**
     * @ORM\ManyToMany(targetEntity=User::class, inversedBy="viewableDocuments")
     */
    #[ApiProperty(security: 'is_granted("VIEW", object)')]
    private Collection $viewers;

    /**
     * @ORM\ManyToOne(targetEntity=User::class)
     */
    #[ApiProperty(security: 'is_granted("VIEW", object)')]
    protected ?User $createdBy = null;
}

The above example only allows admins to see the full collection of each resource (QueryCollection). Users must be granted the VIEW attribute on a resource to be able to query it directly (Query) - which would use a Voter to make this decision.

上記の例では、管理者は各リソース (QueryCollection) の完全なコレクションを表示することしかできません。ユーザーがリソースを直接クエリ (クエリ) できるようにするには、リソースの VIEW 属性を付与する必要があります。これは、Voter を使用してこの決定を行います。

Similar to Query, all associations are secured, requiring VIEW access on the parent object (not on the association). This means that a user with VIEW access to a Document is able to see all users who are in the viewers collection, as well as the createdBy association. This may be a little too open, so you could instead do a role check here to only allow admins to access these fields, or check for a different attribute that could be implemented in the voter (e.g. VIEW_CREATED_BY.) Alternatively, you could still expose the users, but limit the visible fields by limiting access with ApiProperty security (such as the User::$email property above) or with dynamic serializer groups.

Query と同様に、すべての関連付けは保護されており、(関連付けではなく) 親オブジェクトに対する VIEW アクセスが必要です。これは、Document への VIEW アクセスを持つユーザーが、viewers コレクション内のすべてのユーザーを表示できることを意味します。 createdBy アソシエーション。これは少しオープンすぎる可能性があるため、代わりにここでロール チェックを実行して、管理者のみがこれらのフィールドにアクセスできるようにするか、投票者に実装できる別の属性 (VIEW_CREATED_BY など) をチェックすることができます。ユーザーを公開することはできますが、ApiProperty セキュリティ (上記の User::$email プロパティなど) または動的シリアライザー グループを使用してアクセスを制限することにより、表示されるフィールドを制限します。

Serialization Groups

You may want to restrict some resource's attributes to your GraphQL clients.

一部のリソースの属性を GraphQL クライアントに制限したい場合があります。

As described in the serialization process documentation, you can use serialization groups to expose only the attributes you want in queries or in mutations.

シリアル化プロセスのドキュメントで説明されているように、シリアル化グループを使用して、クエリまたはミューテーションで必要な属性のみを公開できます。

If the (de)normalization context between GraphQL and REST is different, use the (de)normalizationContext key to change it in each query and mutations.

GraphQL と REST の間の (非) 正規化コンテキストが異なる場合は、(非) 正規化コンテキスト キーを使用して、各クエリとミューテーションでそれを変更します。

Note that:

ご了承ください:

  • A query is only using the normalization context.
    クエリは、正規化コンテキストのみを使用しています。
  • A mutation is using the denormalization context for its input and the normalization context for its output.
    ミューテーションは、入力に非正規化コンテキストを使用し、出力に正規化コンテキストを使用しています。

The following example shows you what can be done:

次の例は、何ができるかを示しています。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
    normalizationContext: ['groups' => ['read']], 
    denormalizationContext: ['groups' => ['write']],
    graphQlOperations: [
        new Query(normalizationContext: ['groups' => ['query']]),
        new QueryCollection(normalizationContext: ['groups' => ['query_collection']])
        new Mutation(
            name: 'create',
            normalizationContext: ['groups' => ['query_collection']],
            denormalizationContext: ['groups' => ['mutation']]
        )
    ]
)]
class Book
{
    // ...

    #[Groups(['read', 'write', 'query', 'query_collection'])]
    public $title;

    #[Groups(['read', 'mutation', 'query'])]
    public $author;

    // ...
}

In this case, the REST endpoint will be able to get the two attributes of the book and to modify only its title.

この場合、REST エンドポイントは本の 2 つの属性を取得し、タイトルのみを変更できます。

The GraphQL endpoint will be able to query the title and author of an item. It will be able to query the title of the items in the collection. It will only be able to create a book with an author. When doing this mutation, the author of the created book will not be returned (the title will be instead).

GraphQL エンドポイントは、アイテムのタイトルと作成者をクエリできます。コレクション内のアイテムのタイトルをクエリできます。作成できるのは、作成者のいる本のみです。このミューテーションを実行すると、作成された本の著者は返されません(代わりにタイトルが返されます)。

Different Types when Using Different Serialization Groups

When you use different serialization groups, it will create different types in your schema.

異なるシリアル化グループを使用すると、スキーマに異なる型が作成されます。

Make sure you understand the implications when doing this: having different types means breaking the cache features in some GraphQL clients (in Apollo Client for example).

これを行う際の意味を理解していることを確認してください: 異なる型を持つことは、一部の GraphQL クライアント (たとえば Apollo クライアント) のキャッシュ機能を壊すことを意味します。

For instance:

例えば:

  • If you use a different normalizationContext for a mutation, a MyResourcePayloadData type with the restricted fields will be generated and used instead of MyResource (the query type).
    ミューテーションに別の normalizationContext を使用すると、制限されたフィールドを持つ MyResourcePayloadData タイプが生成され、MyResource (クエリ タイプ) の代わりに使用されます。
  • If you use a different normalizationContext for the query of an item (Query attribute) and for the query of a collection (QueryCollection attribute), two types MyResourceItem and MyResourceCollection with the restricted fields will be generated and used instead of MyResource (the query type).
    アイテムのクエリ (Query 属性) とコレクションのクエリ (QueryCollection 属性) に異なる normalizationContext を使用する場合、制限されたフィールドを持つ MyResourceItem と MyResourceCollection の 2 つのタイプが生成され、MyResource (クエリの種類) の代わりに使用されます。 .

Embedded Relation Input (Creation of Relation in Mutation)

By default, creating a relation when using a create or update mutation is not possible.

デフォルトでは、create または update ミューテーションを使用するときにリレーションを作成することはできません。

Indeed, the mutation expects an IRI for the relation in the input, so you need to use an existing relation.

実際、ミューテーションは入力のリレーションの IRI を想定しているため、既存のリレーションを使用する必要があります。

For instance if you have the following resource:

たとえば、次のリソースがあるとします。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;

#[ApiResource(graphQlOperations: [new Mutation(name: 'create')])]
class Book
{
    // ...

    public string $title;

    public ?Author $author;

    // ...
}

Creating a book with its author will be done like this, where /authors/32 is the IRI of an existing resource:

/authors/32 は既存のリソースの IRI です。

{
  mutation {
    createBook(input: {title: "The Name of the Wind", author: "/authors/32"}) {
      book {
        title
        author {
          name
        }
      }
    }
  }
}

In order to create an author as the same time as a book, you need to use the denormalization context and groups on the book and the author (see also the dedicated part in the serialization documentation:

本と同時に著者を作成するには、本と著者の非正規化コンテキストとグループを使用する必要があります (連載ドキュメントの専用部分も参照してください:

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(graphQlOperations: [new Mutation(name: 'create', denormalizationContext: ['groups' => ['book:create']])])]
class Book
{
    // ...

    #[Groups(['book:create'])]
    public string $title;

    #[Groups(['book:create'])]
    public ?Author $author;

    // ...
}

And in the author resource:

そして、著者のリソースで:

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

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource]
class Author
{
    // ...

    #[Groups(['book:create'])]
    public string $name;

    // ...
}

In this case, creating a book with its author can now be done like this:

この場合、著者を含む本の作成は次のように実行できます。

{
  mutation {
    createBook(input: {title: "The Name of the Wind", author: {name: "Patrick Rothfuss"}}) {
      book {
        title
        author {
          name
        }
      }
    }
  }
}

Exception and Error

Handling Exceptions and Errors (Logging, Filtering, ...)

When there are errors (GraphQL ones, or if an exception is sent), a default error handler (api_platform.graphql.error_handler) is called. Its main responsibility is to apply a formatter to them.

エラー (GraphQL エラー、または例外が送信された場合) がある場合、デフォルトのエラー ハンドラー (api_platform.graphql.error_handler) が呼び出されます。その主な役割は、それらにフォーマッターを適用することです。

If you need to log the errors, or if you want to filter them, you have to decorate this service.

エラーをログに記録する必要がある場合、またはエラーをフィルター処理する必要がある場合は、このサービスをデコレートする必要があります。

For instance, create a class like this:

たとえば、次のようなクラスを作成します。

<?php
// api/src/Error/ErrorHandler.php
namespace App\Error;

use ApiPlatform\GraphQl\Error\ErrorHandlerInterface;

final class ErrorHandler implements ErrorHandlerInterface
{
    private $defaultErrorHandler;

    public function __construct(ErrorHandlerInterface $defaultErrorHandler)
    {
        $this->defaultErrorHandler = $defaultErrorHandler;
    }

    /**
     * {@inheritdoc}
     */
    public function __invoke(array $errors, callable $formatter): array
    {
        // Log or filter the errors.

        return ($this->defaultErrorHandler)($errors, $formatter);
    }
}

Then register the service:

次に、サービスを登録します。

[codeSelector]

[コードセレクター]

# api/config/services.yaml
services:
    # ...
    App\Error\ErrorHandler:
        decorates: api_platform.graphql.error_handler
<?xml version="1.0" encoding="UTF-8" ?>
<!-- api/config/services.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance"
    xsd:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="App\Error\ErrorHandler"
            decorates="api_platform.graphql.error_handler"
        />
    </services>
</container>
<?php
// api/config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Error\ErrorHandler;
use App\Mailer;

return function(ContainerConfigurator $configurator) {
    $services = $configurator->services();

    $services->set(ErrorHandler::class)
        ->decorate('api_platform.graphql.error_handler');
};

[/codeSelector]

[/コードセレクター]

Formatting Exceptions and Errors

By default, if an exception is sent when resolving a query or a mutation or if there are GraphQL errors, they are normalized following the GraphQL specification.

デフォルトでは、クエリまたはミューテーションの解決時に例外が送信された場合、または GraphQL エラーがある場合、それらは GraphQL 仕様に従って正規化されます。

It means an errors entry will be returned in the response, containing the following entries: message, extensions, locations and path. For more information, please refer to the documentation in graphql-php.

これは、次のエントリを含むエラー エントリが応答で返されることを意味します: メッセージ、拡張子、場所、およびパス。詳細については、graphql-php のドキュメントを参照してください。

In prod mode, the displayed message will be a generic one, excepted for a RuntimeException (and all exceptions inherited from it) for which it will be its actual message. This behavior is different from what is described in the graphql-php documentation. It's because a built-in custom exception normalizer is used to normalize the RuntimeException and change the default behavior.

prod モードでは、表示されるメッセージは、実際のメッセージとなる RuntimeException (およびそれから継承されたすべての例外) を除いて、一般的なものになります。組み込みのカスタム例外ノーマライザーを使用して、RuntimeException を正規化し、デフォルトの動作を変更するためです。

If you are in dev mode, more entries will be added in the response: debugMessage (containing the actual exception message, for instance in the case of a LogicException) and trace (the formatted exception trace).

dev モードの場合、応答にさらにエントリが追加されます: debugMessage (LogicException の場合などの実際の例外メッセージを含む) および trace (フォーマットされた例外トレース)。

For some specific exceptions, built-in custom exception normalizers are also used to add more information. It's the case for a HttpException for which the status entry will be added under extensions and for a ValidationException for which status (by default 422) and violations entries will be added.

一部の特定の例外については、組み込みのカスタム例外ノーマライザーを使用して、さらに情報を追加することもできます。これは、拡張機能の下にステータス エントリが追加される HttpException と、ステータス (デフォルトでは 422) および違反エントリの ValidationException の場合です。追加されます。

Custom Exception Normalizer

If you want to add more specific behaviors depending on the exception or if you want to change the behavior of the built-in ones, you can do so by creating your own normalizer.

例外に応じてより具体的な動作を追加したい場合、または組み込みの動作を変更したい場合は、独自のノーマライザーを作成することで実行できます。

Please follow the Symfony documentation to create a custom normalizer.

Symfony のドキュメントに従って、カスタム ノーマライザーを作成してください。

The code should look like this:

コードは次のようになります。

<?php
// api/src/Serializer/Exception/MyExceptionNormalizer.php
namespace App\Serializer\Exception;

use App\Exception\MyException;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class MyExceptionNormalizer implements NormalizerInterface
{
    /**
     * {@inheritdoc}
     */
    public function normalize($object, $format = null, array $context = []): array
    {
        $exception = $object->getPrevious();
        $error = FormattedError::createFromException($object);

        // Add your logic here and add your specific data in the $error array (in the 'extensions' entry to follow the GraphQL specification).
        // $error['extensions']['yourEntry'] = ...;

        return $error;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsNormalization($data, $format = null): bool
    {
        return $data instanceof Error && $data->getPrevious() instanceof MyException;
    }
}

You can see that, in the normalize method, you should add a call to FormattedError::createFromException in order to have the same behavior as the other normalizers.

他のノーマライザーと同じ動作をさせるには、normalize メソッドで FormattedError::createFromException への呼び出しを追加する必要があることがわかります。

When registering your custom normalizer, you can add a priority to order your normalizers between themselves.

カスタム ノーマライザーを登録するときに、ノーマライザーを順番に並べる優先度を追加できます。

If you use a positive priority (or no priority), your normalizer will always be called before the built-in normalizers. For instance, you can register a custom normalizer like this:

正の優先度 (または優先度なし) を使用する場合、ノーマライザーは常に組み込みノーマライザーの前に呼び出されます。たとえば、次のようにカスタム ノーマライザーを登録できます。

# api/config/services.yaml
services:
    App\Serializer\Exception\MyExceptionNormalizer:
        tags:
            - { name: 'serializer.normalizer', priority: 12 }

Name Conversion

You can modify how the property names of your resources are converted into field and filter names of your GraphQL schema.

リソースのプロパティ名を GraphQL スキーマのフィールド名およびフィルター名に変換する方法を変更できます。

By default the property name will be used without conversion. If you want to apply a name converter, follow the Name Conversion documentation.

デフォルトでは、プロパティ名は変換なしで使用されます。名前コンバーターを適用する場合は、名前変換のドキュメントに従ってください。

For instance, your resource can have properties in camelCase:

たとえば、リソースは camelCase でプロパティを持つことができます。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['publicationDate' => 'partial'])]
class Book
{
    // ...

    public $publicationDate;

    // ...
}

By default, with the search filter, the query to retrieve a collection will be:

デフォルトでは、検索フィルターを使用すると、コレクションを取得するためのクエリは次のようになります。

{
  books(publicationDate: "2010") {
    edges {
      node {
        publicationDate
      }
    }
  }
}

But if you use the CamelCaseToSnakeCaseNameConverter, it will be:

しかし、CamelCaseToSnakeCaseNameConverter を使用すると、次のようになります。

{
  books(publication_date: "2010") {
    edges {
      node {
        publication_date
      }
    }
  }
}

Nesting Separator

If you use snake_case, you can wonder how to make the difference between an underscore and the separator of the nested fields in the filter names, by default an underscore too.

snake_case を使用する場合、アンダースコアとフィルター名のネストされたフィールドの区切り記号 (デフォルトではアンダースコアも) の違いをどのように区別するのか疑問に思うかもしれません。

For instance if you have this resource:

たとえば、次のリソースがある場合:

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['relatedBooks.title' => 'exact'])]
class Book
{
    // ...

    public $title;

    #[ORM\OneToMany(targetEntity: Book::class)]
    public $relatedBooks;

    // ...
}

You would need to use the search filter like this:

次のように検索フィルターを使用する必要があります。

{
  books(related_books_title: "The Fitz and the Fool") {
    edges {
      node {
        title
      }
    }
  }
}

To avoid this issue, you can configure the nesting separator to use, for example, __ instead of _:

この問題を回避するには、_ の代わりに __ などを使用するようにネスト区切り記号を構成できます。

# api/config/packages/api_platform.yaml
api_platform:
    graphql:
        nesting_separator: __
# ...

In this case, your query will be:

この場合、クエリは次のようになります。

{
  books(related_books__title: "The Fitz and the Fool") {
    edges {
      node {
        title
      }
    }
  }
}

Much better, isn't it?

はるかに良いですね。

Custom Types

You might need to add your own types to your GraphQL application.

独自の型を GraphQL アプリケーションに追加する必要がある場合があります。

Create your type class by implementing the interface ApiPlatform\GraphQl\Type\Definition\TypeInterface.

インターフェイス ApiPlatform\GraphQl\Type\Definition\TypeInterface を実装して、型クラスを作成します。

You should extend the GraphQL\Type\Definition\ScalarType class too to take advantage of its useful methods.

GraphQL\Type\Definition\ScalarType クラスも拡張して、その便利なメソッドを利用する必要があります。

For instance, to create a custom DateType:

たとえば、カスタム DateType を作成するには:

<?php
namespace App\Type\Definition;

use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
use GraphQL\Error\Error;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Utils\Utils;

final class DateTimeType extends ScalarType implements TypeInterface
{
    public function __construct()
    {
        $this->name = 'DateTime';
        $this->description = 'The `DateTime` scalar type represents time data.';

        parent::__construct();
    }

    public function getName(): string
    {
        return $this->name;
    }

    /**
     * {@inheritdoc}
     */
    public function serialize($value)
    {
        // Already serialized.
        if (\is_string($value)) {
            return (new \DateTime($value))->format('Y-m-d');
        }

        if (!($value instanceof \DateTime)) {
            throw new Error(sprintf('Value must be an instance of DateTime to be represented by DateTime: %s', Utils::printSafe($value)));
        }

        return $value->format(\DateTime::ATOM);
    }

    /**
     * {@inheritdoc}
     */
    public function parseValue($value)
    {
        if (!\is_string($value)) {
            throw new Error(sprintf('DateTime cannot represent non string value: %s', Utils::printSafeJson($value)));
        }

        if (false === \DateTime::createFromFormat(\DateTime::ATOM, $value)) {
            throw new Error(sprintf('DateTime cannot represent non date value: %s', Utils::printSafeJson($value)));
        }

        // Will be denormalized into a \DateTime.
        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function parseLiteral($valueNode, ?array $variables = null)
    {
        if ($valueNode instanceof StringValueNode && false !== \DateTime::createFromFormat(\DateTime::ATOM, $valueNode->value)) {
            return $valueNode->value;
        }

        // Intentionally without message, as all information already in wrapped Exception
        throw new \Exception();
    }
}

You can also check the documentation of graphql-php.

また、graphql-php のドキュメントを確認することもできます。

The big difference in API Platform is that the value is already serialized when it's received in your type class. Similarly, you would not want to denormalize your parsed value since it will be done by API Platform later.

API プラットフォームの大きな違いは、値が型クラスで受信されたときに既にシリアル化されていることです。同様に、解析された値は後で API プラットフォームによって行われるため、非正規化する必要はありません。

If you use autoconfiguration (the default Symfony configuration) in your application, then you are done!

アプリケーションで自動構成 (デフォルトの Symfony 構成) を使用する場合は、完了です!

Else, you need to tag your type class like this:

それ以外の場合は、次のように型クラスにタグを付ける必要があります。

# api/config/services.yaml
services:
    # ...
    App\Type\Definition\DateTimeType:
        tags:
            - { name: api_platform.graphql.type }

Your custom type is now registered and is available in the TypesContainer.

カスタム タイプが登録され、TypesContainer で使用できるようになりました。

To use it please modify the extracted types or use it directly in custom queries or custom mutations.

使用するには、抽出されたタイプを変更するか、カスタム クエリまたはカスタム ミューテーションで直接使用してください。

Modify the Extracted Types

The GraphQL schema and its types are extracted from your resources. In some cases, you would want to modify the extracted types for instance to use your custom ones.

GraphQL スキーマとそのタイプは、リソースから抽出されます。場合によっては、抽出されたタイプを変更して、カスタムのものを使用したいことがあります。

To do so, you need to decorate the api_platform.graphql.type_converter service:

そのためには、api_platform.graphql.type_converter サービスを装飾する必要があります。

# api/config/services.yaml
services:
    # ...
    'App\Type\TypeConverter':
        decorates: api_platform.graphql.type_converter

Your class needs to look like this:

クラスは次のようにする必要があります。

<?php
namespace App\Type;

use ApiPlatform\GraphQl\Type\TypeConverterInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use App\Entity\Book;
use GraphQL\Type\Definition\Type as GraphQLType;
use Symfony\Component\PropertyInfo\Type;

final class TypeConverter implements TypeConverterInterface
{
    private $defaultTypeConverter;

    public function __construct(TypeConverterInterface $defaultTypeConverter)
    {
        $this->defaultTypeConverter = $defaultTypeConverter;
    }

    /**
     * {@inheritdoc}
     */
    public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth)
    {
        if ('publicationDate' === $property
            && Book::class === $resourceClass
        ) {
            return 'DateTime';
        }

        return $this->defaultTypeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
    }

    /**
     * {@inheritdoc}
     */
    public function resolveType(string $type): ?GraphQLType
    {
        return $this->defaultTypeConverter->resolveType($type);
    }
}

In this case, the publicationDate property of the Book class will have a custom DateTime type.

この場合、Book クラスの publicationDate プロパティはカスタムの DateTime 型になります。

You can even apply this logic for a kind of property. Replace the previous condition with something like this:

このロジックをある種のプロパティに適用することもできます。前の条件を次のように置き換えます。

if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType()
    && is_a($type->getClassName(), \DateTimeInterface::class, true)
) {
    return 'DateTime';
}

All DateTimeInterface properties will have the DateTime type in this example.

この例では、すべての DateTimeInterface プロパティが DateTime 型になります。

Changing the Serialization Context Dynamically

As REST, it's possible to add dynamically a (de)serialization group when resolving a query or a mutation.

REST として、クエリまたはミューテーションを解決するときに (逆) シリアル化グループを動的に追加できます。

There are some differences though.

ただし、いくつかの違いがあります。

The service is api_platform.graphql.serializer.context_builder and the method to override is create.

サービスは api_platform.graphql.serializer.context_builder で、オーバーライドするメソッドは create です。

The decorator could be like this:

デコレータは次のようになります。

<?php
namespace App\Serializer;

use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface;
use App\Entity\Book;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class BookContextBuilder implements SerializerContextBuilderInterface
{
    private $decorated;
    private $authorizationChecker;

    public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->decorated = $decorated;
        $this->authorizationChecker = $authorizationChecker;
    }

    public function create(?string $resourceClass, string $operationName, array $resolverContext, bool $normalization): array
    {
        $context = $this->decorated->create($resourceClass, $operationName, $resolverContext, $normalization);
        $resourceClass = $context['resource_class'] ?? null;

        if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) {
            $context['groups'][] = 'admin:input';
        }

        return $context;
    }
}

Export the Schema in SDL

You may need to export your schema in SDL (Schema Definition Language) to import it in some tools.

一部のツールにスキーマをインポートするには、SDL (スキーマ定義言語) でスキーマをエクスポートする必要がある場合があります。

The api:graphql:export command is provided to do so:

そのために api:graphql:export コマンドが提供されています。

docker compose exec php \
    bin/console api:graphql:export -o path/to/your/volume/schema.graphql

Since the command prints the schema to the output if you don't use the -o option, you can also use this command:

-o オプションを使用しない場合、コマンドはスキーマを出力に出力するため、次のコマンドも使用できます。

docker compose exec php \
    bin/console api:graphql:export > path/in/host/schema.graphql

Handling File Upload

Please follow the file upload documentation, only the differences will be documented here.

ファイル アップロードのドキュメントに従ってください。ここでは相違点のみを説明します。

The file upload with GraphQL follows the GraphQL multipart request specification.

GraphQL を使用したファイルのアップロードは、GraphQL マルチパート リクエスト仕様に従います。

You can also upload multiple files at the same time.

複数のファイルを同時にアップロードすることもできます。

Configuring the Entity Receiving the Uploaded File

Configure the entity by adding a custom mutation resolver:

カスタム ミューテーション リゾルバーを追加して、エンティティを構成します。

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

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use App\Resolver\CreateMediaObjectResolver;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * @Vich\Uploadable
 */
#[ORM\Entity]
#[ApiResource(
    normalizationContext: ['groups' => ['media_object_read']], 
    types: ['https://schema.org/MediaObject'],
    graphQlOperations: [
        new Mutation(
            name: 'upload', 
            resolver: CreateMediaObjectResolver::class, 
            deserialize: false, 
            args: [
                'file' => [
                    'type' => 'Upload!', 
                    'description' => 'The file to upload'
                ]
            ]
        )
    ]
)]
class MediaObject
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    protected ?int $id = null;

    #[ApiProperty(types: ['https://schema.org/contentUrl'])]
    #[Groups(['media_object_read'])]
    public ?string $contentUrl = null;

    /**
     * @Vich\UploadableField(mapping="media_object", fileNameProperty="filePath")
     */
    #[Assert\NotNull(groups: ['media_object_create'])] 
    public ?File $file = null;

    #[ORM\Column(nullable: true)]
    public ?string $filePath = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}

As you can see, a dedicated type Upload is used in the argument of the upload mutation. If you need to upload multiple files, replace 'file' => ['type' => 'Upload!', 'description' => 'The file to upload'] with 'files' => ['type' => '[Upload!]!', 'description' => 'Files to upload'].

ご覧のとおり、upload ミューテーションの引数には専用のタイプ Upload が使用されています。複数のファイルをアップロードする必要がある場合は、'file' => ['type' => 'Upload!', 'description' => ' を置き換えます。アップロードするファイル'] with 'files' => ['type' => '[Upload!]!', 'description' => 'アップロードするファイル'].

You don't need to create it, it's provided in API Platform.

API Platform で提供されているため、作成する必要はありません。

Resolving the File Upload

The corresponding resolver you added in the resource configuration should be written like this:

リソース構成に追加した対応するリゾルバーは、次のように記述する必要があります。

<?php
// api/src/Resolver/CreateMediaObjectResolver.php
namespace App\Resolver;

use ApiPlatform\GraphQl\Resolver\MutationResolverInterface;
use App\Entity\MediaObject;
use Symfony\Component\HttpFoundation\File\UploadedFile;

final class CreateMediaObjectResolver implements MutationResolverInterface
{
    /**
     * @param null $item
     */
    public function __invoke($item, array $context): MediaObject
    {
        $uploadedFile = $context['args']['input']['file'];

        $mediaObject = new MediaObject();
        $mediaObject->file = $uploadedFile;

        return $mediaObject;
    }
}

For handling the upload of multiple files, iterate over $context['args']['input']['files'].

複数のファイルのアップロードを処理するには、$context['args']['input']['files'] を繰り返します。

Using the createMediaObject Mutation

Following the specification, the upload must be done with a multipart/form-data content type.

仕様に従って、アップロードは multipart/form-data コンテンツ タイプで行う必要があります。

You need to enable it in the allowed formats of API Platform:

API プラットフォームの許可された形式で有効にする必要があります。

# api/config/packages/api_platform.yaml
api_platform:
    formats:
        # ...
        multipart: ['multipart/form-data']

You can now upload files using the createMediaObject mutation, for details check GraphQL multipart request specification and for an example implementation for the Apollo client check out Apollo Upload Client.

createMediaObject ミューテーションを使用してファイルをアップロードできるようになりました。詳細については、GraphQL マルチパート リクエストの仕様を確認してください。Apollo クライアントの実装例については、Apollo Upload Client を確認してください。

mutation CreateMediaObject($file: Upload!) {
    createMediaObject(input: {file: $file}) {
        mediaObject {
            id
            contentUrl
        }
    }
}

Change Default Descriptions

By default, API Platform generates descriptions for mutations and subscriptions.

デフォルトでは、API プラットフォームはミューテーションとサブスクリプションの説明を生成します。

If you want to change them, or add some for queries, you can do it in the resource declaration, at the operation level, with the description attribute.

それらを変更したり、クエリ用に追加したりしたい場合は、操作レベルのリソース宣言で description 属性を使用して実行できます。

For instance, if you want to change the description of the create mutation:

たとえば、create ミューテーションの説明を変更する場合は、次のようにします。

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

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;

#[ApiResource(graphQlOperations: [
    new Mutation(name: 'create', description: 'My custom description.')
])]
class Book
{
    // ...
}