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

CoroutineExecutor::isPromise()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 2
nc 2
nop 1
crap 2
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