# GraphQL Support [GraphQL](https://graphql.org/) is a query language made to communicate with an API and therefore is an alternative to 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. API Platform creates a REST API by default. But you can choose to enable GraphQL as well. Once enabled, you have nothing to do: your schema describing your API is automatically built and your GraphQL endpoint is ready to go! ## Enabling GraphQL To enable GraphQL and its IDE (GraphiQL and GraphQL Playground) in your API, simply require the [graphql-php](https://webonyx.github.io/graphql-php/) package using Composer and clear the cache one more time: $ docker-compose exec php composer req webonyx/graphql-php && docker-compose exec php bin/console cache:clear You can now use GraphQL at the endpoint: `https://localhost:8443/graphql`. *Note:* If you used [Symfony Flex to install API Platform](../distribution/index.md#using-symfony-flex-and-composer-advanced-users), the GraphQL endpoint will be: `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. ```yaml # 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. ## 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. The GraphiQL IDE can also be found at `/graphql/graphiql`. If you need to disable it, it can be done in the configuration: ```yaml # 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: ```yaml # 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. It can be found at `/graphql/graphql_playground`. You can disable it if you want in the configuration: ```yaml # 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`: ```yaml # 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. ```yaml # 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`. ```yaml # 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](https://graphql.org/learn/serving-over-http/#post-request), you need to enable it in the [allowed formats of API Platform](content-negotiation.md#configuring-formats-globally): ```yaml # api/config/packages/api_platform.yaml api_platform: formats: # ... graphql: ['application/graphql'] ``` ## Queries If you don't know what queries are yet, please [read the documentation about them](https://graphql.org/learn/queries/). 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. ### Global Object Identifier When querying an item, you need to pass an identifier as argument. Following the [GraphQL Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm), the identifier needs to be globally unique. In API Platform, this argument is represented as an [IRI (Internationalized Resource Identifier)](https://www.w3.org/TR/ld-glossary/#internationalized-resource-identifier). For example, to query a book having as identifier `89`, you have to run the following: ```graphql { book(id: "/books/89") { title isbn } } ``` Note that in this example, we're retrieving two fields: `title` and `isbn`. ### 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 $collection * * @return iterable */ 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! Else, you need to tag your resolver like this: ```yaml # api/config/services.yaml services: # ... App\Resolver\BookCollectionResolver: tags: - { name: api_platform.graphql.query_resolver } ``` The resolver for an item is very similar: ```php 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: ```yaml # 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 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](https://webonyx.github.io/graphql-php/type-system/scalar-types/#writing-custom-scalar-types). 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. If you use autoconfiguration (the default Symfony configuration) in your application, then you are done! Else, you need to tag your type class like this: ```yaml # 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`. To use it please [modify the extracted types](#modify-the-extracted-types) or use it directly in [custom queries](#custom-queries) or [custom mutations](#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. To do so, you need to decorate the `api_platform.graphql.type_converter` service: ```yaml # api/config/services.yaml services: # ... 'App\Type\TypeConverter': decorates: api_platform.graphql.type_converter ``` Your class needs to look like this: ```php defaultTypeConverter = $defaultTypeConverter; } /** * {@inheritdoc} */ public function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth) { if ('publicationDate' === $property && Book::class === $resourceClass ) { return 'DateTime'; } return $this->defaultTypeConverter->convertType($type, $input, $queryName, $mutationName, $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. You can even apply this logic for a kind of property. Replace the previous condition with something like this: ```php 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. ## Changing the Serialization Context Dynamically [As REST](serialization.md#changing-the-serialization-context-dynamically), it's possible to add dynamically a (de)serialization group when resolving a query or a mutation. There are some differences though. The service is `api_platform.graphql.serializer.context_builder` and the method to override is `create`. The decorator could be like this: ```php 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. The `api:graphql:export` command is provided to do so: ```shell-session $ 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: ```shell-session $ docker-compose exec php bin/console api:graphql:export > path/in/host/schema.graphql ``` ## Handling File Upload Please follow the [file upload documentation](file-upload.md), only the differences will be documented here. The file upload with GraphQL follows the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). 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](#custom-mutations): ```php 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"}`. You don't need to create it, it's provided in API Platform. ### Resolving the File Upload The corresponding resolver you added in the resource configuration should be written like this: ```php file = $uploadedFile; return $mediaObject; } } ``` For handling the upload of multiple files, iterate over `$context['args']['input']['files']`. ### Using the `createMediaObject` Mutation Following the specification, the upload must be done with a `multipart/form-data` content type. You need to enable it in the [allowed formats of API Platform](content-negotiation.md#configuring-formats-globally): ```yaml # 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](https://github.com/jaydenseric/graphql-multipart-request-spec) and for an example implementation for the Apollo client check out [Apollo Upload Client](https://github.com/jaydenseric/apollo-upload-client). ```graphql mutation CreateMediaObject($file: Upload!) { createMediaObject(input: {file: $file}) { mediaObject { id contentUrl } } } ```