diff --git a/core/graphql.md b/core/graphql.md index 360ee56320e..c2f5d23d378 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -133,19 +133,15 @@ api_platform: To understand what an operation is, please refer to the [operations documentation](operations.md). -For GraphQL, the operations are defined under the `Query`, `QueryCollection` and `Mutation` attributes. -By default, all operations are enabled. +For GraphQL, the operations are defined by using the `Query`, `QueryCollection`, `Mutation` and `Subscription` attributes. -For the queries, the operations are: +By default, the following operations are enabled: * `Query` * `QueryCollection` - -For the mutations, the operations are: - -* `create` -* `update` -* `delete` +* `Mutation(name: 'create')` +* `Mutation(name: 'update')` +* `Mutation(name: 'delete')` You can of course disable or configure these operations. @@ -156,8 +152,8 @@ For instance, in the following example, only the query of an item and the create // api/src/Entity/Book.php namespace App\Entity; -use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\ApiResource; #[ApiResource] @@ -205,7 +201,7 @@ If you want a custom query for a collection, create a class like this: // api/src/Resolver/BookCollectionResolver.php namespace App\Resolver; -use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface; +use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; use App\Entity\Book; final class BookCollectionResolver implements QueryCollectionResolverInterface @@ -248,7 +244,7 @@ The resolver for an item is very similar: // api/src/Resolver/BookResolver.php namespace App\Resolver; -use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface; +use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; use App\Entity\Book; final class BookResolver implements QueryItemResolverInterface @@ -285,29 +281,35 @@ In your resource, add the following: // api/src/Entity/Book.php namespace App\Entity; -use App\Resolver\BookCollectionResolver; -use App\Resolver\BookResolver; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\ApiResource; +use App\Resolver\BookCollectionResolver; +use App\Resolver\BookResolver; #[ApiResource] -/** Auto generated graphql operations and mutations */ +/** Auto-generated queries and mutations */ #[Query] #[QueryCollection] #[Mutation(name: 'create')] #[Mutation(name: 'update')] #[Mutation(name: 'delete')] -/** Custom graphql operations */ +/** Custom queries */ #[Query(name: 'retrievedQuery', resolver: BookResolver::class)] -#[Query(name: 'notRetrievedQuery', resolver: BookResolver::class, +#[Query( + name: 'notRetrievedQuery', + resolver: BookResolver::class, args: [] )] -#[Query(name: 'withDefaultArgsNotRetrievedQuery', resolver: BookResolver::class, +#[Query( + name: 'withDefaultArgsNotRetrievedQuery', + resolver: BookResolver::class, read: false )] -#[Query(name: 'withCustomArgsQuery', resolver: BookResolver::class, +#[Query( + name: 'withCustomArgsQuery', + resolver: BookResolver::class, args: [ 'id' => ['type' => 'ID!'], 'log' => ['type' => 'Boolean!', 'description' => 'Is logging activated?'], @@ -404,7 +406,7 @@ Create your resolver: // api/src/Resolver/BookMutationResolver.php namespace App\Resolver; -use ApiPlatform\Core\GraphQl\Resolver\MutationResolverInterface; +use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; use App\Entity\Book; final class BookMutationResolver implements MutationResolverInterface @@ -445,22 +447,24 @@ Now in your resource: // api/src/Entity/Book.php namespace App\Entity; -use App\Resolver\BookMutationResolver; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\ApiResource; +use App\Resolver\BookMutationResolver; #[ApiResource] -/** Auto generated graphql operations and mutations */ +/** Auto-generated queries and mutations */ #[Query] #[QueryCollection] #[Mutation(name: 'create')] #[Mutation(name: 'update')] #[Mutation(name: 'delete')] -/** Custom graphql operations */ +/** Custom mutations */ #[Mutation(name: 'mutation', resolver: BookMutationResolver::class)] -#[Mutation(name: 'withCustomArgsMutation', resolver: BookMutationResolver::class, +#[Mutation( + name: 'withCustomArgsMutation', + resolver: BookMutationResolver::class, args: [ 'sendMail' => [ 'type' => 'Boolean!', @@ -468,7 +472,9 @@ use ApiPlatform\Metadata\ApiResource; ] ] )] -#[Mutation(name: 'disabledStagesMutation', resolver: BookMutationResolver::class, +#[Mutation( + name: 'disabledStagesMutation', + resolver: BookMutationResolver::class, deserialize: false, write: false )] @@ -525,11 +531,12 @@ In API Platform, the built-in subscription support is handled by using [Mercure] ### Enable Update Subscriptions for a Resource -To enable update subscriptions for a resource, three conditions have to be met: +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.md#installing-mercure-support). * Mercure needs to be enabled for the resource. * the `update` mutation needs to be enabled for the resource. +* the subscription needs to be enabled for the resource. For instance, your resource should look like this: @@ -540,9 +547,11 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Subscription; #[ApiResource(mercure: true)] #[Mutation(name: 'update')] +#[Subscription] class Book { // ... @@ -622,7 +631,7 @@ Create your *WriteStage*: ['read']], denormalizationContext: ['groups' => ['write']] )] -#[Query( - normalizationContext: ['groups' => ['Query']] -)] -#[QueryCollection( - normalizationContext: ['groups' => ['QueryCollection']] -)] +#[Query(normalizationContext: ['groups' => ['query']])] +#[QueryCollection(normalizationContext: ['groups' => ['query_collection']])] #[Mutation( - name: 'create', - normalizationContext: ['groups' => ['QueryCollection']], + name: 'create', + normalizationContext: ['groups' => ['query_collection']], denormalizationContext: ['groups' => ['mutation']] )] class Book { // ... - /** - * @Groups({"read", "write", "Query", "QueryCollection"}) - */ - public $name; + #[Groups(['read', 'write', 'query', 'query_collection'])] + public $title; - /** - * @Groups({"read", "mutation", "Query"}) - */ + #[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 name. +In this case, the REST endpoint will be able to get the two attributes of the book and to modify only its title. -The GraphQL endpoint will be able to query the name and author of an item. -It will be able to query the name of the items in the collection. +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 name will be instead). +When doing this mutation, the author of the created book will not be returned (the title will be instead). ### Different Types when Using Different Serialization Groups @@ -1287,7 +1286,122 @@ Make sure you understand the implications when doing this: having different type 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). -* If you use a different `normalizationContext` for the query of an item (`Query` operation) and for the query of a collection (`QueryCollection` operation), two types `MyResourceItem` and `MyResourceCollection` with the restricted fields will be generated and used instead of `MyResource` (the query type). +* 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). + +### Embedded Relation Input (Creation of Relation in Mutation) + +By default, creating a relation when using a `create` or `update` mutation is not possible. + +Indeed, the mutation expects an IRI for the relation in the input, so you need to use an existing relation. + +For instance if you have the following resource: + +```php + ['book:create']])] +class Book +{ + // ... + + #[Groups(['book:create'])] + public string $title; + + #[Groups(['book:create'])] + public ?Author $author; + + // ... +} +``` + +And in the author resource: + +```php + 'exact'])] +#[ApiFilter(SearchFilter::class, properties: ['relatedBooks.title' => 'exact'])] class Book { // ... - public $name; + public $title; /** * @ORM\OneToMany(targetEntity="Book") @@ -1541,10 +1655,10 @@ You would need to use the search filter like this: ```graphql { - books(related_books_name: "The Fitz and the Fool") { + books(related_books_title: "The Fitz and the Fool") { edges { node { - name + title } } } @@ -1565,10 +1679,10 @@ In this case, your query will be: ```graphql { - books(related_books__name: "The Fitz and the Fool") { + books(related_books__title: "The Fitz and the Fool") { edges { node { - name + title } } } @@ -1581,7 +1695,7 @@ Much better, isn't it? You might need to add your own types to your GraphQL application. -Create your type class by implementing the interface `ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface`. +Create your type class by implementing the interface `ApiPlatform\GraphQl\Type\Definition\TypeInterface`. You should extend the `GraphQL\Type\Definition\ScalarType` class too to take advantage of its useful methods. @@ -1591,7 +1705,7 @@ For instance, to create a custom `DateType`: ['type' => 'Upload!','description' => 'The file to upload'] -'files' => ['type' => '[Upload!]!','description' => 'Files to upload'] - 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']`. +If you need to upload multiple files, replace `'file' => ['type' => 'Upload!', 'description' => 'The file to upload']` +with `'files' => ['type' => '[Upload!]!', 'description' => 'Files to upload']`. You don't need to create it, it's provided in API Platform. @@ -1922,7 +2032,7 @@ The corresponding resolver you added in the resource configuration should be wri // api/src/Resolver/CreateMediaObjectResolver.php namespace App\Resolver; -use ApiPlatform\Core\GraphQl\Resolver\MutationResolverInterface; +use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; use App\Entity\MediaObject; use Symfony\Component\HttpFoundation\File\UploadedFile; diff --git a/distribution/index.md b/distribution/index.md index 5bf3ff09766..03153a0de0f 100644 --- a/distribution/index.md +++ b/distribution/index.md @@ -514,7 +514,9 @@ docker-compose exec php \ bin/console make:entity --api-resource ``` -Doctrine's [annotations](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html) map these entities to tables in the database. Mapping through [attributes](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/attributes-reference.html) is also supported, if you prefer those. Both methods are convenient as they allow grouping the code and the configuration but, if you want to decouple classes from their metadata, you can switch to XML or YAML mappings. +Doctrine's [annotations](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html) map these entities to tables in the database. +Mapping through [attributes](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/attributes-reference.html) is also supported, if you prefer those. +Both methods are convenient as they allow grouping the code and the configuration but, if you want to decouple classes from their metadata, you can switch to XML or YAML mappings. They are supported as well. Learn more about how to map entities with the Doctrine ORM in [the project's official documentation](https://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html)