Passed
Pull Request — master (#314)
by Jakub
08:46 queued 02:41
created

CoroutineExecutor::run()   C

Complexity

Conditions 11
Paths 0

Size

Total Lines 82
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 0
Metric Value
eloc 55
dl 0
loc 82
ccs 0
cts 66
cp 0
rs 6.8351
c 0
b 0
f 0
cc 11
nc 0
nop 0
crap 132

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\InputType;
22
use GraphQL\Type\Definition\InterfaceType;
23
use GraphQL\Type\Definition\LeafType;
24
use GraphQL\Type\Definition\ListOfType;
25
use GraphQL\Type\Definition\NonNull;
26
use GraphQL\Type\Definition\ObjectType;
27
use GraphQL\Type\Definition\ResolveInfo;
28
use GraphQL\Type\Definition\Type;
29
use GraphQL\Type\Definition\UnionType;
30
use GraphQL\Type\Introspection;
31
use GraphQL\Type\Schema;
32
use GraphQL\Utils\AST;
33
use GraphQL\Utils\Utils;
34
use SplQueue;
35
use stdClass;
36
use Throwable;
37
use function is_array;
38
use function is_string;
39
use function sprintf;
40
41
class CoroutineExecutor implements Runtime, ExecutorImplementation
42
{
43
    /** @var object */
44
    private static $undefined;
45
46
    /** @var Schema */
47
    private $schema;
48
49
    /** @var callable */
50
    private $fieldResolver;
51
52
    /** @var PromiseAdapter */
53
    private $promiseAdapter;
54
55
    /** @var mixed|null */
56
    private $rootValue;
57
58
    /** @var mixed|null */
59
    private $contextValue;
60
61
    /** @var mixed|null */
62
    private $rawVariableValues;
63
64
    /** @var mixed|null */
65
    private $variableValues;
66
67
    /** @var DocumentNode */
68
    private $documentNode;
69
70
    /** @var string|null */
71
    private $operationName;
72
73
    /** @var Collector */
74
    private $collector;
75
76
    /** @var Error[] */
77
    private $errors;
78
79
    /** @var SplQueue */
80
    private $queue;
81
82
    /** @var SplQueue */
83
    private $schedule;
84
85
    /** @var stdClass */
86
    private $rootResult;
87
88
    /** @var int */
89
    private $pending;
90
91
    /** @var callable */
92
    private $doResolve;
93
94
    public function __construct(
95
        PromiseAdapter $promiseAdapter,
96
        Schema $schema,
97
        DocumentNode $documentNode,
98
        $rootValue,
99
        $contextValue,
100
        $rawVariableValues,
101
        ?string $operationName,
102
        callable $fieldResolver
103
    ) {
104
        if (self::$undefined === null) {
105
            self::$undefined = Utils::undefined();
106
        }
107
108
        $this->schema            = $schema;
109
        $this->fieldResolver     = $fieldResolver;
110
        $this->promiseAdapter    = $promiseAdapter;
111
        $this->rootValue         = $rootValue;
112
        $this->contextValue      = $contextValue;
113
        $this->rawVariableValues = $rawVariableValues;
114
        $this->documentNode      = $documentNode;
115
        $this->operationName     = $operationName;
116
    }
117
118
    public static function create(
119
        PromiseAdapter $promiseAdapter,
120
        Schema $schema,
121
        DocumentNode $documentNode,
122
        $rootValue,
123
        $contextValue,
124
        $variableValues,
125
        ?string $operationName,
126
        callable $fieldResolver
127
    ) {
128
        return new static(
129
            $promiseAdapter,
130
            $schema,
131
            $documentNode,
132
            $rootValue,
133
            $contextValue,
134
            $variableValues,
135
            $operationName,
136
            $fieldResolver
137
        );
138
    }
139
140
    private static function resultToArray($value, $emptyObjectAsStdClass = true)
141
    {
142
        if ($value instanceof stdClass) {
143
            $array = [];
144
            foreach ($value as $propertyName => $propertyValue) {
145
                $array[$propertyName] = self::resultToArray($propertyValue);
146
            }
147
            if ($emptyObjectAsStdClass && empty($array)) {
148
                return new stdClass();
149
            }
150
            return $array;
151
        }
152
153
        if (is_array($value)) {
154
            $array = [];
155
            foreach ($value as $item) {
156
                $array[] = self::resultToArray($item);
157
            }
158
            return $array;
159
        }
160
161
        return $value;
162
    }
163
164
    public function doExecute() : Promise
165
    {
166
        $this->rootResult = new stdClass();
167
        $this->errors     = [];
168
        $this->queue      = new SplQueue();
169
        $this->schedule   = new SplQueue();
170
        $this->pending    = 0;
171
172
        $this->collector = new Collector($this->schema, $this);
173
        $this->collector->initialize($this->documentNode, $this->operationName);
174
175
        if (! empty($this->errors)) {
176
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors));
177
        }
178
179
        [$errors, $coercedVariableValues] = Values::getVariableValues(
180
            $this->schema,
181
            $this->collector->operation->variableDefinitions ?: [],
182
            $this->rawVariableValues ?: []
183
        );
184
185
        if (! empty($errors)) {
186
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors));
187
        }
