CoroutineExecutor::doExecute()   B
last analyzed

Complexity

Conditions 10
Paths 12

Size

Total Lines 62
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 36
c 1
b 0
f 0
dl 0
loc 62
rs 7.6666
ccs 36
cts 36
cp 1
cc 10
nc 12
nop 0
crap 10

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Experimental\Executor;
6
7
use Generator;
8
use GraphQL\Error\Error;
9
use GraphQL\Error\InvariantViolation;
10
use GraphQL\Error\Warning;
11
use GraphQL\Executor\ExecutionResult;
12
use GraphQL\Executor\ExecutorImplementation;
13
use GraphQL\Executor\Promise\Promise;
14
use GraphQL\Executor\Promise\PromiseAdapter;
15
use GraphQL\Executor\Values;
16
use GraphQL\Language\AST\DocumentNode;
17
use GraphQL\Language\AST\SelectionSetNode;
18
use GraphQL\Language\AST\ValueNode;
19
use GraphQL\Type\Definition\AbstractType;
20
use GraphQL\Type\Definition\CompositeType;
21
use GraphQL\Type\Definition\EnumType;
22
use GraphQL\Type\Definition\InputObjectType;
23
use GraphQL\Type\Definition\InputType;
24
use GraphQL\Type\Definition\InterfaceType;
25
use GraphQL\Type\Definition\LeafType;
26
use GraphQL\Type\Definition\ListOfType;
27
use GraphQL\Type\Definition\NonNull;
28
use GraphQL\Type\Definition\ObjectType;
29
use GraphQL\Type\Definition\ResolveInfo;
30
use GraphQL\Type\Definition\ScalarType;
31
use GraphQL\Type\Definition\Type;
32
use GraphQL\Type\Definition\UnionType;
33
use GraphQL\Type\Introspection;
34
use GraphQL\Type\Schema;
35
use GraphQL\Utils\AST;
36
use GraphQL\Utils\Utils;
37
use SplQueue;
38
use stdClass;
39
use Throwable;
40
use function is_array;
41
use function is_string;
42
use function sprintf;
43
44
class CoroutineExecutor implements Runtime, ExecutorImplementation
45
{
46
    /** @var object */
47
    private static $undefined;
48
49
    /** @var Schema */
50
    private $schema;
51
52
    /** @var callable */
53
    private $fieldResolver;
54
55
    /** @var PromiseAdapter */
56
    private $promiseAdapter;
57
58
    /** @var mixed|null */
59
    private $rootValue;
60
61
    /** @var mixed|null */
62
    private $contextValue;
63
64
    /** @var mixed|null */
65
    private $rawVariableValues;
66
67
    /** @var mixed|null */
68
    private $variableValues;
69
70
    /** @var DocumentNode */
71
    private $documentNode;
72
73
    /** @var string|null */
74
    private $operationName;
75
76
    /** @var Collector|null */
77
    private $collector;
78
79
    /** @var array<Error> */
80
    private $errors;
81
82
    /** @var SplQueue */
83
    private $queue;
84
85
    /** @var SplQueue */
86
    private $schedule;
87
88
    /** @var stdClass|null */
89
    private $rootResult;
90
91
    /** @var int|null */
92
    private $pending;
93
94
    /** @var callable */
95
    private $doResolve;
96
97 231
    public function __construct(
98
        PromiseAdapter $promiseAdapter,
99
        Schema $schema,
100
        DocumentNode $documentNode,
101
        $rootValue,
102
        $contextValue,
103
        $rawVariableValues,
104
        ?string $operationName,
105
        callable $fieldResolver
106
    ) {
107 231
        if (self::$undefined === null) {
108 1
            self::$undefined = Utils::undefined();
109
        }
110
111 231
        $this->errors            = [];
112 231
        $this->queue             = new SplQueue();
113 231
        $this->schedule          = new SplQueue();
114 231
        $this->schema            = $schema;
115 231
        $this->fieldResolver     = $fieldResolver;
116 231
        $this->promiseAdapter    = $promiseAdapter;
117 231
        $this->rootValue         = $rootValue;
118 231
        $this->contextValue      = $contextValue;
119 231
        $this->rawVariableValues = $rawVariableValues;
120 231
        $this->documentNode      = $documentNode;
121 231
        $this->operationName     = $operationName;
122 231
    }
123
124 231
    public static function create(
125
        PromiseAdapter $promiseAdapter,
126
        Schema $schema,
127
        DocumentNode $documentNode,
128
        $rootValue,
129
        $contextValue,
130
        $variableValues,
131
        ?string $operationName,
132
        callable $fieldResolver
133
    ) {
134 231
        return new static(
135 231
            $promiseAdapter,
136 231
            $schema,
137 231
            $documentNode,
138 231
            $rootValue,
139 231
            $contextValue,
140 231
            $variableValues,
141 231
            $operationName,
142 231
            $fieldResolver
143
        );
144
    }
145
146 212
    private static function resultToArray($value, $emptyObjectAsStdClass = true)
147
    {
148 212
        if ($value instanceof stdClass) {
149 212
            $array = (array) $value;
150 212
            foreach ($array as $propertyName => $propertyValue) {
151 208
                $array[$propertyName] = self::resultToArray($propertyValue);
152
            }
153
154 212
            if ($emptyObjectAsStdClass && empty($array)) {
155 1
                return new stdClass();
156
            }
157
158 212
            return $array;
159
        }
160
161 208
        if (is_array($value)) {
162 61
            $array = [];
163 61
            foreach ($value as $key => $item) {
164 60
                $array[$key] = self::resultToArray($item);
165
            }
166
167 61
            return $array;
168
        }
169
170 207
        return $value;
171
    }
172
173 231
    public function doExecute() : Promise
174
    {
175 231
        $this->rootResult = new stdClass();
176 231
        $this->errors     = [];
177 231
        $this->queue      = new SplQueue();
178 231
        $this->schedule   = new SplQueue();
179 231
        $this->pending    = 0;
180
181 231
        $this->collector = new Collector($this->schema, $this);
182 231
        $this->collector->initialize($this->documentNode, $this->operationName);
183
184 231
        if (! empty($this->errors)) {
185 4
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors));
186
        }
