1
<?php
2

3
namespace Nuwave\Lighthouse\Defer;
4

5
use Closure;
6
use GraphQL\Error\Error;
7
use GraphQL\Language\AST\NonNullTypeNode;
8
use GraphQL\Language\AST\TypeNode;
9
use GraphQL\Type\Definition\Directive;
10
use GraphQL\Type\Definition\ResolveInfo;
11
use Nuwave\Lighthouse\ClientDirectives\ClientDirective;
12
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
13
use Nuwave\Lighthouse\Schema\RootType;
14
use Nuwave\Lighthouse\Schema\Values\FieldValue;
15
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
16
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
17

18
class DeferrableDirective extends BaseDirective implements FieldMiddleware
19
{
20
    public const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD = 'The @defer directive cannot be used on a root mutation field.';
21
    public const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD = 'The @defer directive cannot be used on a Non-Nullable field.';
22
    public const DEFER_DIRECTIVE_NAME = 'defer';
23

24 0
    public static function definition(): string
25
    {
26
        return /** @lang GraphQL */ <<<'GRAPHQL'
27 0
"""
28
Do not use this directive directly, it is automatically added to the schema
29
when using the defer extension.
30
"""
31
directive @deferrable on FIELD_DEFINITION
32
GRAPHQL;
33
    }
34

35
    /**
36
     * @var \Nuwave\Lighthouse\Defer\Defer
37
     */
38
    protected $defer;
39

40 1
    public function __construct(Defer $defer)
41
    {
42 1
        $this->defer = $defer;
43
    }
44

45 1
    public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
46
    {
47 1
        $previousResolver = $fieldValue->getResolver();
48 1
        $fieldType = $fieldValue->getField()->type;
49

50 1
        $fieldValue->setResolver(
51
            function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($previousResolver, $fieldType) {
52
                $wrappedResolver = function () use ($previousResolver, $root, $args, $context, $resolveInfo) {
53 1
                    return $previousResolver($root, $args, $context, $resolveInfo);
54 1
                };
55 1
                $path = implode('.', $resolveInfo->path);
56

57 1
                if ($this->shouldDefer($fieldType, $resolveInfo)) {
58 1
                    return $this->defer->defer($wrappedResolver, $path);
59
                }
60

61 1
                return $this->defer->isStreaming()
62 1
                    ? $this->defer->findOrResolve($wrappedResolver, $path)
63 1
                    : $previousResolver($root, $args, $context, $resolveInfo);
64
            }
65
        );
66

67 1
        return $next($fieldValue);
68
    }
69

70
    /**
71
     * Determine if field should be deferred.
72
     *
73
     * @throws \GraphQL\Error\Error
74
     */
75 1
    protected function shouldDefer(TypeNode $fieldType, ResolveInfo $resolveInfo): bool
76
    {
77 1
        $defers = (new ClientDirective(self::DEFER_DIRECTIVE_NAME))->forField($resolveInfo);
78

79 1
        if ($this->anyFieldHasDefer($defers)) {
80 1
            if ($resolveInfo->parentType->name === RootType::MUTATION) {
81 1
                throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD);
82
            }
83 1
            if ($fieldType instanceof NonNullTypeNode) {
84 1
                throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD);
85
            }
86
        }
87

88
        // Following the semantics of Apollo:
89
        // All declarations of a field have to contain @defer for the field to be deferred
90 1
        foreach ($defers as $defer) {
91 1
            if ($defer === null || $defer === [Directive::IF_ARGUMENT_NAME => false]) {
92 1
                return false;
93
            }
94
        }
95

96 1
        $skips = (new ClientDirective(Directive::SKIP_NAME))->forField($resolveInfo);
97 1
        foreach ($skips as $skip) {
98 1
            if ($skip === [Directive::IF_ARGUMENT_NAME => true]) {
99 0
                return false;
100
            }
101
        }
102

103 1
        $includes = (new ClientDirective(Directive::INCLUDE_NAME))->forField($resolveInfo);
104

105 1
        return ! in_array(
106 1
            [Directive::IF_ARGUMENT_NAME => false],
107
            $includes,
108 1
            true
109
        );
110
    }
111

112
    /**
113
     * @param  array<array<string, mixed>|null>  $defers
114
     */
115 1
    protected function anyFieldHasDefer(array $defers): bool
116
    {
117 1
        foreach ($defers as $defer) {
118 1
            if ($defer !== null) {
119 1
                return true;
120
            }
121
        }
122

123 1
        return false;
124
    }
125
}

Read our documentation on viewing source code .

Loading