188
189
        $this->variableValues = $coercedVariableValues;
190
191
        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

191
        foreach ($this->collector->collectFields(/** @scrutinizer ignore-type */ $this->collector->rootType, $this->collector->operation->selectionSet) as $shared) {
Loading history...
192
            /** @var CoroutineContextShared $shared */
193
194
            // !!! assign to keep object keys sorted
195
            $this->rootResult->{$shared->resultName} = null;
196
197
            $ctx = new CoroutineContext(
198
                $shared,
199
                $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

199
                /** @scrutinizer ignore-type */ $this->collector->rootType,
Loading history...
200
                $this->rootValue,
201
                $this->rootResult,
202
                [$shared->resultName]
203
            );
204
205
            $fieldDefinition = $this->findFieldDefinition($ctx);
206
            if (! $fieldDefinition->getType() instanceof NonNull) {
0 ignored issues
show
Bug introduced by
The method getType() does not exist on GraphQL\Type\Definition\Type. ( Ignorable by Annotation )

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

206
            if (! $fieldDefinition->/** @scrutinizer ignore-call */ getType() instanceof NonNull) {

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...
207
                $ctx->nullFence = [$shared->resultName];
208
            }
209
210
            if ($this->collector->operation->operation === 'mutation' && ! $this->queue->isEmpty()) {
211
                $this->schedule->enqueue($ctx);
212
            } else {
213
                $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...
214
            }
215
        }
216
217
        $this->run();
218
219
        if ($this->pending > 0) {
220
            return $this->promiseAdapter->create(function (callable $resolve) {
221
                $this->doResolve = $resolve;
222
            });
223
        }
224
225
        return $this->promiseAdapter->createFulfilled($this->finishExecute($this->rootResult, $this->errors));
226
    }
227
228
    /**
229
     * @param object|null $value
230
     * @param Error[]     $errors
231
     */
232
    private function finishExecute($value, array $errors) : ExecutionResult
233
    {
234
        $this->rootResult     = null;
235
        $this->errors         = null;
236
        $this->queue          = null;
237
        $this->schedule       = null;
238
        $this->pending        = null;
239
        $this->collector      = null;
240
        $this->variableValues = null;
241
242
        if ($value !== null) {
243
            $value = self::resultToArray($value, false);
244
        }
245
246
        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

246
        return new ExecutionResult(/** @scrutinizer ignore-type */ $value, $errors);
Loading history...
247
    }
248
249
    /**
250
     * @internal
251
     */
252
    public function evaluate(ValueNode $valueNode, InputType $type)
253
    {
254
        return AST::valueFromAST($valueNode, $type, $this->variableValues);
255
    }
256
257
    /**
258
     * @internal
259
     */
260
    public function addError($error)
261
    {
262
        $this->errors[] = $error;
263
    }