187
188 227
        [$errors, $coercedVariableValues] = Values::getVariableValues(
189 227
            $this->schema,
190 227
            $this->collector->operation->variableDefinitions ?: [],
191 227
            $this->rawVariableValues ?: []
192
        );
193
194 227
        if (! empty($errors)) {
195 12
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors));
196
        }
197
198 216
        $this->variableValues = $coercedVariableValues;
199
200 216
        foreach ($this->collector->collectFields($this->collector->rootType, $this->collector->operation->selectionSet) as $shared) {
0 ignored issues
show
Bug introduced by
It seems like $this->collector->rootType can also be of type null; however, parameter $runtimeType of GraphQL\Experimental\Exe...lector::collectFields() does only seem to accept GraphQL\Type\Definition\ObjectType, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

200
        foreach ($this->collector->collectFields(/** @scrutinizer ignore-type */ $this->collector->rootType, $this->collector->operation->selectionSet) as $shared) {
Loading history...
201
            /** @var CoroutineContextShared $shared */
202
203
            // !!! assign to keep object keys sorted
204 212
            $this->rootResult->{$shared->resultName} = null;
205
206 212
            $ctx = new CoroutineContext(
207 212
                $shared,
208 212
                $this->collector->rootType,
0 ignored issues
show
Bug introduced by
It seems like $this->collector->rootType can also be of type null; however, parameter $type of GraphQL\Experimental\Exe...eContext::__construct() does only seem to accept GraphQL\Type\Definition\ObjectType, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

208
                /** @scrutinizer ignore-type */ $this->collector->rootType,
Loading history...
209 212
                $this->rootValue,
210 212
                $this->rootResult,
211 212
                [$shared->resultName]
212
            );
213
214 212
            $fieldDefinition = $this->findFieldDefinition($ctx);
215 212
            if (! $fieldDefinition->getType() instanceof NonNull) {
216 199
                $ctx->nullFence = [$shared->resultName];
217
            }
218
219 212
            if ($this->collector->operation->operation === 'mutation' && ! $this->queue->isEmpty()) {
220 2
                $this->schedule->enqueue($ctx);
221
            } else {
222 212
                $this->queue->enqueue(new Strand($this->spawn($ctx)));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->spawn($ctx) targeting GraphQL\Experimental\Exe...outineExecutor::spawn() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
223
            }
224
        }
225
226 216
        $this->run();
227
228 216
        if ($this->pending > 0) {
229
            return $this->promiseAdapter->create(function (callable $resolve) {
230 41
                $this->doResolve = $resolve;
231 41
            });
232
        }
233
234 176
        return $this->promiseAdapter->createFulfilled($this->finishExecute($this->rootResult, $this->errors));
235
    }
