Completed
Push — master ( 988072...21e0c8 )
by Vladimir
15s queued 13s
created

CoroutineExecutor::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 19
ccs 0
cts 19
cp 0
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 8
crap 2

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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