264
265
    private function run()
266
    {
267
        RUN:
268
        while (! $this->queue->isEmpty()) {
269
            /** @var Strand $strand */
270
            $strand = $this->queue->dequeue();
271
272
            try {
273
                if ($strand->success !== null) {
274
                    RESUME:
275
276
                    if ($strand->success) {
277
                        $strand->current->send($strand->value);
278
                    } else {
279
                        $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

279
                        $strand->current->/** @scrutinizer ignore-call */ 
280
                                          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...
280
                    }
281
282
                    $strand->success = null;
283
                    $strand->value   = null;
284
                }
285
286
                START:
287
                if ($strand->current->valid()) {
288
                    $value = $strand->current->current();
289
290
                    if ($value instanceof Generator) {
291
                        $strand->stack[$strand->depth++] = $strand->current;
292
                        $strand->current                 = $value;
293
                        goto START;
294
                    } elseif ($this->promiseAdapter->isThenable($value)) {
295
                        // !!! increment pending before calling ->then() as it may invoke the callback right away
296
                        ++$this->pending;
297
298
                        $this->promiseAdapter
299
                            ->convertThenable($value)
300
                            ->then(
301
                                function ($value) use ($strand) {
302
                                    $strand->success = true;
303
                                    $strand->value   = $value;
304
                                    $this->queue->enqueue($strand);
305
                                    $this->done();
306
                                },
307
                                function (Throwable $throwable) use ($strand) {
308
                                    $strand->success = false;
309
                                    $strand->value   = $throwable;
310
                                    $this->queue->enqueue($strand);
311
                                    $this->done();
312
                                }
313
                            );
314
                        continue;
315
                    } else {
316
                        $strand->success = true;
317
                        $strand->value   = $value;
318
                        goto RESUME;
319
                    }
320
                }
321
322
                $strand->success = true;
323
                $strand->value   = $strand->current->getReturn();
324
            } catch (Throwable $reason) {
325
                $strand->success = false;
326
                $strand->value   = $reason;
327
            }
328
329
            if ($strand->depth <= 0) {
330
                continue;
331
            }
332
333
            $current         = &$strand->stack[--$strand->depth];
334
            $strand->current = $current;
335
            $current         = null;
336
            goto RESUME;
337
        }
338
339
        if ($this->pending > 0 || $this->schedule->isEmpty()) {
340
            return;
341
        }
342
343
        /** @var CoroutineContext $ctx */
344
        $ctx = $this->schedule->dequeue();
345
        $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...
346
        goto RUN;
347
    }
348
349
    private function done()
350
    {
351
        --$this->pending;
352
353
        $this->run();
354
355
        if ($this->pending > 0) {
356
            return;
357
        }
358
359
        $doResolve = $this->doResolve;
360
        $doResolve($this->finishExecute($this->rootResult, $this->errors));
361
    }
362
363
    private function spawn(CoroutineContext $ctx)
364
    {
365
        // short-circuit evaluation for __typename
366
        if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
367
            $ctx->result->{$ctx->shared->resultName} = $ctx->type->name;
368
            return;
369
        }