236
237
    /**
238
     * @param object|null $value
239
     * @param Error[]     $errors
240
     */
241 231
    private function finishExecute($value, array $errors) : ExecutionResult
242
    {
243 231
        $this->rootResult     = null;
244 231
        $this->errors         = [];
245 231
        $this->queue          = new SplQueue();
246 231
        $this->schedule       = new SplQueue();
247 231
        $this->pending        = null;
248 231
        $this->collector      = null;
249 231
        $this->variableValues = null;
250
251 231
        if ($value !== null) {
252 212
            $value = self::resultToArray($value, false);
253
        }
254
255 231
        return new ExecutionResult($value, $errors);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type object; however, parameter $data of GraphQL\Executor\ExecutionResult::__construct() does only seem to accept array<mixed,mixed>, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

255
        return new ExecutionResult(/** @scrutinizer ignore-type */ $value, $errors);
Loading history...
256
    }
257
258
    /**
259
     * @internal
260
     *
261
     * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type
262
     */
263 5
    public function evaluate(ValueNode $valueNode, InputType $type)
264
    {
265 5
        return AST::valueFromAST($valueNode, $type, $this->variableValues);
266
    }
267
268
    /**
269
     * @internal
270
     */
271 61
    public function addError($error)
272
    {
273 61
        $this->errors[] = $error;
274 61
    }
275
276 216
    private function run()
277
    {
278
        RUN:
279 216
        while (! $this->queue->isEmpty()) {
280
            /** @var Strand $strand */
281 212
            $strand = $this->queue->dequeue();
282
283
            try {
284 212
                if ($strand->success !== null) {
285
                    RESUME:
286
287 115
                    if ($strand->success) {
288 115
                        $strand->current->send($strand->value);
289
                    } else {
290 19
                        $strand->current->throw($strand->value);
0 ignored issues
show
Bug introduced by
The method throw() does not exist on Generator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

290
                        $strand->current->/** @scrutinizer ignore-call */ 
291
                                          throw($strand->value);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
291
                    }
292
293 115
                    $strand->success = null;
294 115
                    $strand->value   = null;
295
                }
296
297
                START:
298 212
                if ($strand->current->valid()) {
299 115
                    $value = $strand->current->current();
300
301 115
                    if ($value instanceof Generator) {
302 115
                        $strand->stack[$strand->depth++] = $strand->current;
303 115
                        $strand->current                 = $value;
304 115
                        goto START;
305 68
                    } elseif ($this->isPromise($value)) {
306
                        // !!! increment pending before calling ->then() as it may invoke the callback right away
307 41
                        ++$this->pending;
308
309 41
                        if (! $value instanceof Promise) {
310 40
                            $value = $this->promiseAdapter->convertThenable($value);
311
                        }
312
313 41
                        $this->promiseAdapter
314 41
                            ->then(
315 41
                                $value,
316
                                function ($value) use ($strand) {
317 37
                                    $strand->success = true;
318 37
                                    $strand->value   = $value;
319 37
                                    $this->queue->enqueue($strand);
320 37
                                    $this->done();
321 41
                                },
322
                                function (Throwable $throwable) use ($strand) {
323 18
                                    $strand->success = false;
324 18
                                    $strand->value   = $throwable;
325 18
                                    $this->queue->enqueue($strand);
326 18
                                    $this->done();
327 41
                                }
328
                            );
329 41
                        continue;
330
                    } else {
331 29
                        $strand->success = true;
332 29
                        $strand->value   = $value;
333 29
                        goto RESUME;
334
                    }
335
                }
336
337 212
                $strand->success = true;
338 212
                $strand->value   = $strand->current->getReturn();
339 3
            } catch (Throwable $reason) {
340 3
                $strand->success = false;
341 3
                $strand->value   = $reason;
342
            }
343
344 212
            if ($strand->depth <= 0) {
345 212
                continue;
346
            }
347
348 115
            $current         = &$strand->stack[--$strand->depth];
349 115
            $strand->current = $current;
350 115
            $current         = null;
351 115
            goto RESUME;
352
        }
353
354 216
        if ($this->pending > 0 || $this->schedule->isEmpty()) {
355 216
            return;
356
        }
357
358
        /** @var CoroutineContext $ctx */
359 2
        $ctx = $this->schedule->dequeue();
360 2
        $this->queue->enqueue(new Strand($this->spawn($ctx)));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->spawn($ctx) targeting GraphQL\Experimental\Exe...outineExecutor::spawn() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
361 2
        goto RUN;
362
    }
