Passed
Push — master ( e17f57...acc044 )
by Vladimir
09:17 queued 11s
created

CoroutineExecutor::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 22
ccs 11
cts 11
cp 1
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
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 212
    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 212
        if (self::$undefined === null) {
105 1
            self::$undefined = Utils::undefined();
106
        }
107
108 212
        $this->schema            = $schema;
109 212
        $this->fieldResolver     = $fieldResolver;
110 212
        $this->promiseAdapter    = $promiseAdapter;
111 212
        $this->rootValue         = $rootValue;
112 212
        $this->contextValue      = $contextValue;
113 212
        $this->rawVariableValues = $rawVariableValues;
114 212
        $this->documentNode      = $documentNode;
115 212
        $this->operationName     = $operationName;
116 212
    }
117
118 212
    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 212
        return new static(
129 212
            $promiseAdapter,
130 212
            $schema,
131 212
            $documentNode,
132 212
            $rootValue,
133 212
            $contextValue,
134 212
            $variableValues,
135 212
            $operationName,
136 212
            $fieldResolver
137
        );
138
    }
139
140 194
    private static function resultToArray($value, $emptyObjectAsStdClass = true)
141
    {
142 194
        if ($value instanceof stdClass) {
143 194
            $array = [];
144 194
            foreach ($value as $propertyName => $propertyValue) {
145 190
                $array[$propertyName] = self::resultToArray($propertyValue);
146
            }
147 194
            if ($emptyObjectAsStdClass && empty($array)) {
148 1
                return new stdClass();
149
            }
150
151 194
            return $array;
152
        }
153
154 190
        if (is_array($value)) {
155 56
            $array = [];
156 56
            foreach ($value as $key => $item) {
157 56
                $array[$key] = self::resultToArray($item);
158
            }
159
160 56
            return $array;
161
        }
162
163 190
        return $value;
164
    }
165
166 212
    public function doExecute() : Promise
167
    {
168 212
        $this->rootResult = new stdClass();
169 212
        $this->errors     = [];
170 212
        $this->queue      = new SplQueue();
171 212
        $this->schedule   = new SplQueue();
172 212
        $this->pending    = 0;
173
174 212
        $this->collector = new Collector($this->schema, $this);
175 212
        $this->collector->initialize($this->documentNode, $this->operationName);
176
177 212
        if (! empty($this->errors)) {
178 4
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors));
179
        }
180
181 208
        [$errors, $coercedVariableValues] = Values::getVariableValues(
182 208
            $this->schema,
183 208
            $this->collector->operation->variableDefinitions ?: [],
184 208
            $this->rawVariableValues ?: []
185
        );
186
187 208
        if (! empty($errors)) {
188 11
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors));
189
        }
190
191 198
        $this->variableValues = $coercedVariableValues;
192
193 198
        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

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

201
                /** @scrutinizer ignore-type */ $this->collector->rootType,
Loading history...
202 194
                $this->rootValue,
203 194
                $this->rootResult,
204 194
                [$shared->resultName]
205
            );
206
207 194
            $fieldDefinition = $this->findFieldDefinition($ctx);
208 194
            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

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

248
        return new ExecutionResult(/** @scrutinizer ignore-type */ $value, $errors);
Loading history...
249
    }
250
251
    /**
252
     * @internal
253
     */
254 5
    public function evaluate(ValueNode $valueNode, InputType $type)
255
    {
256 5
        return AST::valueFromAST($valueNode, $type, $this->variableValues);
257
    }
258
259
    /**
260
     * @internal
261
     */
262 57
    public function addError($error)
263
    {
264 57
        $this->errors[] = $error;
265 57
    }
266
267 198
    private function run()