370
371
        try {
372
            if ($ctx->shared->typeGuard1 === $ctx->type) {
373
                $resolve                = $ctx->shared->resolveIfType1;
374
                $ctx->resolveInfo       = clone $ctx->shared->resolveInfoIfType1;
375
                $ctx->resolveInfo->path = $ctx->path;
376
                $arguments              = $ctx->shared->argumentsIfType1;
377
                $returnType             = $ctx->resolveInfo->returnType;
378
            } else {
379
                $fieldDefinition = $this->findFieldDefinition($ctx);
380
381
                if ($fieldDefinition->resolveFn !== null) {
0 ignored issues
show
Bug introduced by
The property resolveFn does not seem to exist on GraphQL\Type\Definition\Type.
Loading history...
382
                    $resolve = $fieldDefinition->resolveFn;
383
                } elseif ($ctx->type->resolveFieldFn !== null) {
384
                    $resolve = $ctx->type->resolveFieldFn;
385
                } else {
386
                    $resolve = $this->fieldResolver;
387
                }
388
389
                $returnType = $fieldDefinition->getType();
390
391
                $ctx->resolveInfo = new ResolveInfo(
392
                    $ctx->shared->fieldName,
393
                    $ctx->shared->fieldNodes,
394
                    $returnType,
395
                    $ctx->type,
396
                    $ctx->path,
397
                    $this->schema,
398
                    $this->collector->fragments,
399
                    $this->rootValue,
400
                    $this->collector->operation,
401
                    $this->variableValues
402
                );
403
404
                $arguments = Values::getArgumentValuesForMap(
405
                    $fieldDefinition,
0 ignored issues
show
Bug introduced by
It seems like $fieldDefinition can also be of type GraphQL\Type\Definition\Type; however, parameter $fieldDefinition of GraphQL\Executor\Values::getArgumentValuesForMap() does only seem to accept GraphQL\Type\Definition\...inition\FieldDefinition, 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

405
                    /** @scrutinizer ignore-type */ $fieldDefinition,
Loading history...
406
                    $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

406
                    /** @scrutinizer ignore-type */ $ctx->shared->argumentValueMap,
Loading history...
407
                    $this->variableValues
408
                );
409
410
                // !!! assign only in batch when no exception can be thrown in-between
411
                $ctx->shared->typeGuard1         = $ctx->type;
412
                $ctx->shared->resolveIfType1     = $resolve;
413
                $ctx->shared->argumentsIfType1   = $arguments;
414
                $ctx->shared->resolveInfoIfType1 = $ctx->resolveInfo;
415
            }
416
417
            $value = $resolve($ctx->value, $arguments, $this->contextValue, $ctx->resolveInfo);
418
419
            if (! $this->completeValueFast($ctx, $returnType, $value, $ctx->path, $returnValue)) {
420
                $returnValue = yield $this->completeValue(
421
                    $ctx,
422
                    $returnType,
423
                    $value,
424
                    $ctx->path,
425
                    $ctx->nullFence
426
                );
427
            }
428
        } catch (Throwable $reason) {
429
            $this->addError(Error::createLocatedError(
430
                $reason,
431
                $ctx->shared->fieldNodes,
432
                $ctx->path
433
            ));
434
435
            $returnValue = self::$undefined;
436
        }
437
438
        if ($returnValue !== self::$undefined) {
439
            $ctx->result->{$ctx->shared->resultName} = $returnValue;
440
        } elseif ($ctx->resolveInfo !== null && $ctx->resolveInfo->returnType instanceof NonNull) { // !!! $ctx->resolveInfo might not have been initialized yet
441
            $result =& $this->rootResult;
442
            foreach ($ctx->nullFence ?? [] as $key) {
443
                if (is_string($key)) {
444
                    $result =& $result->{$key};
445
                } else {
446
                    $result =& $result[$key];
447
                }
448
            }
449
            $result = null;
450
        }
451
    }
452
453
    private function findFieldDefinition(CoroutineContext $ctx)
454
    {
455
        if ($ctx->shared->fieldName === Introspection::SCHEMA_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
456
            return Introspection::schemaMetaFieldDef();
457
        }
458
459
        if ($ctx->shared->fieldName === Introspection::TYPE_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
460
            return Introspection::typeMetaFieldDef();
461
        }
462
463
        if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
464
            return Introspection::typeNameMetaFieldDef();
465
        }
466
467
        return $ctx->type->getField($ctx->shared->fieldName);
468
    }
469
470
    /**
471
     * @param mixed    $value
472
     * @param string[] $path
473
     * @param mixed    $returnValue
474
     */
475
    private function completeValueFast(CoroutineContext $ctx, Type $type, $value, array $path, &$returnValue) : bool
