1
<?php
2

3
namespace Nuwave\Lighthouse\Defer;
4

5
use Closure;
6
use GraphQL\Language\Parser;
7
use GraphQL\Server\Helper;
8
use Illuminate\Contracts\Container\Container;
9
use Illuminate\Http\Request;
10
use Illuminate\Support\Arr;
11
use Laragraph\Utils\RequestParser;
12
use Nuwave\Lighthouse\Events\ManipulateAST;
13
use Nuwave\Lighthouse\GraphQL;
14
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
15
use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse;
16
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
17
use Symfony\Component\HttpFoundation\Response;
18

19
class Defer implements CreatesResponse
20
{
21
    /**
22
     * @var \Nuwave\Lighthouse\Support\Contracts\CanStreamResponse
23
     */
24
    protected $stream;
25

26
    /**
27
     * @var \Nuwave\Lighthouse\GraphQL
28
     */
29
    protected $graphQL;
30

31
    /**
32
     * @var \Illuminate\Contracts\Container\Container
33
     */
34
    protected $container;
35

36
    /**
37
     * @var array<mixed>
38
     */
39
    protected $result = [];
40

41
    /**
42
     * @var array<mixed>
43
     */
44
    protected $deferred = [];
45

46
    /**
47
     * @var array<int, mixed>
48
     */
49
    protected $resolved = [];
50

51
    /**
52
     * @var bool
53
     */
54
    protected $acceptFurtherDeferring = true;
55

56
    /**
57
     * @var bool
58
     */
59
    protected $isStreaming = false;
60

61
    /**
62
     * @var float|int
63
     */
64
    protected $maxExecutionTime = 0;
65

66
    /**
67
     * @var int
68
     */
69
    protected $maxNestedFields = 0;
70

71 1
    public function __construct(CanStreamResponse $stream, GraphQL $graphQL, Container $container)
72
    {
73 1
        $this->stream = $stream;
74 1
        $this->graphQL = $graphQL;
75 1
        $this->container = $container;
76 1
        $this->maxNestedFields = config('lighthouse.defer.max_nested_fields', 0);
77
    }
78

79
    /**
80
     * Set the tracing directive on all fields of the query to enable tracing them.
81
     */
82 1
    public function handleManipulateAST(ManipulateAST $manipulateAST): void
83
    {
84 1
        ASTHelper::attachDirectiveToObjectTypeFields(
85 1
            $manipulateAST->documentAST,
86 1
            Parser::constDirective(/** @lang GraphQL */ '@deferrable')
87
        );
88

89 1
        $manipulateAST->documentAST->setDirectiveDefinition(
90 1
            Parser::directiveDefinition(/** @lang GraphQL */ '
91
"""
92
Use this directive on expensive or slow fields to resolve them asynchronously.
93
Must not be placed upon:
94
- Non-Nullable fields
95
- Mutation root fields
96
"""
97
directive @defer(if: Boolean = true) on FIELD
98
')
99
        );
100
    }
101

102 1
    public function isStreaming(): bool
103
    {
104 1
        return $this->isStreaming;
105
    }
106

107
    /**
108
     * Register deferred field.
109
     *
110
     * @return mixed The data if it is already available.
111
     */
112 1
    public function defer(Closure $resolver, string $path)
113
    {
114 1
        if ($data = Arr::get($this->result, "data.{$path}")) {
115 1
            return $data;
116
        }
117

118 1
        if ($this->isDeferred($path) || ! $this->acceptFurtherDeferring) {
119 1
            return $this->resolve($resolver, $path);
120
        }
121

122 1
        $this->deferred[$path] = $resolver;
123

124 1
        return null;
125
    }
126

127
    /**
128
     * @return mixed The loaded data.
129
     */
130 1
    public function findOrResolve(Closure $originalResolver, string $path)
131
    {
132 1
        if (! $this->hasData($path)) {
133 1
            if (isset($this->deferred[$path])) {
134 0
                unset($this->deferred[$path]);
135
            }
136

137 1
            return $this->resolve($originalResolver, $path);
138
        }
139

140 1
        return Arr::get($this->result, "data.{$path}");
141
    }
142

143
    /**
144
     * Resolve field with data or resolver.
145
     *
146
     * @return mixed The result of calling the resolver.
147
     */
148 1
    public function resolve(Closure $originalResolver, string $path)
149
    {
150 1
        $isDeferred = $this->isDeferred($path);
151 1
        $resolver = $isDeferred
152 1
            ? $this->deferred[$path]
153 1
            : $originalResolver;
154

155 1
        if ($isDeferred) {
156 1
            $this->resolved[] = $path;
157

158 1
            unset($this->deferred[$path]);
159
        }
160

161 1
        return $resolver();
162
    }
163

164 1
    public function isDeferred(string $path): bool
165
    {
166 1
        return isset($this->deferred[$path]);
167
    }
168

169 1
    public function hasData(string $path): bool
170
    {
171 1
        return Arr::has($this->result, "data.{$path}");
172
    }
173

174
    /**
175
     * Return either a final response or a stream of responses.
176
     *
177
     * @param  array<mixed>  $result
178
     * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\StreamedResponse
179
     */
180 1
    public function createResponse(array $result): Response
181
    {
182 1
        if (empty($this->deferred)) {
183 1
            return response($result);
184
        }
185

186 1
        return response()->stream(
187
            function () use ($result): void {
188 1
                $nested = 1;
189 1
                $this->result = $result;
190 1
                $this->isStreaming = true;
191 1
                $this->stream->stream($result, [], empty($this->deferred));
192

193 1
                if ($executionTime = config('lighthouse.defer.max_execution_ms', 0)) {
194 0
                    $this->maxExecutionTime = microtime(true) + $executionTime * 1000;
195
                }
196

197
                while (
198 1
                    count($this->deferred)
199 1
                    && ! $this->executionTimeExpired()
200 1
                    && ! $this->maxNestedFieldsResolved($nested)
201
                ) {
202 1
                    $nested++;
203 1
                    $this->executeDeferred();
204
                }
205

206
                // We've hit the max execution time or max nested levels of deferred fields.
207
                // We process remaining deferred fields, but are no longer allowing additional
208
                // fields to be deferred.
209 1
                if (count($this->deferred) > 0) {
210 1
                    $this->acceptFurtherDeferring = false;
211 1
                    $this->executeDeferred();
212
                }
213 1
            },
214 1
            200,
215
            [
216 1
                'X-Accel-Buffering' => 'no',
217
                'Content-Type' => 'multipart/mixed; boundary="-"',
218
            ]
219
        );
220
    }
221

222 1
    public function setMaxExecutionTime(float $time): void
223
    {
224 1
        $this->maxExecutionTime = $time;
225
    }
226

227
    /**
228
     * Override max nested fields.
229
     */
230 1
    public function setMaxNestedFields(int $max): void
231
    {
232 1
        $this->maxNestedFields = $max;
233
    }
234

235
    /**
236
     * Check if the maximum execution time has expired.
237
     */
238 1
    protected function executionTimeExpired(): bool
239
    {
240 1
        if ($this->maxExecutionTime === 0) {
241 1
            return false;
242
        }
243

244 1
        return $this->maxExecutionTime <= microtime(true);
245
    }
246

247
    /**
248
     * Check if the maximum number of nested field has been resolved.
249
     */
250 1
    protected function maxNestedFieldsResolved(int $nested): bool
251
    {
252 1
        if ($this->maxNestedFields === 0) {
253 1
            return false;
254
        }
255

256 1
        return $nested >= $this->maxNestedFields;
257
    }
258

259
    /**
260
     * Execute deferred fields.
261
     */
262 1
    protected function executeDeferred(): void
263
    {
264 1
        $this->result = $this->container->call(
265
            function (Request $request, RequestParser $requestParser, Helper $graphQLHelper) {
266 1
                return $this->graphQL->executeRequest($request, $requestParser, $graphQLHelper);
267
            }
268
        );
269

270 1
        $this->stream->stream(
271 1
            $this->result,
272 1
            $this->resolved,
273 1
            empty($this->deferred)
274
        );
275

276 1
        $this->resolved = [];
277
    }
278
}

Read our documentation on viewing source code .

Loading