Skip to content

Commit d95afbf

Browse files
committed
Documentation about parameter middlewares
1 parent e0bae07 commit d95afbf

File tree

7 files changed

+178
-15
lines changed

7 files changed

+178
-15
lines changed

docs/argument_resolving.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
id: argument-resolving
3+
title: Extending argument resolving
4+
sidebar_label: Custom argument resolving
5+
---
6+
<small>Available in GraphQLite 4.0+</small>
7+
8+
Using a **parameter middleware**, you can hook into the argument resolution of field/query/mutation/factory.
9+
10+
<div class="alert alert-info">Use a parameter middleware if you want to alter the way arguments are injected in a method
11+
or if you want to alter the way input types are imported (for instance if you want to add a validation step)</div>
12+
13+
As an example, GraphQLite uses *parameter middlewares* internally to:
14+
15+
- Inject the Webonyx GraphQL resolution object when you type-hint on the `ResolveInfo` object. For instance:
16+
```php
17+
/**
18+
* @Query
19+
* @return Product[]
20+
*/
21+
public function products(ResolveInfo $info): array
22+
```
23+
In the query above, the `$info` argument is filled with the Webonyx `ResolveInfo` class thanks to the
24+
[`ResolveInfoParameterHandler parameter middleware`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ResolveInfoParameterHandler.php)
25+
- Inject a service from the container when you use the `@Autowire` annotation
26+
- Perform validation with the `@Validate` annotation (in Laravel package)
27+
28+
<!-- https://docs.google.com/drawings/d/10zHfWdbvEab6_dyQBcM68_I_bXQkZtO5ePqt4jdDlk8/edit?usp=sharing -->
29+
30+
**Parameter middlewares**
31+
32+
![](assets/parameter_middleware.svg)
33+
34+
Each middleware is passed number of objects describing the parameter:
35+
36+
- a PHP `ReflectionParameter` object representing the parameter being manipulated
37+
- a `phpDocumentor\Reflection\DocBlock` instance (useful to analyze the `@param` comment if any)
38+
- a `phpDocumentor\Reflection\Type` instance (useful to analyze the type if the argument)
39+
- a `TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations` instance. This is a collection of all custom annotations that apply to this specific argument (more on that later)
40+
- a `$next` handler to pass the argument resolving to the next middleware.
41+
42+
Parameter resolution is done in 2 passes.
43+
44+
On the first pass, middlewares are traversed. They must return a `TheCodingMachine\GraphQLite\Parameters\ParameterInterface` (an object that does the actual resolving).
45+
46+
```php
47+
interface ParameterMiddlewareInterface
48+
{
49+
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface;
50+
}
51+
```
52+
53+
Then, resolution actually happen by executing the resolver (this is the second pass).
54+
55+
## Annotations parsing
56+
57+
If you plan to use annotations while resolving arguments, your annotation should extend the [`ParameterAnnotationInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Annotations/ParameterAnnotationInterface.php)
58+
59+
For instance, if we want GraphQLite to inject a service in an argument, we can use `@Autowire(for="myService")`.
60+
61+
The annotation looks like this:
62+
63+
```php
64+
/**
65+
* Use this annotation to autowire a service from the container into a given parameter of a field/query/mutation.
66+
*
67+
* @Annotation
68+
*/
69+
class Autowire implements ParameterAnnotationInterface
70+
{
71+
/**
72+
* @var string
73+
*/
74+
public $for;
75+
76+
/**
77+
* The getTarget method must return the name of the argument
78+
*/
79+
public function getTarget(): string
80+
{
81+
return $this->for;
82+
}
83+
}
84+
```
85+
86+
## Writing the parameter middleware
87+
88+
The middleware purpose is to analyze a parameter and decide whether or not it can handle it.
89+
90+
**Parameter middleware class**
91+
```php
92+
class ContainerParameterHandler implements ParameterMiddlewareInterface
93+
{
94+
/** @var ContainerInterface */
95+
private $container;
96+
97+
public function __construct(ContainerInterface $container)
98+
{
99+
$this->container = $container;
100+
}
101+
102+
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
103+
{
104+
// The $parameterAnnotations object can be used to fetch any annotation implementing ParameterAnnotationInterface
105+
$autowire = $parameterAnnotations->getAnnotationByType(Autowire::class);
106+
107+
if ($autowire === null) {
108+
// If there are no annotation, this middleware cannot handle the parameter. Let's ask
109+
// the next middleware in the chain (using the $next object)
110+
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
111+
}
112+
113+
// We found a @Autowire annotation, let's return a parameter resolver.
114+
return new ContainerParameter($this->container, $parameter->getType());
115+
}
116+
}
117+
```
118+
119+
The last step is to write the actual parameter resolver.
120+
121+
**Parameter resolver class**
122+
```php
123+
/**
124+
* A parameter filled from the container.
125+
*/
126+
class ContainerParameter implements ParameterInterface
127+
{
128+
/** @var ContainerInterface */
129+
private $container;
130+
/** @var string */
131+
private $identifier;
132+
133+
public function __construct(ContainerInterface $container, string $identifier)
134+
{
135+
$this->container = $container;
136+
$this->identifier = $identifier;
137+
}
138+
139+
/**
140+
* The "resolver" returns the actual value that will be fed to the function.
141+
*/
142+
public function resolve(?object $source, array $args, $context, ResolveInfo $info)
143+
{
144+
return $this->container->get($this->identifier);
145+
}
146+
}
147+
```
148+
149+
## Registering a parameter middleware
150+
151+
The last step is to register the parameter middleware we just wrote:
152+
153+
You can register your own parameter middlewares using the `SchemaFactory::addParameterMiddleware()` method.
154+
155+
```php
156+
$schemaFactory->addParameterMiddleware(new ContainerParameterHandler($container));
157+
```
158+
159+
If you are using the Symfony bundle, you can tag the service as "graphql.parameter_middleware".

docs/assets/parameter_middleware.svg

Lines changed: 1 addition & 0 deletions
Loading

docs/field_middlewares.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ sidebar_label: Custom annotations
88
Just like the `@Logged` or `@Right` annotation, you can develop your own annotation that extends/modifies the behaviour
99
of a field/query/mutation.
1010

11+
<div class="alert alert-warning">If you want to create an annotation that targets a single argument (like <code>@AutoWire(for="$service")</code>),
12+
you should rather check the documentation about <a href="argument-resolving">custom argument resolving</a></div>
13+
1114
## Field middlewares
1215

1316
GraphQLite is based on the Webonyx/Graphql-PHP library. In Webonyx, fields are represented by the `FieldDefinition` class.

docs/internals.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ Imagine that class "B" extends class "A" and class "A" maps to GraphQL type "ATy
101101

102102
Since "B" *is a* "A", the "recursive type mapper" role is to make sure that "B" will also map to GraphQL type "AType".
103103

104-
## Parameter mappers
104+
## Parameter mapper middlewares
105105

106-
"Parameter mappers" are used to decide what argument should be injected into a parameter.
106+
"Parameter middlewares" are used to decide what argument should be injected into a parameter.
107107

108108
Let's have a look at a simple query:
109109

@@ -116,11 +116,11 @@ public function products(ResolveInfo $info): array
116116
```
117117

118118
As you may know, [the `ResolveInfo` object injected in this query comes from Webonyx/GraphQL-PHP library](query_plan.md).
119-
GraphQLite knows that is must inject a `ResolveInfo` instance because it comes with a `ResolveInfoParameterMapper` class
120-
that implements the [`ParameterMapperInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ParameterMapperInterface.php)).
119+
GraphQLite knows that is must inject a `ResolveInfo` instance because it comes with a [`ResolveInfoParameterHandler`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ResolveInfoParameterHandler.php) class
120+
that implements the [`ParameterMiddlewareInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ParameterMiddlewareInterface.php)).
121121

