Skip to content

Commit a8bc457

Browse files
joaogarinpmelab
authored andcommitted
docs(mutations): Documentation for mutations (#718)
* Provide documentation for mutation plugins * fix markdown * update docs * fix typos and other adjustments * indent php code
1 parent 573153e commit a8bc457

4 files changed

Lines changed: 353 additions & 5 deletions

File tree

doc/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
* [Introduction](mutations/readme.md)
2222
* [Creating mutation plugins](mutations/creating-mutation-plugins.md)
23+
* [Writing custom mutations](mutations/custom-mutations.md)
24+
* [Writing custom validations](mutations/custom-validations.md)
2325

2426
## Authentication
2527

Lines changed: 203 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,205 @@
1-
## Creating mutation plugins
1+
# Creating mutations for Entities
22

3-
WIP : For now for relevant information on custom mutations check the following links :
3+
The graphql module uses the [Drupal plugin system](https://www.drupal.org/docs/8/api/plugin-api/plugin-api-overview) for a lot of the extensibility features of plugins. So a lot of the times when you want to extend the graphql module (for example when creating your own mutations) you will be using the plugin system and creating your own plugins.
44

5-
- https://www.amazeelabs.com/en/blog/extending-graphql-part-3-mutations
6-
- https://github.com/justinlevi/graphql_custom_mutation
7-
- http://joaogarin.com/posts/drupal-graphql-with-angular-and-apollo-part3
5+
## Why the automatic mutations were removed.
6+
7+
In [this article](https://www.amazeelabs.com/en/blog/extending-graphql-part-3-mutations) it's well explained why automatic mutations were removed. But this, as stated in the article, does not mean that creating mutations is complicated. In fact, it's a simple task and one that might even provide with the extra flexibility you know and love from Drupal.
8+
9+
## Mutations to create Drupal Entities
10+
11+
One of the most common things you will want to do when with mutations is to create, update and delete entities. These CRUD operations on entities were made as simple as possible to implement and only require you to extend some generic classes provided by the graphql_core module.
12+
13+
So let's have a look at how you can create a mutation from scratch to generate a new entity of type node, and in this case a new article. You can refer to the [Examples](https://github.com/drupal-graphql/graphql-examples) repository to look at some other examples as well for how to create other kinds of mutations.
14+
15+
### CreateArticle Plugin
16+
17+
The first step to create a mutation is to make the plugin. The graphql module provides a base class for creating new entities called `CreateEntityBase`. You should implement a plugin that extends this class when you want to create an entity.
18+
19+
Let's look at what the code for this plugin looks like :
20+
21+
```
22+
<?php
23+
24+
namespace Drupal\graphql_examples\Plugin\GraphQL\Mutations;
25+
use Drupal\graphql\Annotation\GraphQLMutation;
26+
use Drupal\graphql\GraphQL\Execution\ResolveContext;
27+
use Drupal\graphql_core\Plugin\GraphQL\Mutations\Entity\CreateEntityBase;
28+
use GraphQL\Type\Definition\ResolveInfo;
29+
30+
/**
31+
* Simple mutation for creating a new article node.
32+
*
33+
* @GraphQLMutation(
34+
* id = "create_article",
35+
* entity_type = "node",
36+
* entity_bundle = "article",
37+
* secure = true,
38+
* name = "createArticle",
39+
* type = "EntityCrudOutput!",
40+
* arguments = {
41+
* "input" = "ArticleInput"
42+
* }
43+
* )
44+
*/
45+
class CreateArticle extends CreateEntityBase {
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
protected function extractEntityInput(
51+
$value,
52+
array $args,
53+
ResolveContext $context,
54+
ResolveInfo $info
55+
) {
56+
return [
57+
'title' => $args['input']['title'],
58+
'body' => $args['input']['body'],
59+
];
60+
}
61+
62+
}
63+
```
64+
65+
We can see a couple things in this code that are particularly interesting :
66+
67+
### Namespacing and folder structure
68+
69+
The first thing we need to do when implementing the plugin is to give it a namespace, in this case we can see we use `graphql_examples`, you should replace this by your own module name.
70+
71+
Make sure that this plugin lives inside `{{module_name}}//src/Plugin/GraphQL`
72+
73+
### GraphQLMutation annotations
74+
75+
The graphql module uses anotations for classes in order to have some information define the mutation in a simple way, things like :
76+
77+
- id - The id of the mutation.
78+
- entity_type - The type of entity that is going to be created from this mutation (only important for when extending CreateEntityBase mutations)
79+
- entity_bundle - The bundle of the entity that is going to be created
80+
- secure - Fields that are not marked secure are automatically blocked in untrusted environments. For example there is a field that allows to fetch content from a remote url, which would basically turn your website into a proxy for anybody. This field will only work with a certain user permission or in persisted queries, where we are in control of what they do. The other way around, a field that is marked as secure doesn't allow any operations drupal itself wouldn't.
81+
- name - The name for the mutation. This name is what you will use when calling the mutation.
82+
- type - the "type" is the returned type by the mutation. In the example above the mutation returns a "EntityCrudOutput" type which is provided by the graphql module itself.
83+
- arguments - The arguments passed to the mutation. These are the fields for the entity you want to create, in the case above we are passing one argument called "Input" of type "ArticleInput". We will look at InputTypes afterwards. But essentially since graphql is strictly typed we want to provide information for types for each field we pass to the mutation we can do that using "InputTypes".
84+
85+
### extractEntityInput method
86+
87+
There is one method you should always implement when doing mutations, that is the `extractEntityInput` method which will be sort of a mapping between the arguments you pass to the mutation and the fields that drupal expects to receive for the entity being created.
88+
89+
We can see we are assigning the `title` that we are passing in the input (we will look at the ArticleInput after) to the title property in the entity, same for the `body`.
90+
91+
## Mutations to update Drupal Entities
92+
93+
Let's continue with our article example. In this case we implement a mutation to update a given article. Because we are updating a particular entity and we need to know which entity it is, we will need to provide the plugin annotation with something extra, an Id for the entity.
94+
95+
```
96+
<?php
97+
98+
namespace Drupal\graphql_examples\Plugin\GraphQL\Mutations;
99+
use Drupal\graphql\Annotation\GraphQLMutation;
100+
use Drupal\graphql\GraphQL\Execution\ResolveContext;
101+
use Drupal\graphql_core\Plugin\GraphQL\Mutations\Entity\UpdateEntityBase;
102+
use GraphQL\Type\Definition\ResolveInfo;
103+
104+
/**
105+
* Simple mutation for updating an existing article node.
106+
*
107+
* @GraphQLMutation(
108+
* id = "update_article",
109+
* entity_type = "node",
110+
* entity_bundle = "article",
111+
* secure = true,
112+
* name = "updateArticle",
113+
* type = "EntityCrudOutput!",
114+
* arguments = {
115+
* "id" = "String",
116+
* "input" = "ArticleInput"
117+
* }
118+
* )
119+
*/
120+
class UpdateArticle extends UpdateEntityBase {
121+
122+
/**
123+
* {@inheritdoc}
124+
*/
125+
protected function extractEntityInput(
126+
$value,
127+
array $args,
128+
ResolveContext $context,
129+
ResolveInfo $info
130+
) {
131+
return array_filter([
132+
'title' => $args['input']['title'],
133+
'body' => $args['input']['body'],
134+
]);
135+
}
136+
137+
}
138+
```
139+
140+
The first thing we noticed is we are now using `UpdateEntityBase` instead of "CreateEntityBase" as our parent class,
141+
we can also see that we use the same argument "Input" as above but we also have another argument called `id`. The Graphql Module will be smart enough to use that id to match to the right entity.
142+
143+
## Mutations to Delete Drupal Entities
144+
145+
The only thing left now is really to delete the entity right? This is the simplest type of operation out of the 3, because we only need to give graphql the `id`, it will check if we can access that type of operation and if so delete the entity with the id we give to it. So let's look at how the plugin looks like
146+
147+
```
148+
<?php
149+
150+
namespace Drupal\graphql_examples\Plugin\GraphQL\Mutations;
151+
use Drupal\graphql_core\Plugin\GraphQL\Mutations\Entity\DeleteEntityBase;
152+
153+
/**
154+
* Simple mutation for deleting an article node.
155+
*
156+
* @GraphQLMutation(
157+
* id = "delete_article",
158+
* entity_type = "node",
159+
* entity_bundle = "article",
160+
* secure = true,
161+
* name = "deleteArticle",
162+
* type = "EntityCrudOutput!",
163+
* arguments = {
164+
* "id" = "String"
165+
* }
166+
* )
167+
*/
168+
class DeleteArticle extends DeleteEntityBase {
169+
}
170+
```
171+
172+
We extend the `DeleteEntityBase` class and only require one argument: the id of the entity we want to delete. Additionally we add the required annotations as we previously did for the other mutations.
173+
174+
## ArticleInput
175+
176+
We know from the examples above that we need to define the arguments for mutations. Similar to how a function, mutations receive arguments that can be used to do whatever the mutation needs to do to work. In order for graphql to know information about the arguments we create an `InputType`. The ArticleInput that we used above looks like this :
177+
178+
```
179+
<?php
180+
181+
namespace Drupal\graphql_examples\Plugin\GraphQL\InputTypes;
182+
use Drupal\graphql\Plugin\GraphQL\InputTypes\InputTypePluginBase;
183+
184+
/**
185+
* The input type for article mutations.
186+
*
187+
* @GraphQLInputType(
188+
* id = "article_input",
189+
* name = "ArticleInput",
190+
* fields = {
191+
* "title" = "String",
192+
* "body" = {
193+
* "type" = "String",
194+
* "nullable" = "TRUE"
195+
* }
196+
* }
197+
* )
198+
*/
199+
class ArticleInput extends InputTypePluginBase {
200+
}
201+
```
202+
203+
We can see again that we namespace this plugin to our module name, in this case `graphql_examples` should be replaced by your own module name. This file should live inside `{{module_name}}/src/Plugin/GraphQL/InputTypes/ArticleInput.php`
204+
205+
We can also see above that we only use annotations here to define the arguments inside the `fields` property. So we know that it receives a `title` and that's a "String" and we also receive a `body` which is also a `String`.

doc/mutations/custom-mutations.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# The MutationPluginBase plugin
2+
3+
Not all mutations are directly related to an entity, and often you might need to perform operations on a mutation that are not necessarily creating, updating or deleting an entity in Drupal. For these cases you can use the `MutationPluginBase` plugin and extend that instead of extending the `CreateEntityBase` as we saw on _Creating mutations for Entities_.
4+
5+
The mutation itself wouldn't be too different from what we did previously, you can see an example in the [Examples repo](https://github.com/drupal-graphql/graphql-examples/blob/master/src/Plugin/GraphQL/Mutations/FileUpload.php) of a file upload mutation.
6+
7+
## Resolve method
8+
9+
One important method of the MutationPluginBase is the resolve method where we, similar to our "extractEntityInput" above, get access to the arguments passed to the mutation and we can then perform the operation we want on Drupal.
10+
11+
Let's look at an example that will perform an operation of buying a car. The operation itself exists on a service so it's not really important to look at the details of that operation, but what is important is that in the resolve method we take the `car` from our arguments (defined in the annotation as seen above) and we call our `garage` service and pass it the car :
12+
13+
```
14+
<?php
15+
16+
namespace Drupal\graphql_plugin_test\Plugin\GraphQL\Mutations;
17+
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
18+
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
19+
use Drupal\graphql\GraphQL\Execution\ResolveContext;
20+
use Drupal\graphql\Plugin\GraphQL\Mutations\MutationPluginBase;
21+
use Drupal\graphql_plugin_test\GarageInterface;
22+
use Symfony\Component\DependencyInjection\ContainerInterface;
23+
use GraphQL\Type\Definition\ResolveInfo;
24+
25+
/**
26+
* A test mutation.
27+
*
28+
* @GraphQLMutation(
29+
* id = "buy_car",
30+
* secure = true,
31+
* name = "buyCar",
32+
* type = "Car",
33+
* arguments = {
34+
* "car" = "CarInput!"
35+
* }
36+
* )
37+
*/
38+
class BuyCar extends MutationPluginBase implements ContainerFactoryPluginInterface {
39+
40+
use DependencySerializationTrait;
41+
42+
/**
43+
* The garage.
44+
*
45+
* @var \Drupal\graphql_plugin_test\GarageInterface
46+
*/
47+
protected $garage;
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) {
53+
return new static($configuration, $pluginId, $pluginDefinition, $container->get('graphql_test.garage'));
54+
}
55+
56+
/**
57+
* BuyCar constructor.
58+
*
59+
* @param array $configuration
60+
* The plugin configuration array.
61+
* @param string $pluginId
62+
* The plugin id.
63+
* @param mixed $pluginDefinition
64+
* The plugin definition array.
65+
* @param \Drupal\graphql_plugin_test\GarageInterface $garage
66+
* The garage service.
67+
*/
68+
public function __construct(array $configuration, $pluginId, $pluginDefinition, GarageInterface $garage) {
69+
parent::__construct($configuration, $pluginId, $pluginDefinition);
70+
$this->garage = $garage;
71+
}
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function resolve($value, array $args, ResolveContext $context, ResolveInfo $info) {
76+
return $this->garage->insertVehicle($args['car']);
77+
}
78+
}
79+
```
80+
81+
This example was taken from the [a test](https://github.com/drupal-graphql/graphql/blob/188be525a007f385a3d3c4f8d2900b62a0150a5f/tests/modules/graphql_plugin_test/src/Plugin/GraphQL/Mutations/BuyCar.php) inside the graphql repository. Inside the resolve method it could be doing other things.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Providing custom validation for mutations
2+
3+
One aspect that is important to consider when creating mutations is providing good error messages and validations. Often you will be connecting these mutations to forms or other types of UI that should give the user clear indication of what went wrong. Access checks and permissions are also important to consider when creating mutations for your entities.
4+
5+
## CreateEntitybase plugin access
6+
7+
The `CreateEntityBase` plugin does a entity access check in it's resolveOutput method, so it will validate if a user is trying to create an entity it does not have access to and fail if that happens with a message : **"You do not have the necessary permissions to create entities of this type."**
8+
9+
However you might have some other logic you want to perform, for example check that a user has done something else before he can perform this action, some kind of custom validation or a simple field access check, so that maybe a user that has no access to a particular field give his role fails accordingly.
10+
11+
You can make custom validations by implementing your own `resolveOutput` method inside your mutation.
12+
13+
## Custom validations - errors and violations
14+
15+
Graphql mutations by default return 3 things :
16+
17+
- data - The data that was returned by the mutation. what the consumer of the mutation asked for when running it (if successful)
18+
- errors - If an error occurred in Drupal (an exception) it will be added to the errors array.
19+
- violations - Violations are a useful way to provide error messages to users, nothing "crashed" but something went wrong and the user can't do the operation. Maybe he has no access or something else.
20+
21+
### Adding custom information to errors
22+
23+
To add things to the errors for example when creating an entity you can return a new `EntityCrudOutputWrapper`, e.g. :
24+
25+
```
26+
if (!$entity->access('create')) {
27+
return new EntityCrudOutputWrapper(NULL, NULL, [
28+
$this->t('You do not have the necessary permissions to create entities of this type.'),
29+
]);
30+
}
31+
```
32+
33+
In this case if the user has no access to create on this entity its going to fail. You can make your own logic inside resolve or resolveOutput to output your own information and logic to users.
34+
35+
### Adding custom violations
36+
37+
To add violations the process is very similar, you need to return a new `EntityCrudOutputWrapper`, you can decide based on your own situation if the entity should or not be returned (or if even should or not be processed and created) but the second argument to this `EntityCrudOutputWrapper` where we passed NULL previously is a `Violations` array of type `ConstraintViolationList` from Symphony. Check the [Drupal information on ConstraintViolationList](https://api.drupal.org/api/drupal/vendor%21symfony%21validator%21ConstraintViolationList.php/8.2.x) as well as [ConstraintViolation](https://api.drupal.org/api/drupal/vendor%21symfony%21validator%21ConstraintViolation.php/class/ConstraintViolation/8.2.x)
38+
39+
There are a couple imporant pieces in ContrainstViolations you can use that are output by the graphql module to the user in the `violations` array :
40+
41+
- code - Can indicate the type of violation
42+
- message - a clear message for the user of what went wrong
43+
- path - The path (field or other part) where the violation occurred
44+
45+
See the following error for an example of a situation where a user tries updating an entity which he has access to but not a particular field :
46+
47+
```
48+
{
49+
"data": {
50+
"addCredit": {
51+
"entity": null,
52+
"violations": [
53+
{
54+
"code": "403",
55+
"message": "Access denied",
56+
"path": "field_credit_status"
57+
}
58+
],
59+
"errors": [
60+
"You do not have the necessary permissions to create some fields for this entity."
61+
]
62+
}
63+
}
64+
}
65+
```
66+
67+
In this case it was decided to fail creating the entity `Credit` because the user does not have access to fields he is trying to create, but instead of only providing the generic message : _"You do not have the necessary permissions to create some fields for this entity."_ some extra information is added specifying which exact fields failed and why.

0 commit comments

Comments
 (0)