363
364 41
    private function done()
365
    {
366 41
        --$this->pending;
367
368 41
        $this->run();
369
370 41
        if ($this->pending > 0) {
371 25
            return;
372
        }
373
374 41
        $doResolve = $this->doResolve;
375 41
        $doResolve($this->finishExecute($this->rootResult, $this->errors));
376 41
    }
377
378 212
    private function spawn(CoroutineContext $ctx)
379
    {
380
        // short-circuit evaluation for __typename
381 212
        if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
382 7
            $ctx->result->{$ctx->shared->resultName} = $ctx->type->name;
383
384 7
            return;
385
        }
386
387
        try {
388 212
            if ($ctx->shared->typeGuard1 === $ctx->type) {
389 23
                $resolve                = $ctx->shared->resolveIfType1;
390 23
                $ctx->resolveInfo       = clone $ctx->shared->resolveInfoIfType1;
391 23
                $ctx->resolveInfo->path = $ctx->path;
392 23
                $arguments              = $ctx->shared->argumentsIfType1;
393 23
                $returnType             = $ctx->resolveInfo->returnType;
394
            } else {
395 212
                $fieldDefinition = $this->findFieldDefinition($ctx);
396
397 212
                if ($fieldDefinition->resolveFn !== null) {
398 155
                    $resolve = $fieldDefinition->resolveFn;
399 99
                } elseif ($ctx->type->resolveFieldFn !== null) {
400
                    $resolve = $ctx->type->resolveFieldFn;
401
                } else {
402 99
                    $resolve = $this->fieldResolver;
403
                }
404
405 212
                $returnType = $fieldDefinition->getType();
406
407 212
                $ctx->resolveInfo = new ResolveInfo(
408 212
                    $fieldDefinition,
409 212
                    $ctx->shared->fieldNodes,
410 212
                    $ctx->type,
411 212
                    $ctx->path,
412 212
                    $this->schema,
413 212
                    $this->collector->fragments,
414 212
                    $this->rootValue,
415 212
                    $this->collector->operation,
416 212
                    $this->variableValues
0 ignored issues
show
Bug introduced by
It seems like $this->variableValues can also be of type null; however, parameter $variableValues of GraphQL\Type\Definition\ResolveInfo::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

416
                    /** @scrutinizer ignore-type */ $this->variableValues
Loading history...
417
                );
418
419 212
                $arguments = Values::getArgumentValuesForMap(
420 212
                    $fieldDefinition,
421 212
                    $ctx->shared->argumentValueMap,
0 ignored issues
show
Bug introduced by
It seems like $ctx->shared->argumentValueMap can also be of type GraphQL\Language\AST\ValueNode[]; however, parameter $argumentValueMap of GraphQL\Executor\Values::getArgumentValuesForMap() does only seem to accept GraphQL\Language\AST\ArgumentNode[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

421
                    /** @scrutinizer ignore-type */ $ctx->shared->argumentValueMap,
Loading history...
422 212
                    $this->variableValues
423
                );
424
425
                // !!! assign only in batch when no exception can be thrown in-between
426 205
                $ctx->shared->typeGuard1         = $ctx->type;
427 205
                $ctx->shared->resolveIfType1     = $resolve;
428 205
                $ctx->shared->argumentsIfType1   = $arguments;
429 205
                $ctx->shared->resolveInfoIfType1 = $ctx->resolveInfo;
430
            }
431
432 205
            $value = $resolve($ctx->value, $arguments, $this->contextValue, $ctx->resolveInfo);
433
434 200
            if (! $this->completeValueFast($ctx, $returnType, $value, $ctx->path, $returnValue)) {
435 115
                $returnValue = yield $this->completeValue(
436 115
                    $ctx,
437 115
                    $returnType,
438 115
                    $value,
439 115
                    $ctx->path,
440 200
                    $ctx->nullFence
441
                );
442
            }
443 21
        } catch (Throwable $reason) {
444 21
            $this->addError(Error::createLocatedError(
445 21
                $reason,
446 21
                $ctx->shared->fieldNodes,
447 21
                $ctx->path
448
            ));
449
450 21
            $returnValue = self::$undefined;
451
        }
452
453 212
        if ($returnValue !== self::$undefined) {
454 197
            $ctx->result->{$ctx->shared->resultName} = $returnValue;
455 39
        } elseif ($ctx->resolveInfo !== null && $ctx->resolveInfo->returnType instanceof NonNull) { // !!! $ctx->resolveInfo might not have been initialized yet
456 19
            $result =& $this->rootResult;
457 19
            foreach ($ctx->nullFence ?? [] as $key) {
458 15
                if (is_string($key)) {
459 15
                    $result =& $result->{$key};
460
                } else {
461 15
                    $result =& $result[$key];
462
                }
463
            }
464 19
            $result = null;
465
        }
466 212
    }