476
    {
477
        // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
478
        if ($this->promiseAdapter->isThenable($value) || $value instanceof Throwable) {
479
            return false;
480
        }
481
482
        $nonNull = false;
483
        if ($type instanceof NonNull) {
484
            $nonNull = true;
485
            $type    = $type->getWrappedType();
486
        }
487
488
        if (! $type instanceof LeafType) {
489
            return false;
490
        }
491
492
        if ($type !== $this->schema->getType($type->name)) {
0 ignored issues
show
introduced by
The condition $type !== $this->schema->getType($type->name) is always true.
Loading history...
493
            $hint = '';
494
            if ($this->schema->getConfig()->typeLoader) {
495
                $hint = sprintf(
496
                    'Make sure that type loader returns the same instance as defined in %s.%s',
497
                    $ctx->type,
498
                    $ctx->shared->fieldName
499
                );
500
            }
501
            $this->addError(Error::createLocatedError(
502
                new InvariantViolation(
503
                    sprintf(
504
                        'Schema must contain unique named types but contains multiple types named "%s". %s ' .
505
                        '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
506
                        $type->name,
507
                        $hint
508
                    )
509
                ),
510
                $ctx->shared->fieldNodes,
511
                $path
512
            ));
513
514
            $value = null;
515
        }
516
517
        if ($value === null) {
0 ignored issues
show
introduced by
The condition $value === null is always true.
Loading history...
518
            $returnValue = null;
519
        } else {
520
            try {
521
                $returnValue = $type->serialize($value);
522
            } catch (Throwable $error) {
523
                $this->addError(Error::createLocatedError(
524
                    new InvariantViolation(
525
                        'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
526
                        0,
527
                        $error
528
                    ),
529
                    $ctx->shared->fieldNodes,
530
                    $path
531
                ));
532
                $returnValue = null;
533
            }
534
        }
535
536
        if ($nonNull && $returnValue === null) {
537
            $this->addError(Error::createLocatedError(
538
                new InvariantViolation(sprintf(
539
                    'Cannot return null for non-nullable field %s.%s.',
540
                    $ctx->type->name,
541
                    $ctx->shared->fieldName
542
                )),
543
                $ctx->shared->fieldNodes,
544
                $path
545
            ));
546
547
            $returnValue = self::$undefined;
548
        }
549
550
        return true;
551
    }
552
553
    /**
554
     * @param mixed         $value
555
     * @param string[]      $path
556
     * @param string[]|null $nullFence
557
     *
558
     * @return mixed
559
     */
560
    private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