268
    {
269
        RUN:
270 198
        while (! $this->queue->isEmpty()) {
271
            /** @var Strand $strand */
272 194
            $strand = $this->queue->dequeue();
273
274
            try {
275 194
                if ($strand->success !== null) {
276
                    RESUME:
277
278 108
                    if ($strand->success) {
279 108
                        $strand->current->send($strand->value);
280
                    } else {
281 19
                        $strand->current->throw($strand->value);
0 ignored issues
show
Bug introduced by
The method throw() does not exist on Generator. ( Ignorable by Annotation )

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

281
                        $strand->current->/** @scrutinizer ignore-call */ 
282
                                          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...
282
                    }
283
284 108
                    $strand->success = null;
285 108
                    $strand->value   = null;
286
                }
287
288
                START:
289 194
                if ($strand->current->valid()) {
290 108
                    $value = $strand->current->current();
291
292 108
                    if ($value instanceof Generator) {
293 108
                        $strand->stack[$strand->depth++] = $strand->current;
294 108
                        $strand->current                 = $value;
295 108
                        goto START;
296 65
                    } elseif ($this->isPromise($value)) {
297
                        // !!! increment pending before calling ->then() as it may invoke the callback right away
298 40
                        ++$this->pending;
299
300 40
                        if (! $value instanceof Promise) {
301 39
                            $value = $this->promiseAdapter->convertThenable($value);
302
                        }
303
304 40
                        $this->promiseAdapter
305 40
                            ->then(
306 40
                                $value,
307
                                function ($value) use ($strand) {
308 36
                                    $strand->success = true;
309 36
                                    $strand->value   = $value;
310 36
                                    $this->queue->enqueue($strand);
311 36
                                    $this->done();
312 40
                                },
313
                                function (Throwable $throwable) use ($strand) {
314 18
                                    $strand->success = false;
315 18
                                    $strand->value   = $throwable;
316 18
                                    $this->queue->enqueue($strand);
317 18
                                    $this->done();
318 40
                                }
319
                            );
320 40
                        continue;
321
                    } else {
322 27
                        $strand->success = true;
323 27
                        $strand->value   = $value;
324 27
                        goto RESUME;
325
                    }
326
                }
327
328 194
                $strand->success = true;
329 194
                $strand->value   = $strand->current->getReturn();
330 3
            } catch (Throwable $reason) {
331 3
                $strand->success = false;
332 3
                $strand->value   = $reason;
333
            }
334
335 194
            if ($strand->depth <= 0) {
336 194
                continue;
337
            }
338
339 108
            $current         = &$strand->stack[--$strand->depth];
340 108
            $strand->current = $current;
341 108
            $current         = null;
342 108
            goto RESUME;
343
        }
344
345 198
        if ($this->pending > 0 || $this->schedule->isEmpty()) {
346 198
            return;
347
        }
348
349
        /** @var CoroutineContext $ctx */
350 2
        $ctx = $this->schedule->dequeue();
351 2
        $this->queue->enqueue(new Strand($this->spawn($ctx)));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->spawn($ctx) targeting GraphQL\Experimental\Exe...outineExecutor::spawn() seems to always return null.

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

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

}

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

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

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

Loading history...
352 2
        goto RUN;
353
    }
354
355 40
    private function done()
356
    {
357 40
        --$this->pending;
358
359 40
        $this->run();
360
361 40
        if ($this->pending > 0) {
362 24
            return;
363
        }
364
365 40
        $doResolve = $this->doResolve;
366 40
        $doResolve($this->finishExecute($this->rootResult, $this->errors));
367 40
    }
368
369 194
    private function spawn(CoroutineContext $ctx)
370
    {
371
        // short-circuit evaluation for __typename
372 194
        if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
373 7
            $ctx->result->{$ctx->shared->resultName} = $ctx->type->name;
374
375 7
            return;
376
        }
377
378
        try {
379 194
            if ($ctx->shared->typeGuard1 === $ctx->type) {
380 21
                $resolve                = $ctx->shared->resolveIfType1;
381 21
                $ctx->resolveInfo       = clone $ctx->shared->resolveInfoIfType1;
382 21
                $ctx->resolveInfo->path = $ctx->path;
0 ignored issues
show
Documentation Bug introduced by
It seems like $ctx->path of type string[] is incompatible with the declared type array<mixed,string[]> of property $path.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
383 21
                $arguments              = $ctx->shared->argumentsIfType1;
384 21
                $returnType             = $ctx->resolveInfo->returnType;
385
            } else {
386 194
                $fieldDefinition = $this->findFieldDefinition($ctx);
387
388 194
                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...
389 137
                    $resolve = $fieldDefinition->resolveFn;
390 117
                } elseif ($ctx->type->resolveFieldFn !== null) {
391
                    $resolve = $ctx->type->resolveFieldFn;
392
                } else {
393 117
                    $resolve = $this->fieldResolver;
394
                }
395
396 194
                $returnType = $fieldDefinition->getType();
397
398 194
                $ctx->resolveInfo = new ResolveInfo(
399 194
                    $ctx->shared->fieldName,
400 194
                    $ctx->shared->fieldNodes,
401 194
                    $returnType,
402 194
                    $ctx->type,
403 194
                    $ctx->path,
404 194
                    $this->schema,
405 194
                    $this->collector->fragments,
406 194
                    $this->rootValue,
407 194
                    $this->collector->operation,
408 194
                    $this->variableValues
0 ignored issues
show
Bug introduced by
It seems like $this->variableValues can also be of type null; however, parameter $variableValues of GraphQL\Type\Definition\ResolveInfo::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

408
                    /** @scrutinizer ignore-type */ $this->variableValues
Loading history...
409
                );
410
411 194
                $arguments = Values::getArgumentValuesForMap(
412 194
                    $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

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

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

693
                    $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\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

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

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

928
            $typeCheck = yield $type->isTypeOf($value, $this->contextValue, /** @scrutinizer ignore-type */ $ctx->resolveInfo);
Loading history...
929 9
            if ($selectedType !== null || $typeCheck !== true) {
930 9
                continue;
931
            }
932
933 9
            $selectedType = $type;
934
        }
935
936 8
        return $selectedType;
937
    }
938
939
    /**
940
     * @param mixed $value
941
     *
942
     * @return bool
943
     */
944 186
    private function isPromise($value)
945
    {
946 186
        return $value instanceof Promise || $this->promiseAdapter->isThenable($value);
947
    }
948
}
949