GraphQL-aware middleware solve a variety of problems for me, a while ago I needed to be able to Logstash various stats about the queries going through my API (e.g. what args were passed, what fields were requested), and more recently I wanted to move authorization ahead of validation after I learnt about https://github.com/rebing/graphql-laravel/issues/481.
Turns out pipelines can do a lot of the heavy lifting here, this is what I'm running with currently, it'd be interesting to wrap this up into native support.
Middleware resolver:
<?php
namespace App\GraphQL\Traits;
use Closure;
use App\GraphQL\Middleware;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Facades\App;
trait HasMiddleware
{
protected $middleware = [
Middleware\Authenticate::class,
Middleware\Logstash::class,
Middleware\Paginator::class,
];
protected function getResolver() : ?Closure
{
$resolver = parent::getResolver();
if (!$resolver) {
return null;
}
return function ($root, ...$arguments) use ($resolver) {
return app(Pipeline::class)
->send(array_merge([$this], $arguments))
->through($this->middleware)
->via('resolve')
->then(function ($arguments) use ($resolver, $root) {
$result = $resolver($root, ...array_slice($arguments, 1));
App::terminating(function () use ($arguments, $result) {
foreach ($this->middleware as $name) {
$instance = App::make($name);
if (method_exists($instance, 'terminate')) {
$instance->terminate($this, ...array_slice($arguments, 1), ...[$result]);
}
}
});
return $result;
});
};
}
}
Middleware base class:
<?php
namespace App\GraphQL\Middleware;
use Closure;
abstract class Middleware
{
public function resolve(array $arguments, Closure $next)
{
return $this->handle(...$arguments, ...[
function (...$arguments) use ($next) {
return $next($arguments);
}
]);
}
}
Example middleware:
<?php
namespace App\GraphQL\Middleware;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate;
class Paginator extends Middleware
{
public function handle($root, $args, $context, ResolveInfo $info, Closure $next)
{
Illuminate\Pagination\Paginator::currentPageResolver(function () use ($args) {
return $args['pagination']['page'] ?? 1;
});
return $next($root, $args, $context, $info);
}
// public function terminate($root, $args, $context, ResolveInfo $info, $result) {
// // optional
// }
}
I'm open / interested in this!
Would be interesting what other contributors think /cc @crissi
@mfn alright great, I've updated my example above, I worked out how to get rid of the awkward array $arguments problem via a base middleware class and wrapping the $next closure.
Also I refined the $root usage a bit, from within middleware $root is the query/mutation instance, and then the original root (probably null?) is restored in the then callback... is this an acceptable usage of $root?
Access to the query/mutation instance via $root is useful for defining your own DSLs.
Here's an example of a middleware adding support for a query-level authenticate method that is mostly the same as authorize, but gets called before validation:
<?php
namespace App\GraphQL\Middleware;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Support\Facades\Auth;
use Rebing\GraphQL\Error\AuthorizationError;
/**
* Authenticate is variation on authorize, but it happens before validation
*
* https://github.com/rebing/graphql-laravel/issues/481
*/
class Authenticate extends Middleware
{
public function handle($root, $args, $context, ResolveInfo $info, Closure $next)
{
if (!$this->authenticate($root, $args, $context, $info)) {
throw new AuthorizationError('Unauthorized');
}
return $next($root, $args, $context, $info);
}
private function authenticate($root, $args, $context, ResolveInfo $info) : bool
{
if (method_exists($root, 'authenticate')) {
return $root->authenticate($root, $args, $context, $info);
}
return Auth::check();
}
}
@mfn any further thoughts about the above? I'll wrap this up into a PR if we're satisfied with the implementation. The only question I have is whether it should be on Field or a trait, the former is fine I guess? The $middleware array would be empty by default.
I like it, I am also using a middleware for logging mutations, I am using a terminable middleware, so could be cool to make it easy to use that as well https://laravel.com/docs/5.0/middleware#terminable-middleware
@crissi have updated my example to include support for terminate 馃
Great Idea. Is it work in current version?
PR is finally merged, apologies for this super long delay. 2020 aint easy on anyone.
For anyone interested => you can test this via https://github.com/rebing/graphql-laravel/releases/tag/6.1.0-rc1 , will try to wait for some feedback before making a non-pre release.
Most helpful comment
@crissi have updated my example to include support for
terminate馃