561
    {
562
        $nonNull     = false;
563
        $returnValue = null;
564
565
        if ($type instanceof NonNull) {
566
            $nonNull = true;
567
            $type    = $type->getWrappedType();
568
        } else {
569
            $nullFence = $path;
570
        }
571
572
        // !!! $value might be promise, yield to resolve
573
        try {
574
            if ($this->promiseAdapter->isThenable($value)) {
575
                $value = yield $value;
576
            }
577
        } catch (Throwable $reason) {
578
            $this->addError(Error::createLocatedError(
579
                $reason,
580
                $ctx->shared->fieldNodes,
581
                $path
582
            ));
583
            if ($nonNull) {
584
                $returnValue = self::$undefined;
585
            } else {
586
                $returnValue = null;
587
            }
588
            goto CHECKED_RETURN;
589
        }
590
591
        if ($value === null) {
592
            $returnValue = $value;
593
            goto CHECKED_RETURN;
594
        } elseif ($value instanceof Throwable) {
595
            // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
596
            $this->addError(Error::createLocatedError(
597
                $value,
598
                $ctx->shared->fieldNodes,
599
                $path
600
            ));
601
            if ($nonNull) {
602
                $returnValue = self::$undefined;
603
            } else {
604
                $returnValue = null;
605
            }
606
            goto CHECKED_RETURN;
607
        }
608
609
        if ($type instanceof ListOfType) {
610
            $returnValue = [];
611
            $index       = -1;
612
            $itemType    = $type->getWrappedType();
613
            foreach ($value as $itemValue) {
614
                ++$index;
615
616
                $itemPath   = $path;
617
                $itemPath[] = $index; // !!! use arrays COW semantics
618
619
                try {
620
                    if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
621
                        $itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
622
                    }
623
                } catch (Throwable $reason) {
624
                    $this->addError(Error::createLocatedError(
625
                        $reason,
626
                        $ctx->shared->fieldNodes,
627
                        $itemPath
628
                    ));
629
                    $itemReturnValue = null;
630
                }
631
                if ($itemReturnValue === self::$undefined) {
632
                    $returnValue = self::$undefined;
633
                    goto CHECKED_RETURN;
634
                }
635
                $returnValue[$index] = $itemReturnValue;
636
            }
637
638
            goto CHECKED_RETURN;
639
        } else {
640
            if ($type !== $this->schema->getType($type->name)) {
641
                $hint = '';
642
                if ($this->schema->getConfig()->typeLoader) {
643
                    $hint = sprintf(
644
                        'Make sure that type loader returns the same instance as defined in %s.%s',
645
                        $ctx->type,
646
                        $ctx->shared->fieldName
647
                    );
648
                }
649
                $this->addError(Error::createLocatedError(
650
                    new InvariantViolation(
651
                        sprintf(
652
                            'Schema must contain unique named types but contains multiple types named "%s". %s ' .
653
                            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
654
                            $type->name,
655
                            $hint
656
                        )
657
                    ),
658
                    $ctx->shared->fieldNodes,
659
                    $path
660
                ));
661
662
                $returnValue = null;
663
                goto CHECKED_RETURN;
664
            }
665
666
            if ($type instanceof LeafType) {
667
                try {
668
                    $returnValue = $type->serialize($value);
669
                } catch (Throwable $error) {
670
                    $this->addError(Error::createLocatedError(
671
                        new InvariantViolation(
672
                            'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
673
                            0,
674
                            $error
675
                        ),
676
                        $ctx->shared->fieldNodes,
677
                        $path
678
                    ));
679
                    $returnValue = null;
680
                }
681
                goto CHECKED_RETURN;
682
            } elseif ($type instanceof CompositeType) {
683
                /** @var ObjectType|null $objectType */
684
                $objectType = null;
685
                if ($type instanceof InterfaceType || $type instanceof UnionType) {
686
                    $objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);
0 ignored issues
show
Bug introduced by
It seems like $ctx->resolveInfo can also be of type null; however, parameter $info of GraphQL\Type\Definition\UnionType::resolveType() does only seem to accept GraphQL\Type\Definition\ResolveInfo, 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

686
                    $objectType = $type->resolveType($value, $this->contextValue, /** @scrutinizer ignore-type */ $ctx->resolveInfo);
Loading history...
Bug introduced by
It seems like $ctx->resolveInfo can also be of type null; however, parameter $info of GraphQL\Type\Definition\...faceType::resolveType() does only seem to accept GraphQL\Type\Definition\ResolveInfo, 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

686
                    $objectType = $type->resolveType($value, $this->contextValue, /** @scrutinizer ignore-type */ $ctx->resolveInfo);
Loading history...
687
688
                    if ($objectType === null) {
689
                        $objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
690
                    }
691
692
                    // !!! $objectType->resolveType() might return promise, yield to resolve
693
                    $objectType = yield $objectType;
694
                    if (is_string($objectType)) {
695
                        $objectType = $this->schema->getType($objectType);
696
                    }
697
698
                    if ($objectType === null) {
699
                        $this->addError(Error::createLocatedError(
700
                            sprintf(
701
                                'Composite type "%s" did not resolve concrete object type for value: %s.',
702
                                $type->name,
703
                                Utils::printSafe($value)
704
                            ),
705
                            $ctx->shared->fieldNodes,
706
                            $path
707
                        ));
708
709
                        $returnValue = self::$undefined;
710
                        goto CHECKED_RETURN;
711
                    } elseif (! $objectType instanceof ObjectType) {
712
                        $this->addError(Error::createLocatedError(
713
                            new InvariantViolation(sprintf(
714
                                'Abstract type %s must resolve to an Object type at ' .
715
                                'runtime for field %s.%s with value "%s", received "%s". ' .
716
                                'Either the %s type should provide a "resolveType" ' .
717
                                'function or each possible type should provide an "isTypeOf" function.',
718
                                $type,
719
                                $ctx->resolveInfo->parentType,
720
                                $ctx->resolveInfo->fieldName,
721
                                Utils::printSafe($value),
722
                                Utils::printSafe($objectType),
723
                                $type
724
                            )),
725
                            $ctx->shared->fieldNodes,
726
                            $path
727
                        ));
728
729
                        $returnValue = null;
730
                        goto CHECKED_RETURN;
731
                    } elseif (! $this->schema->isPossibleType($type, $objectType)) {
732
                        $this->addError(Error::createLocatedError(
733
                            new InvariantViolation(sprintf(
734
                                'Runtime Object type "%s" is not a possible type for "%s".',
735
                                $objectType,
736
                                $type
737
                            )),
738
                            $ctx->shared->fieldNodes,
739
                            $path
740
                        ));
741
742
                        $returnValue = null;
743
                        goto CHECKED_RETURN;
744
                    } 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...
745
                        $this->addError(Error::createLocatedError(
746
                            new InvariantViolation(
747
                                sprintf(
748
                                    'Schema must contain unique named types but contains multiple types named "%s". ' .
749
                                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
750
                                    'type instance as referenced anywhere else within the schema ' .
751
                                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
752
                                    $objectType,
753
                                    $type
754
                                )
755
                            ),
756
                            $ctx->shared->fieldNodes,
757
                            $path
758
                        ));
759
760
                        $returnValue = null;
761
                        goto CHECKED_RETURN;
762
                    }
763
                } elseif ($type instanceof ObjectType) {
764
                    $objectType = $type;
765
                } else {
766
                    $this->addError(Error::createLocatedError(
767
                        sprintf(
768
                            'Unexpected field type "%s".',
769
                            Utils::printSafe($type)
770
                        ),
771
                        $ctx->shared->fieldNodes,
772
                        $path
773
                    ));
774
775
                    $returnValue = self::$undefined;
776
                    goto CHECKED_RETURN;
777
                }