467
468 212
    private function findFieldDefinition(CoroutineContext $ctx)
469
    {
470 212
        if ($ctx->shared->fieldName === Introspection::SCHEMA_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
471 6
            return Introspection::schemaMetaFieldDef();
472
        }
473
474 212
        if ($ctx->shared->fieldName === Introspection::TYPE_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
475 15
            return Introspection::typeMetaFieldDef();
476
        }
477
478 212
        if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
479 5
            return Introspection::typeNameMetaFieldDef();
480
        }
481
482 212
        return $ctx->type->getField($ctx->shared->fieldName);
483
    }
484
485
    /**
486
     * @param mixed    $value
487
     * @param string[] $path
488
     * @param mixed    $returnValue
489
     */
490 200
    private function completeValueFast(CoroutineContext $ctx, Type $type, $value, array $path, &$returnValue) : bool
491
    {
492
        // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
493 200
        if ($this->isPromise($value) || $value instanceof Throwable) {
494 35
            return false;
495
        }
496
497 191
        $nonNull = false;
498 191
        if ($type instanceof NonNull) {
499 36
            $nonNull = true;
500 36
            $type    = $type->getWrappedType();
501
        }
502
503 191
        if (! $type instanceof LeafType) {
504 103
            return false;
505
        }
506
507 169
        if ($type !== $this->schema->getType($type->name)) {
508
            $hint = '';
509
            if ($this->schema->getConfig()->typeLoader !== null) {
510
                $hint = sprintf(
511
                    'Make sure that type loader returns the same instance as defined in %s.%s',
512
                    $ctx->type,
513
                    $ctx->shared->fieldName
514
                );
515
            }
516
            $this->addError(Error::createLocatedError(
517
                new InvariantViolation(
518
                    sprintf(
519
                        'Schema must contain unique named types but contains multiple types named "%s". %s ' .
520
                        '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
521
                        $type->name,
522
                        $hint
523
                    )
524
                ),
525
                $ctx->shared->fieldNodes,
526
                $path
527
            ));
528
529
            $value = null;
530
        }
531
532 169
        if ($value === null) {
533 35
            $returnValue = null;
534
        } else {
535
            try {
536 154
                $returnValue = $type->serialize($value);
537 3
            } catch (Throwable $error) {
538 3
                $this->addError(Error::createLocatedError(
539 3
                    new InvariantViolation(
540 3
                        'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
541 3
                        0,
542 3
                        $error
543
                    ),
544 3
                    $ctx->shared->fieldNodes,
545 3
                    $path
546
                ));
547 3
                $returnValue = null;
548
            }
549
        }
550
551 169
        if ($nonNull && $returnValue === null) {
552 8
            $this->addError(Error::createLocatedError(
553 8
                new InvariantViolation(sprintf(
554 8
                    'Cannot return null for non-nullable field %s.%s.',
555 8
                    $ctx->type->name,
556 8
                    $ctx->shared->fieldName
557
                )),
558 8
                $ctx->shared->fieldNodes,
559 8
                $path
560
            ));
561
562 8
            $returnValue = self::$undefined;
563
        }
564
565 169
        return true;
566
    }
567
568
    /**
569
     * @param mixed         $value
570
     * @param string[]      $path
571
     * @param string[]|null $nullFence
572
     *
573
     * @return mixed
574
     */
575 115
    private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