122-
You can register your own parameter mappers using the `SchemaFactory::addParameterMapper()` method, or by tagging the
123-
service as "graphql.parameter_mapper" if you are using the Symfony bundle.
122+
You can register your own parameter middlewares using the `SchemaFactory::addParameterMiddleware()` method, or by tagging the
123+
service as "graphql.parameter_middleware" if you are using the Symfony bundle.
124124

125-
<div class="alert alert-info">Use a parameter mapper if you want to inject an argument in a method and if this argument
126-
is not a GraphQL input type</div>
125+
<div class="alert alert-info">Use a parameter middleware if you want to inject an argument in a method and if this argument
126+
is not a GraphQL input type or if you want to alter the way input types are imported (for instance if you want to add a validation step)</div>

src/SchemaFactory.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class SchemaFactory
7070
/** @var TypeMapperFactoryInterface[] */
7171
private $typeMapperFactories = [];
7272
/** @var ParameterMiddlewareInterface[] */
73-
private $parameterMappers = [];
73+
private $parameterMiddlewares = [];
7474
/** @var Reader */
7575
private $doctrineAnnotationReader;
7676
/** @var AuthenticationServiceInterface|null */
@@ -169,11 +169,11 @@ public function addTypeMapperFactory(TypeMapperFactoryInterface $typeMapperFacto
169169
}
170170