778
779
                $typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
0 ignored issues
show
Bug introduced by
It seems like $ctx->resolveInfo can also be of type null; however, parameter $info of GraphQL\Type\Definition\ObjectType::isTypeOf() does only seem to accept GraphQL\Type\Definition\ResolveInfo, 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

779
                $typeCheck = $objectType->isTypeOf($value, $this->contextValue, /** @scrutinizer ignore-type */ $ctx->resolveInfo);
Loading history...
780
                if ($typeCheck !== null) {
781
                    // !!! $objectType->isTypeOf() might return promise, yield to resolve
782
                    $typeCheck = yield $typeCheck;
783
                    if (! $typeCheck) {
784
                        $this->addError(Error::createLocatedError(
785
                            sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
786
                            $ctx->shared->fieldNodes,
787
                            $path
788
                        ));
789
790
                        $returnValue = null;
791
                        goto CHECKED_RETURN;
792
                    }
793
                }
794
795
                $returnValue = new stdClass();
796
797
                if ($ctx->shared->typeGuard2 === $objectType) {
0 ignored issues
show
introduced by
The condition $ctx->shared->typeGuard2 === $objectType is always false.
Loading history...
798
                    foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
799
                        $childCtx              = clone $childCtx;
800
                        $childCtx->type        = $objectType;
801
                        $childCtx->value       = $value;
802
                        $childCtx->result      = $returnValue;
803
                        $childCtx->path        = $path;
804
                        $childCtx->path[]      = $childCtx->shared->resultName; // !!! uses array COW semantics
805
                        $childCtx->nullFence   = $nullFence;
806
                        $childCtx->resolveInfo = null;
807
808
                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));
809
810
                        // !!! assign null to keep object keys sorted
811
                        $returnValue->{$childCtx->shared->resultName} = null;
812
                    }