576
    {
577 115
        $nonNull     = false;
578 115
        $returnValue = null;
579
580 115
        if ($type instanceof NonNull) {
581 34
            $nonNull = true;
582 34
            $type    = $type->getWrappedType();
583
        } else {
584 108
            $nullFence = $path;
585
        }
586
587
        // !!! $value might be promise, yield to resolve
588
        try {
589 115
            if ($this->isPromise($value)) {
590 115
                $value = yield $value;
591
            }
592 16
        } catch (Throwable $reason) {
593 16
            $this->addError(Error::createLocatedError(
594 16
                $reason,
595 16
                $ctx->shared->fieldNodes,
596 16
                $path
597
            ));
598 16
            if ($nonNull) {
599 8
                $returnValue = self::$undefined;
600
            } else {
601 8
                $returnValue = null;
602
            }
603 16
            goto CHECKED_RETURN;
604
        }
605
606 113
        if ($value === null) {
607 33
            $returnValue = $value;
608 33
            goto CHECKED_RETURN;
609 104
        } elseif ($value instanceof Throwable) {
610
            // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
611 1
            $this->addError(Error::createLocatedError(
612 1
                $value,
613 1
                $ctx->shared->fieldNodes,
614 1
                $path
615
            ));
616 1
            if ($nonNull) {
617
                $returnValue = self::$undefined;
618
            } else {
619 1
                $returnValue = null;
620
            }
621 1
            goto CHECKED_RETURN;
622
        }
623
624 104
        if ($type instanceof ListOfType) {
625 61
            $returnValue = [];
626 61
            $index       = -1;
627 61
            $itemType    = $type->getWrappedType();
628 61
            foreach ($value as $itemValue) {
629 60
                ++$index;
630
631 60
                $itemPath               = $path;
632 60
                $itemPath[]             = $index; // !!! use arrays COW semantics
633 60
                $ctx->resolveInfo->path = $itemPath;
634
635
                try {
636 60
                    if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
637 60
                        $itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
638
                    }
639 3
                } catch (Throwable $reason) {
640 3
                    $this->addError(Error::createLocatedError(
641 3
                        $reason,
642 3
                        $ctx->shared->fieldNodes,
643 3
                        $itemPath
644
                    ));
645 3
                    $itemReturnValue = null;
646
                }
647 60
                if ($itemReturnValue === self::$undefined) {
648 6
                    $returnValue = self::$undefined;
649 6
                    goto CHECKED_RETURN;
650
                }
651 60
                $returnValue[$index] = $itemReturnValue;
652
            }
653
654 61
            goto CHECKED_RETURN;
655
        } else {
656 103
            if ($type !== $this->schema->getType($type->name)) {
657 1
                $hint = '';
658 1
                if ($this->schema->getConfig()->typeLoader !== null) {
659 1
                    $hint = sprintf(
660 1
                        'Make sure that type loader returns the same instance as defined in %s.%s',
661 1
                        $ctx->type,
662 1
                        $ctx->shared->fieldName
663
                    );
664
                }
665 1
                $this->addError(Error::createLocatedError(
666 1
                    new InvariantViolation(
667 1
                        sprintf(
668
                            'Schema must contain unique named types but contains multiple types named "%s". %s ' .
669 1
                            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
670 1
                            $type->name,
671 1
                            $hint
672
                        )
673
                    ),
674 1
                    $ctx->shared->fieldNodes,
675 1
                    $path
676
                ));
677
678 1
                $returnValue = null;
679 1
                goto CHECKED_RETURN;
680
            }
681
682 102
            if ($type instanceof LeafType) {
683
                try {
684 10
                    $returnValue = $type->serialize($value);
685
                } catch (Throwable $error) {
686
                    $this->addError(Error::createLocatedError(
687
                        new InvariantViolation(
688
                            'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
689
                            0,
690
                            $error
691
                        ),
692
                        $ctx->shared->fieldNodes,
693
                        $path
694
                    ));
695
                    $returnValue = null;
696
                }
697 10
                goto CHECKED_RETURN;
698 97
            } elseif ($type instanceof CompositeType) {
699
                /** @var ObjectType|null $objectType */
700 97
                $objectType = null;
701 97
                if ($type instanceof InterfaceType || $type instanceof UnionType) {
702 33
                    $objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);
703
704 33
                    if ($objectType === null) {
705 11
                        $objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
706
                    }
707
708
                    // !!! $objectType->resolveType() might return promise, yield to resolve
709 32
                    $objectType = yield $objectType;
710 31
                    if (is_string($objectType)) {
711 2
                        $objectType = $this->schema->getType($objectType);
712
                    }
713
714 31
                    if ($objectType === null) {
715
                        $this->addError(Error::createLocatedError(
716
                            sprintf(
717
                                'Composite type "%s" did not resolve concrete object type for value: %s.',
718
                                $type->name,
719
                                Utils::printSafe($value)
720
                            ),
721
                            $ctx->shared->fieldNodes,
722
                            $path
723
                        ));
724
725
                        $returnValue = self::$undefined;
726
                        goto CHECKED_RETURN;
727 31
                    } elseif (! $objectType instanceof ObjectType) {
728 1
                        $this->addError(Error::createLocatedError(
729 1
                            new InvariantViolation(sprintf(
730
                                'Abstract type %s must resolve to an Object type at ' .
731
                                'runtime for field %s.%s with value "%s", received "%s". ' .
732
                                'Either the %s type should provide a "resolveType" ' .
733 1
                                'function or each possible type should provide an "isTypeOf" function.',
734 1
                                $type,
735 1
                                $ctx->resolveInfo->parentType,
736 1
                                $ctx->resolveInfo->fieldName,
737 1
                                Utils::printSafe($value),
738 1
                                Utils::printSafe($objectType),
739 1
                                $type
740
                            )),
741 1
                            $ctx->shared->fieldNodes,
742 1
                            $path
743
                        ));
744
745 1
                        $returnValue = null;
746 1
                        goto CHECKED_RETURN;
747 30
                    } elseif (! $this->schema->isPossibleType($type, $objectType)) {
748 4
                        $this->addError(Error::createLocatedError(
749 4
                            new InvariantViolation(sprintf(
750 4
                                'Runtime Object type "%s" is not a possible type for "%s".',
751 4
                                $objectType,
752 4
                                $type
753
                            )),
754 4
                            $ctx->shared->fieldNodes,
755 4
                            $path
756
                        ));
757
758 4
                        $returnValue = null;
759 4
                        goto CHECKED_RETURN;
760 30
                    } elseif ($objectType !== $this->schema->getType($objectType->name)) {
0 ignored issues
show
introduced by
The condition $objectType !== $this->s...Type($objectType->name) is always true.
Loading history...
761 1
                        $this->addError(Error::createLocatedError(
762 1
                            new InvariantViolation(
763 1
                                sprintf(
764
                                    'Schema must contain unique named types but contains multiple types named "%s". ' .
765
                                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
766
                                    'type instance as referenced anywhere else within the schema ' .
767 1
                                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
768 1
                                    $objectType,
769 1
                                    $type
770
                                )
771
                            ),
772 1
                            $ctx->shared->fieldNodes,
773 1
                            $path
774
                        ));
775
776 1
                        $returnValue = null;
777 30
                        goto CHECKED_RETURN;
778
                    }
779 65
                } elseif ($type instanceof ObjectType) {
780 65
                    $objectType = $type;
781
                } else {
782
                    $this->addError(Error::createLocatedError(
783
                        sprintf(
784
                            'Unexpected field type "%s".',
785
                            Utils::printSafe($type)
786
                        ),
787
                        $ctx->shared->fieldNodes,
788
                        $path
789
                    ));
790
791
                    $returnValue = self::$undefined;
792
                    goto CHECKED_RETURN;
793
                }
794
795 93
                $typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
796 93
                if ($typeCheck !== null) {
797
                    // !!! $objectType->isTypeOf() might return promise, yield to resolve
798 11
                    $typeCheck = yield $typeCheck;
799 11
                    if (! $typeCheck) {
800 1
                        $this->addError(Error::createLocatedError(
801 1
                            sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
802 1
                            $ctx->shared->fieldNodes,
803 1
                            $path
804
                        ));
805
806 1
                        $returnValue = null;
807 1
                        goto CHECKED_RETURN;
808
                    }
809
                }
810
811 93
                $returnValue = new stdClass();
812
813 93
                if ($ctx->shared->typeGuard2 === $objectType) {
0 ignored issues
show
introduced by
The condition $ctx->shared->typeGuard2 === $objectType is always false.
Loading history...
814 23
                    foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
815 23
                        $childCtx              = clone $childCtx;
816 23
                        $childCtx->type        = $objectType;
817 23
                        $childCtx->value       = $value;
818 23
                        $childCtx->result      = $returnValue;
819 23
                        $childCtx->path        = $path;
820 23
                        $childCtx->path[]      = $childCtx->shared->resultName; // !!! uses array COW semantics
821 23
                        $childCtx->nullFence   = $nullFence;
822 23
                        $childCtx->resolveInfo = null;
823
824 23
                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));
