CoroutineExecutor::create()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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