Skip to content

Commit d5b71fb

Browse files
authored
Merge pull request #699 from rebing/resolver-middleware
Resolver middleware from #594 plus some necessary adjustments
2 parents 230ad46 + 0c62ae3 commit d5b71fb

16 files changed

+545
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ CHANGELOG
33

44
[Next release](https://github.com/rebing/graphql-laravel/compare/6.0.0...master)
55
--------------
6+
### Added
7+
- Support for resolver middleware [\#594 / stevelacey](https://github.com/rebing/graphql-laravel/pull/594)
68

79
2020-11-26, 6.0.0
810
-----------------

README.md

Lines changed: 162 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ To work this around:
111111
- [Adding validation to a mutation](#adding-validation-to-a-mutation)
112112
- [File uploads](#file-uploads)
113113
- [Resolve method](#resolve-method)
114+
- [Resolver middleware](#resolver-middleware)
115+
- [Defining middleware](#defining-middleware)
116+
- [Registering middleware](#registering-middleware)
117+
- [Terminable middleware](#terminable-middleware)
114118
- [Authorization](#authorization)
115119
- [Privacy](#privacy)
116120
- [Query variables](#query-variables)
@@ -648,14 +652,14 @@ Note: You can test your file upload implementation using [Altair](https://altair
648652
bodyFormData.set('operationName', null);
649653
bodyFormData.set('map', JSON.stringify({"file":["variables.file"]}));
650654
bodyFormData.append('file', this.file);
651-
655+
652656
// Post the request to GraphQL controller
653657
let res = await axios.post('/graphql', bodyFormData, {
654658
headers: {
655659
"Content-Type": "multipart/form-data"
656660
}
657661
});
658-
662+
659663
if (res.data.status.code == 200) {
660664
// On success file upload
661665
this.file = null;
@@ -765,6 +769,159 @@ class UsersQuery extends Query
765769
}
766770
```
767771

772+
### Resolver middleware
773+
774+
#### Defining middleware
775+
776+
To create a new middleware, use the `make:graphql:middleware` Artisan command
777+
778+
```sh
779+
php artisan make:graphql:middleware ResolvePage
780+
```
781+
782+
This command will place a new ResolvePage class within your app/GraphQL/Middleware directory.
783+
In this middleware, we will set the Paginator current page to the argument we accept via our `PaginationType`:
784+
785+
```php
786+
namespace App\GraphQL\Middleware;
787+
788+
use Closure;
789+
use GraphQL\Type\Definition\ResolveInfo;
790+
use Illuminate\Pagination\Paginator;
791+
use Rebing\GraphQL\Support\Middleware;
792+
793+
class ResolvePage extends Middleware
794+
{
795+
public function handle($root, $args, $context, ResolveInfo $info, Closure $next)
796+
{
797+
Paginator::currentPageResolver(function () use ($args) {
798+
return $args['pagination']['page'] ?? 1;
799+
});
800+
801+
return $next($root, $args, $context, $info);
802+
}
803+
}
804+
```
805+
806+
#### Registering middleware
807+
808+
If you would like to assign middleware to specific queries/mutations,
809+
list the middleware class in the `$middleware` property of your query class.
810+
811+
```php
812+
namespace App\GraphQL\Queries;
813+
814+
use App\GraphQL\Middleware;
815+
use Rebing\GraphQL\Support\Query;
816+
use Rebing\GraphQL\Support\Query;
817+
818+
class UsersQuery extends Query
819+
{
820+
protected $middleware = [
821+
Middleware\Logstash::class,
822+
Middleware\ResolvePage::class,
823+
];
824+
}
825+
```
826+
827+
If you want a middleware to run during every GraphQL query/mutation to your application,
828+
list the middleware class in the `$middleware` property of your base query class.
829+
830+
```php
831+
namespace App\GraphQL\Queries;
832+
833+
use App\GraphQL\Middleware;
834+
use Rebing\GraphQL\Support\Query as BaseQuery;
835+
836+
abstract class Query extends BaseQuery
837+
{
838+
protected $middleware = [
839+
Middleware\Logstash::class,
840+
Middleware\ResolvePage::class,
841+
];
842+
}
843+
```
844+
845+
Alternatively, you can override `getMiddleware` to supply your own logic:
846+
847+
```php
848+
protected function getMiddleware(): array
849+
{
850+
return array_merge([...], $this->middleware);
851+
}
852+
```
853+
854+
#### Terminable middleware
855+
856+
Sometimes a middleware may need to do some work after the response has been sent to the browser.
857+
If you define a terminate method on your middleware and your web server is using FastCGI,
858+
the terminate method will automatically be called after the response is sent to the browser:
859+
860+
```php
861+
namespace App\GraphQL\Middleware;
862+
863+
use Countable;
864+
use GraphQL\Language\Printer;
865+
use GraphQL\Type\Definition\ResolveInfo;
866+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
867+
use Illuminate\Pagination\AbstractPaginator;
868+
use Illuminate\Support\Arr;
869+
use Illuminate\Support\Facades\Log;
870+
use Illuminate\Support\Facades\Route;
871+
use Rebing\GraphQL\Support\Middleware;
872+
873+
class Logstash extends Middleware
874+
{
875+
public function terminate($root, $args, $context, ResolveInfo $info, $result): void
876+
{
877+
Log::channel('logstash')->info('', (
878+
collect([
879+
'query' => $info->fieldName,
880+
'operation' => $info->operation->name->value ?? null,
881+
'type' => $info->operation->operation,
882+
'fields' => array_keys(Arr::dot($info->getFieldSelection($depth = PHP_INT_MAX))),
883+
'schema' => Arr::first(Route::current()->parameters()) ?? config('graphql.default_schema'),
884+
'vars' => $this->formatVariableDefinitions($info->operation->variableDefinitions),
885+
])
886+
->when($result instanceof Countable, function ($metadata) use ($result) {
887+
return $metadata->put('count', $result->count());
888+
})
889+
->when($result instanceof AbstractPaginator, function ($metadata) use ($result) {
890+
return $metadata->put('per_page', $result->perPage());
891+
})
892+
->when($result instanceof LengthAwarePaginator, function ($metadata) use ($result) {
893+
return $metadata->put('total', $result->total());
894+
})
895+
->merge($this->formatArguments($args))
896+
->toArray()
897+
));
898+
}
899+
900+
private function formatArguments(array $args): array
901+
{
902+
return collect(Arr::sanitize($args))
903+
->mapWithKeys(function ($value, $key) {
904+
return ["\${$key}" => $value];
905+
})
906+
->toArray();
907+
}
908+
909+
private function formatVariableDefinitions(?iterable $variableDefinitions = []): array
910+
{
911+
return collect($variableDefinitions)
912+
->map(function ($def) {
913+
return Printer::doPrint($def);
914+
})
915+
->toArray();
916+
}
917+
}
918+
```
919+
920+
The terminate method receives both the resolver arguments and the query result.
921+
922+
Once you have defined a terminable middleware, you should add it to the list of
923+
middleware in your queries and mutations.
924+
768925
### Authorization
769926

770927
For authorization similar to Laravel's Request (or middleware) functionality, we can override the `authorize()` function in a Query or Mutation.
@@ -1338,7 +1495,9 @@ class UserType extends GraphQLType
13381495
### Pagination
13391496

13401497
Pagination will be used, if a query or mutation returns a `PaginationType`.
1341-
Note that you have to manually handle the limit and page values:
1498+
1499+
Note that unless you use [resolver middleware](#defining-middleware),
1500+
you will have to manually supply both the limit and page values:
13421501

13431502
```php
13441503
namespace App\GraphQL\Queries;

phpstan-baseline.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,11 @@ parameters:
19551955
count: 1
19561956
path: tests/Unit/Console/InterfaceMakeCommandTest.php
19571957

1958+
-
1959+
message: "#^Method Rebing\\\\GraphQL\\\\Tests\\\\Unit\\\\Console\\\\MiddlewareMakeCommandTest\\:\\:dataForMakeCommand\\(\\) return type has no value type specified in iterable type array\\.$#"
1960+
count: 1
1961+
path: tests/Unit/Console/MiddlewareMakeCommandTest.php
1962+
19581963
-
19591964
message: "#^Method Rebing\\\\GraphQL\\\\Tests\\\\Unit\\\\Console\\\\MutationMakeCommandTest\\:\\:dataForMakeCommand\\(\\) return type has no value type specified in iterable type array\\.$#"
19601965
count: 1

phpstan.neon.dist

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@ parameters:
1919
- '/Call to an undefined method PHPUnit\\Framework\\MockObject\\MockObject::getAttributes\(\)/'
2020
- '/Parameter #4 \$currentPage of class Illuminate\\Pagination\\LengthAwarePaginator constructor expects int\|null, float\|int given/'
2121
- '/Parameter #1 \$offset of method Illuminate\\Support\\Collection<mixed,mixed>::slice\(\) expects int, float\|int given/'
22+
# \Rebing\GraphQL\Support\Middleware
23+
- '/Method Rebing\\GraphQL\\Support\\Middleware::handle\(\) has no return typehint specified/'
24+
- '/Method Rebing\\GraphQL\\Support\\Middleware::handle\(\) has parameter \$args with no typehint specified/'
25+
- '/Method Rebing\\GraphQL\\Support\\Middleware::handle\(\) has parameter \$context with no typehint specified/'
26+
- '/Method Rebing\\GraphQL\\Support\\Middleware::handle\(\) has parameter \$root with no typehint specified/'
27+
- '/Method Rebing\\GraphQL\\Support\\Middleware::resolve\(\) has no return typehint specified/'
28+
- '/Method Rebing\\GraphQL\\Support\\Middleware::resolve\(\) has parameter \$arguments with no value type specified in iterable type array/'
29+
# \Rebing\GraphQL\Support\Objects\ExampleMiddleware
30+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::handle\(\) has no return typehint specified/'
31+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::handle\(\) has parameter \$args with no typehint specified/'
32+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::handle\(\) has parameter \$context with no typehint specified/'
33+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::handle\(\) has parameter \$root with no typehint specified/'
34+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::terminate\(\) has parameter \$args with no typehint specified/'
35+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::terminate\(\) has parameter \$context with no typehint specified/'
36+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::terminate\(\) has parameter \$result with no typehint specified/'
37+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExampleMiddleware::terminate\(\) has parameter \$root with no typehint specified/'
38+
# \Rebing\GraphQL\Support\Objects\ExamplesMiddlewareQuery
39+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExamplesMiddlewareQuery::\$attributes has no typehint specified/'
40+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExamplesMiddlewareQuery::resolve\(\) has no return typehint specified/'
41+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExamplesMiddlewareQuery::resolve\(\) has parameter \$args with no typehint specified/'
42+
- '/Rebing\\GraphQL\\Tests\\Support\\Objects\\ExamplesMiddlewareQuery::resolve\(\) has parameter \$root with no typehint specified/'
2243
# tests/Unit/GraphQLTest.php
2344
- '/Call to an undefined method GraphQL\\Type\\Definition\\Type::getFields\(\)/'
2445
- '/Call to an undefined method Mockery\\/'
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rebing\GraphQL\Console;
6+
7+
use Illuminate\Console\GeneratorCommand;
8+
9+
class MiddlewareMakeCommand extends GeneratorCommand
10+
{
11+
protected $signature = 'make:graphql:middleware {name}';
12+
protected $description = 'Create a new GraphQL middleware class';
13+
protected $type = 'Middleware';
14+
15+
protected function getStub()
16+
{
17+
return __DIR__.'/stubs/middleware.stub';
18+
}
19+
20+
protected function getDefaultNamespace($rootNamespace)
21+
{
22+
return $rootNamespace.'\GraphQL\Middleware';
23+
}
24+
}

src/Console/stubs/middleware.stub

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DummyNamespace;
6+
7+
use Closure;
8+
use GraphQL\Type\Definition\ResolveInfo;
9+
use Rebing\GraphQL\Support\Middleware;
10+
11+
class DummyClass extends Middleware
12+
{
13+
public function handle($root, $args, $context, ResolveInfo $info, Closure $next)
14+
{
15+
return $next($root, $args, $context, $info);
16+
}
17+
}

src/GraphQLServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Rebing\GraphQL\Console\FieldMakeCommand;
1515
use Rebing\GraphQL\Console\InputMakeCommand;
1616
use Rebing\GraphQL\Console\InterfaceMakeCommand;
17+
use Rebing\GraphQL\Console\MiddlewareMakeCommand;
1718
use Rebing\GraphQL\Console\MutationMakeCommand;
1819
use Rebing\GraphQL\Console\QueryMakeCommand;
1920
use Rebing\GraphQL\Console\ScalarMakeCommand;
@@ -163,6 +164,7 @@ public function registerConsole(): void
163164
$this->commands(InputMakeCommand::class);
164165
$this->commands(InterfaceMakeCommand::class);
165166
$this->commands(InterfaceMakeCommand::class);
167+
$this->commands(MiddlewareMakeCommand::class);
166168
$this->commands(MutationMakeCommand::class);
167169
$this->commands(QueryMakeCommand::class);
168170
$this->commands(ScalarMakeCommand::class);

src/Support/Field.php

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
use Closure;
88
use GraphQL\Type\Definition\ResolveInfo;
9-
use GraphQL\Type\Definition\Type as GraphqlType;
9+
use GraphQL\Type\Definition\Type as GraphQLType;
1010
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
11+
use Illuminate\Pipeline\Pipeline;
12+
use Illuminate\Support\Facades\App;
1113
use Illuminate\Support\Facades\Validator;
1214
use InvalidArgumentException;
1315
use Rebing\GraphQL\Error\AuthorizationError;
@@ -29,6 +31,9 @@ abstract class Field
2931

3032
protected $attributes = [];
3133

34+
/** @var string[] */
35+
protected $middleware = [];
36+
3237
/**
3338
* Override this in your queries or mutations
3439
* to provide custom authorization.
@@ -50,7 +55,7 @@ public function attributes(): array
5055
return [];
5156
}
5257

53-
abstract public function type(): GraphqlType;
58+
abstract public function type(): GraphQLType;
5459

5560
/**
5661
* @return array<string,array>
@@ -116,7 +121,48 @@ public function getValidator(array $args, array $rules): ValidatorContract
116121
return Validator::make($args, $rules, $messages);
117122
}
118123

124+
/**
125+
* @return array<string>
126+
*/
127+
protected function getMiddleware(): array
128+
{
129+
return $this->middleware;
130+
}
131+
119132
protected function getResolver(): ?Closure
133+
{
134+
$resolver = $this->originalResolver();
135+
136+
if (! $resolver) {
137+
return null;
138+
}
139+
140+
return function ($root, ...$arguments) use ($resolver) {
141+
$middleware = $this->getMiddleware();
142+
143+
return app()->make(Pipeline::class)
144+
->send(array_merge([$this], $arguments))
145+
->through($middleware)
146+
->via('resolve')
147+
->then(function ($arguments) use ($middleware, $resolver, $root) {
148+
$result = $resolver($root, ...array_slice($arguments, 1));
149+
150+
foreach ($middleware as $name) {
151+
$instance = app()->make($name);
152+
153+
if (method_exists($instance, 'terminate')) {
154+
app()->terminating(function () use ($arguments, $instance, $result) {
155+
$instance->terminate($this, ...array_slice($arguments, 1), ...[$result]);
156+
});
157+
}
158+
}
159+
160+
return $result;
161+
});
162+
};
163+
}
164+
165+
protected function originalResolver(): ?Closure
120166
{
121167
if (! method_exists($this, 'resolve')) {
122168
return null;

0 commit comments

Comments
 (0)