825
826
                        // !!! assign null to keep object keys sorted
827 23
                        $returnValue->{$childCtx->shared->resultName} = null;
828
                    }
829
                } else {
830 93
                    $childContexts = [];
831
832 93
                    $fields = [];
833 93
                    if ($this->collector !== null) {
834 93
                        $fields = $this->collector->collectFields(
835 93
                            $objectType,
836 93
                            $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)
837
                        );
838
                    }
839
840
                    /** @var CoroutineContextShared $childShared */
841 93
                    foreach ($fields as $childShared) {
842 93
                        $childPath   = $path;
843 93
                        $childPath[] = $childShared->resultName; // !!! uses array COW semantics
844 93
                        $childCtx    = new CoroutineContext(
845 93
                            $childShared,
846 93
                            $objectType,
847 93
                            $value,
848 93
                            $returnValue,
849 93
                            $childPath,
850 93
                            $nullFence
851
                        );
852
853 93
                        $childContexts[] = $childCtx;
854
855 93
                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->spawn($childCtx) targeting GraphQL\Experimental\Exe...outineExecutor::spawn() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
856
857
                        // !!! assign null to keep object keys sorted
858 93
                        $returnValue->{$childShared->resultName} = null;
859
                    }
860
861 93
                    $ctx->shared->typeGuard2           = $objectType;