813
                } else {
814
                    $childContexts = [];
815
816
                    foreach ($this->collector->collectFields($objectType, $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)) as $childShared) {
817
                        /** @var CoroutineContextShared $childShared */
818
819
                        $childPath   = $path;
820
                        $childPath[] = $childShared->resultName; // !!! uses array COW semantics
821
                        $childCtx    = new CoroutineContext(
822
                            $childShared,
823
                            $objectType,
824
                            $value,
825
                            $returnValue,
826
                            $childPath,
827
                            $nullFence
828
                        );
829
830
                        $childContexts[] = $childCtx;
831
832
                        $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...
833
834
                        // !!! assign null to keep object keys sorted
835
                        $returnValue->{$childShared->resultName} = null;
836
                    }
837
838
                    $ctx->shared->typeGuard2           = $objectType;
839
                    $ctx->shared->childContextsIfType2 = $childContexts;
840
                }
841
842
                goto CHECKED_RETURN;
843
            } else {
844
                $this->addError(Error::createLocatedError(
845
                    sprintf('Unhandled type "%s".', Utils::printSafe($type)),
846
                    $ctx->shared->fieldNodes,
847
                    $path
848
                ));
849
850
                $returnValue = null;
851
                goto CHECKED_RETURN;
852
            }
853
        }
854
855
        CHECKED_RETURN:
856
        if ($nonNull && $returnValue === null) {
857
            $this->addError(Error::createLocatedError(
858
                new InvariantViolation(sprintf(
859
                    'Cannot return null for non-nullable field %s.%s.',
860
                    $ctx->type->name,
861
                    $ctx->shared->fieldName
862
                )),
863
                $ctx->shared->fieldNodes,
864
                $path
865
            ));
866
867
            return self::$undefined;
868
        }
869
870
        return $returnValue;
871
    }
872
873
    private function mergeSelectionSets(CoroutineContext $ctx)
874
    {
875
        $selections = [];
876
877
        foreach ($ctx->shared->fieldNodes as $fieldNode) {
878
            if ($fieldNode->selectionSet === null) {
879
                continue;
880
            }
881
882
            foreach ($fieldNode->selectionSet->selections as $selection) {
883
                $selections[] = $selection;
884
            }
885
        }
886
887
        return $ctx->shared->mergedSelectionSet = new SelectionSetNode(['selections' => $selections]);
888
    }
889
890
    private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $abstractType)
891
    {
892
        if ($value !== null &&
893
            is_array($value) &&
894
            isset($value['__typename']) &&
895
            is_string($value['__typename'])
896
        ) {
897
            return $this->schema->getType($value['__typename']);
898
        }
899
900
        if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader) {
901
            Warning::warnOnce(
902
                sprintf(
903
                    'GraphQL Interface Type `%s` returned `null` from it`s `resolveType` function ' .
904
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
905
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
906
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
907
                    $abstractType->name,
908
                    Utils::printSafe($value)
909
                ),
910
                Warning::WARNING_FULL_SCHEMA_SCAN
911
            );
912
        }
913
914
        $possibleTypes = $this->schema->getPossibleTypes($abstractType);
915
916
        // to be backward-compatible with old executor, ->isTypeOf() is called for all possible types,
917
        // it cannot short-circuit when the match is found
918
919
        $selectedType = null;
920
        foreach ($possibleTypes as $type) {
921
            $typeCheck = yield $type->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
0 ignored issues
show
Bug introduced by
It seems like $ctx->resolveInfo can also be of type null; however, parameter $info of GraphQL\Type\Definition\ObjectType::isTypeOf() does only seem to accept GraphQL\Type\Definition\ResolveInfo, 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

921
            $typeCheck = yield $type->isTypeOf($value, $this->contextValue, /** @scrutinizer ignore-type */ $ctx->resolveInfo);
Loading history...
922
            if ($selectedType !== null || $typeCheck !== true) {
923
                continue;
924
            }
925
926
            $selectedType = $type;
927
        }
928
929
        return $selectedType;
930
    }
931
}
932