Skip to content

Commit 18fc02c

Browse files
authored
Merge pull request #139 from moufmouf/parameter_middlewares
Migrating ParameterHandler to a middleware system
2 parents 48c50e5 + d95afbf commit 18fc02c

27 files changed

+402
-174
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/FieldsBuilder.php

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface;
1818
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
1919
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
20-
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMapperInterface;
21-
use TheCodingMachine\GraphQLite\Mappers\Parameters\TypeMapper;
20+
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface;
21+
use TheCodingMachine\GraphQLite\Mappers\Parameters\TypeHandler;
2222
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
2323
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
2424
use TheCodingMachine\GraphQLite\Middlewares\FieldHandlerInterface;
@@ -49,9 +49,9 @@ class FieldsBuilder
4949
private $typeResolver;
5050
/** @var NamingStrategyInterface */
5151
private $namingStrategy;
52-
/** @var TypeMapper */
52+
/** @var TypeHandler */
5353
private $typeMapper;
54-
/** @var ParameterMapperInterface */
54+
/** @var ParameterMiddlewareInterface */
5555
private $parameterMapper;
5656
/** @var FieldMiddlewareInterface */
5757
private $fieldMiddleware;
@@ -64,15 +64,15 @@ public function __construct(
6464
CachedDocBlockFactory $cachedDocBlockFactory,
6565
NamingStrategyInterface $namingStrategy,
6666
RootTypeMapperInterface $rootTypeMapper,
67-
ParameterMapperInterface $parameterMapper,
67+
ParameterMiddlewareInterface $parameterMapper,
6868
FieldMiddlewareInterface $fieldMiddleware
6969
) {
7070
$this->annotationReader = $annotationReader;
7171
$this->recursiveTypeMapper = $typeMapper;
7272
$this->typeResolver = $typeResolver;
7373
$this->cachedDocBlockFactory = $cachedDocBlockFactory;
7474
$this->namingStrategy = $namingStrategy;
75-
$this->typeMapper = new TypeMapper($typeMapper, $argumentResolver, $rootTypeMapper, $typeResolver);
75+
$this->typeMapper = new TypeHandler($typeMapper, $argumentResolver, $rootTypeMapper, $typeResolver);
7676
$this->parameterMapper = $parameterMapper;
7777
$this->fieldMiddleware = $fieldMiddleware;
7878
}
@@ -480,11 +480,8 @@ private function mapParameters(array $refParameters, DocBlock $docBlock): array
480480
foreach ($refParameters as $parameter) {
481481
$parameterAnnotations = $this->annotationReader->getParameterAnnotations($parameter);
482482

483-
$parameterObj = $this->parameterMapper->mapParameter($parameter, $docBlock, $docBlockTypes[$parameter->getName()] ?? null, $parameterAnnotations);
483+
$parameterObj = $this->parameterMapper->mapParameter($parameter, $docBlock, $docBlockTypes[$parameter->getName()] ?? null, $parameterAnnotations, $this->typeMapper);
484484

485-
if ($parameterObj === null) {
486-
$parameterObj = $this->typeMapper->mapParameter($parameter, $docBlock, $docBlockTypes[$parameter->getName()] ?? null, $parameterAnnotations);
487-
}
488485
$args[$parameter->getName()] = $parameterObj;
489486
}
490487

src/InputTypeUtils.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use ReflectionClass;
1414
use ReflectionMethod;
1515
use RuntimeException;
16-
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameter;
16+
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface;
1717
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
1818
use Webmozart\Assert\Assert;
1919
use function array_filter;
@@ -101,10 +101,10 @@ private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
101101
public static function getInputTypeArgs(array $args): array
102102
{
103103
$inputTypeArgs = array_filter($args, static function (ParameterInterface $parameter) {
104-
return $parameter instanceof InputTypeParameter;
104+
return $parameter instanceof InputTypeParameterInterface;
105105
});
106106

107-
return array_map(static function (InputTypeParameter $parameter) {
107+
return array_map(static function (InputTypeParameterInterface $parameter) {
108108
$desc = [
109109
'type' => $parameter->getType(),
110110
];

src/Mappers/Parameters/CompositeParameterMapper.php

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/Mappers/Parameters/ContainerParameterMapper.php renamed to src/Mappers/Parameters/ContainerParameterHandler.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
/**
1717
* Maps parameters with the \@Autowire annotation to container entry based on the FQCN or the passed identifier.
1818
*/
19-
class ContainerParameterMapper implements ParameterMapperInterface
19+
class ContainerParameterHandler implements ParameterMiddlewareInterface
2020
{
2121
/** @var ContainerInterface */
2222
private $container;
@@ -26,15 +26,15 @@ public function __construct(ContainerInterface $container)
2626
$this->container = $container;
2727
}
2828

29-
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations): ?ParameterInterface
29+
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
3030
{
3131
/**
3232
* @var Autowire|null $autowire
3333
*/
3434
$autowire = $parameterAnnotations->getAnnotationByType(Autowire::class);
3535

3636
if ($autowire === null) {
37-
return null;
37+
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
3838
}
3939

4040
$id = $autowire->getIdentifier();

src/Mappers/Parameters/Next.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Mappers\Parameters;
6+
7+
use phpDocumentor\Reflection\DocBlock;
8+
use phpDocumentor\Reflection\Type;
9+
use ReflectionParameter;
10+
use SplQueue;
11+
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
12+
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
13+
14+
/**
15+
* Iterate a queue of middlewares and execute them.
16+
*/
17+
final class Next implements ParameterHandlerInterface
18+
{
19+
/** @var ParameterHandlerInterface */
20+
private $fallbackHandler;
21+
22+
/** @var SplQueue */
23+
private $queue;
24+
25+
/**
26+
* Clones the queue provided to allow re-use.
27+
*
28+
* @param ParameterHandlerInterface $fallbackHandler Fallback handler to
29+
* invoke when the queue is exhausted.
30+
*/
31+
public function __construct(SplQueue $queue, ParameterHandlerInterface $fallbackHandler)
32+
{
33+
$this->queue = clone $queue;
34+
$this->fallbackHandler = $fallbackHandler;
35+
}
36+
37+
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations): ParameterInterface
38+
{
39+
if ($this->queue->isEmpty()) {
40+
return $this->fallbackHandler->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
41+
}
42+
43+
/**
44+
* @var ParameterMiddlewareInterface $middleware
45+
*/
46+
$middleware = $this->queue->dequeue();
47+
48+
return $middleware->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations, $this);
49+
}
50+
}

0 commit comments

Comments
 (0)