171171
/**
172-
* Registers a parameter mapper.
172+
* Registers a parameter middleware.
173173
*/
174-
public function addParameterMapper(ParameterMiddlewareInterface $parameterMapper): self
174+
public function addParameterMiddleware(ParameterMiddlewareInterface $parameterMiddleware): self
175175
{
176-
$this->parameterMappers[] = $parameterMapper;
176+
$this->parameterMiddlewares[] = $parameterMiddleware;
177177

178178
return $this;
179179
}
@@ -323,7 +323,7 @@ public function createSchema(): Schema
323323
$argumentResolver = new ArgumentResolver();
324324

325325
$parameterMiddlewarePipe = new ParameterMiddlewarePipe();
326-
foreach ($this->parameterMappers as $parameterMapper) {
326+
foreach ($this->parameterMiddlewares as $parameterMapper) {
327327
$parameterMiddlewarePipe->pipe($parameterMapper);
328328
}
329329
$parameterMiddlewarePipe->pipe(new ResolveInfoParameterHandler());

tests/SchemaFactoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function testSetters(): void
6464
->addTypeMapper(new CompositeTypeMapper())
6565
->addTypeMapperFactory(new StaticClassListTypeMapperFactory([TestSelfType::class]))
6666
->addRootTypeMapper(new CompositeRootTypeMapper([]))
67-
->addParameterMapper(new ParameterMiddlewarePipe())
67+
->addParameterMiddleware(new ParameterMiddlewarePipe())
6868
->addQueryProviderFactory(new AggregateControllerQueryProviderFactory([], $container))
6969
->setSchemaConfig(new SchemaConfig())
7070
->setExpressionLanguage(new ExpressionLanguage(new Psr16Adapter(new ArrayCache())))

website/sidebars.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"Usage": ["queries", "mutations", "type_mapping", "autowiring", "extend_type", "external_type_declaration", "input-types", "inheritance-interfaces"],
66
"Security": ["authentication_authorization", "fine-grained-security", "implementing-security"],
77
"Performance": ["query-plan", "prefetch-method"],
8-
"Advanced": ["file-uploads", "pagination", "custom-types", "field-middlewares", "extend_input_type", "multiple_output_types", "symfony-bundle-advanced", "laravel-package-advanced", "internals", "troubleshooting", "migrating"],
8+
"Advanced": ["file-uploads", "pagination", "custom-types", "field-middlewares", "argument-resolving", "extend_input_type", "multiple_output_types", "symfony-bundle-advanced", "laravel-package-advanced", "internals", "troubleshooting", "migrating"],
99
"Reference": ["annotations_reference"]
1010
}
1111
}

0 commit comments

Comments
 (0)