862 93
                    $ctx->shared->childContextsIfType2 = $childContexts;
863
                }
864
865 93
                goto CHECKED_RETURN;
866
            } else {
867
                $this->addError(Error::createLocatedError(
868
                    sprintf('Unhandled type "%s".', Utils::printSafe($type)),
869
                    $ctx->shared->fieldNodes,
870
                    $path
871
                ));
872
873
                $returnValue = null;
874
                goto CHECKED_RETURN;
875
            }
876
        }
877
878
        CHECKED_RETURN:
879 115
        if ($nonNull && $returnValue === null) {
880 10
            $this->addError(Error::createLocatedError(
881 10
                new InvariantViolation(sprintf(
882 10
                    'Cannot return null for non-nullable field %s.%s.',
883 10
                    $ctx->type->name,
884 10
                    $ctx->shared->fieldName
885
                )),
886 10
                $ctx->shared->fieldNodes,
887 10
                $path
888
            ));
889
890 10
            return self::$undefined;
891
        }
892
893 114
        return $returnValue;
894
    }
895
896 93
    private function mergeSelectionSets(CoroutineContext $ctx)
897
    {
898 93
        $selections = [];
899
900 93
        foreach ($ctx->shared->fieldNodes as $fieldNode) {
901 93
            if ($fieldNode->selectionSet === null) {
902
                continue;
903
            }
904
905 93
            foreach ($fieldNode->selectionSet->selections as $selection) {
906 93
                $selections[] = $selection;
907
            }
908
        }
909
910 93
        return $ctx->shared->mergedSelectionSet = new SelectionSetNode(['selections' => $selections]);
911
    }
912
913
    /**
914
     * @param InterfaceType|UnionType $abstractType
915
     *
916
     * @return Generator|ObjectType|Type|null
917
     */
918 11
    private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $abstractType)
919
    {
920 11
        if ($value !== null &&
921 11
            is_array($value) &&
922 11
            isset($value['__typename']) &&
923 11
            is_string($value['__typename'])
924
        ) {
925 2
            return $this->schema->getType($value['__typename']);
926
        }
927
928 9
        if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader !== null) {
929 1
            Warning::warnOnce(
930 1
                sprintf(
931
                    'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
932
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
933
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
934 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
935 1
                    $abstractType->name,
936 1
                    Utils::printSafe($value)
937
                ),
938 1
                Warning::WARNING_FULL_SCHEMA_SCAN
939
            );
940
        }
941
942 9
        $possibleTypes = $this->schema->getPossibleTypes($abstractType);
943
944
        // to be backward-compatible with old executor, ->isTypeOf() is called for all possible types,
945
        // it cannot short-circuit when the match is found
946
947 9
        $selectedType = null;
948 9
        foreach ($possibleTypes as $type) {
949 9
            $typeCheck = yield $type->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
950 9
            if ($selectedType !== null || ! $typeCheck) {
951 9
                continue;
952
            }
953
954 9
            $selectedType = $type;
955
        }
956
957 8
        return $selectedType;
958
    }
959
960
    /**
961
     * @param mixed $value
962
     *
963
     * @return bool
964
     */
965 200
    private function isPromise($value)
966
    {
967 200
        return $value instanceof Promise || $this->promiseAdapter->isThenable($value);
968
    }
969
}
970