Failed Conditions
Push — master ( bf4e7d...c70528 )
by Vladimir
09:31
created

ReferenceExecutor.php$0 ➔ completeValue()   C

Complexity

Conditions 11

Size

Total Lines 71

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 11.0023

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 71
ccs 36
cts 37
cp 0.973
rs 6.486
cc 11
crap 11.0023

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Executor;
6
7
use ArrayAccess;
8
use ArrayObject;
9
use Exception;
10
use GraphQL\Error\Error;
11
use GraphQL\Error\InvariantViolation;
12
use GraphQL\Error\Warning;
13
use GraphQL\Executor\Promise\Promise;
14
use GraphQL\Executor\Promise\PromiseAdapter;
15
use GraphQL\Language\AST\DocumentNode;
16
use GraphQL\Language\AST\FieldNode;
17
use GraphQL\Language\AST\FragmentDefinitionNode;
18
use GraphQL\Language\AST\FragmentSpreadNode;
19
use GraphQL\Language\AST\InlineFragmentNode;
20
use GraphQL\Language\AST\NodeKind;
21
use GraphQL\Language\AST\OperationDefinitionNode;
22
use GraphQL\Language\AST\SelectionSetNode;
23
use GraphQL\Type\Definition\AbstractType;
24
use GraphQL\Type\Definition\Directive;
25
use GraphQL\Type\Definition\FieldDefinition;
26
use GraphQL\Type\Definition\InterfaceType;
27
use GraphQL\Type\Definition\LeafType;
28
use GraphQL\Type\Definition\ListOfType;
29
use GraphQL\Type\Definition\NonNull;
30
use GraphQL\Type\Definition\ObjectType;
31
use GraphQL\Type\Definition\ResolveInfo;
32
use GraphQL\Type\Definition\Type;
33
use GraphQL\Type\Definition\UnionType;
34
use GraphQL\Type\Introspection;
35
use GraphQL\Type\Schema;
36
use GraphQL\Utils\TypeInfo;
37
use GraphQL\Utils\Utils;
38
use RuntimeException;
39
use SplObjectStorage;
40
use stdClass;
41
use Throwable;
42
use Traversable;
43
use function array_keys;
44
use function array_merge;
45
use function array_reduce;
46
use function array_values;
47
use function get_class;
48
use function is_array;
49
use function is_object;
50
use function is_string;
51
use function sprintf;
52
53
class ReferenceExecutor implements ExecutorImplementation
54
{
55
    /** @var object */
56
    private static $UNDEFINED;
57
58
    /** @var ExecutionContext */
59
    private $exeContext;
60
61
    /** @var SplObjectStorage */
62
    private $subFieldCache;
63
64 210
    private function __construct(ExecutionContext $context)
65
    {
66 210
        if (! self::$UNDEFINED) {
67 1
            self::$UNDEFINED = Utils::undefined();
68
        }
69 210
        $this->exeContext    = $context;
70 210
        $this->subFieldCache = new SplObjectStorage();
71 210
    }
72
73 225
    public static function create(
74
        PromiseAdapter $promiseAdapter,
75
        Schema $schema,
76
        DocumentNode $documentNode,
77
        $rootValue,
78
        $contextValue,
79
        $variableValues,
80
        ?string $operationName,
81
        callable $fieldResolver
82
    ) {
83 225
        $exeContext = self::buildExecutionContext(
84 225
            $schema,
85 225
            $documentNode,
86 225
            $rootValue,
87 225
            $contextValue,
88 225
            $variableValues,
89 225
            $operationName,
90 225
            $fieldResolver,
91 225
            $promiseAdapter
92
        );
93
94 225
        if (is_array($exeContext)) {
95
            return new class($promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext))) implements ExecutorImplementation
96
            {
97
                /** @var Promise */
98
                private $result;
99
100 16
                public function __construct(Promise $result)
101
                {
102 16
                    $this->result = $result;
103 16
                }
104
105 16
                public function doExecute() : Promise
106
                {
107 16
                    return $this->result;
108
                }
109
            };
110
        }
111
112 210
        return new self($exeContext);
113
    }
114
115
    /**
116
     * Constructs an ExecutionContext object from the arguments passed to
117
     * execute, which we will pass throughout the other execution methods.
118
     *
119
     * @param mixed               $rootValue
120
     * @param mixed               $contextValue
121
     * @param mixed[]|Traversable $rawVariableValues
122
     * @param string|null         $operationName
123
     *
124
     * @return ExecutionContext|Error[]
125
     */
126 225
    private static function buildExecutionContext(
127
        Schema $schema,
128
        DocumentNode $documentNode,
129
        $rootValue,
130
        $contextValue,
131
        $rawVariableValues,
132
        $operationName = null,
133
        ?callable $fieldResolver = null,
134
        ?PromiseAdapter $promiseAdapter = null
135
    ) {
136 225
        $errors    = [];
137 225
        $fragments = [];
138
        /** @var OperationDefinitionNode|null $operation */
139 225
        $operation                    = null;
140 225
        $hasMultipleAssumedOperations = false;
141 225
        foreach ($documentNode->definitions as $definition) {
142
            switch (true) {
143 225
                case $definition instanceof OperationDefinitionNode:
144 223
                    if ($operationName === null && $operation !== null) {
145 1
                        $hasMultipleAssumedOperations = true;
146
                    }
147 223
                    if ($operationName === null ||
148 223
                        (isset($definition->name) && $definition->name->value === $operationName)) {
149 222
                        $operation = $definition;
150
                    }
151 223
                    break;
152 18
                case $definition instanceof FragmentDefinitionNode:
153 17
                    $fragments[$definition->name->value] = $definition;
154 225
                    break;
155
            }
156
        }
157 225
        if ($operation === null) {
158 3
            if ($operationName === null) {
159 2
                $errors[] = new Error('Must provide an operation.');
160
            } else {
161 3
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
162
            }
163 222
        } elseif ($hasMultipleAssumedOperations) {
164 1
            $errors[] = new Error(
165 1
                'Must provide operation name if query contains multiple operations.'
166
            );
167
        }
168 225
        $variableValues = null;
169 225
        if ($operation !== null) {
170 222
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
171 222
                $schema,
172 222
                $operation->variableDefinitions ?: [],
173 222
                $rawVariableValues ?: []
0 ignored issues
show
Bug introduced by
It seems like $rawVariableValues ?: array() can also be of type Traversable; however, parameter $inputs of GraphQL\Executor\Values::getVariableValues() 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

173
                /** @scrutinizer ignore-type */ $rawVariableValues ?: []
Loading history...
174
            );
175 222
            if (empty($coercionErrors)) {
176 211
                $variableValues = $coercedVariableValues;
177
            } else {
178 12
                $errors = array_merge($errors, $coercionErrors);
179
            }
180
        }
181 225
        if (! empty($errors)) {
182 16
            return $errors;
183
        }
184 210
        Utils::invariant($operation, 'Has operation if no errors.');
185 210
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
186
187 210
        return new ExecutionContext(
188 210
            $schema,
189 210
            $fragments,
190 210
            $rootValue,
191 210
            $contextValue,
192 210
            $operation,
193 210
            $variableValues,
194 210
            $errors,
195 210
            $fieldResolver,
196 210
            $promiseAdapter
197
        );
198
    }
199
200 210
    public function doExecute() : Promise
201
    {
202
        // Return a Promise that will eventually resolve to the data described by
203
        // the "Response" section of the GraphQL specification.
204
        //
205
        // If errors are encountered while executing a GraphQL field, only that
206
        // field and its descendants will be omitted, and sibling fields will still
207
        // be executed. An execution which encounters errors will still result in a
208
        // resolved Promise.
209 210
        $data   = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
210 210
        $result = $this->buildResponse($data);
211
212
        // Note: we deviate here from the reference implementation a bit by always returning promise
213
        // But for the "sync" case it is always fulfilled
214 210
        return $this->isPromise($result)
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->isPromise(...reateFulfilled($result) could return the type GraphQL\Executor\ExecutionResult which is incompatible with the type-hinted return GraphQL\Executor\Promise\Promise. Consider adding an additional type-check to rule them out.
Loading history...
215 40
            ? $result
216 210
            : $this->exeContext->promiseAdapter->createFulfilled($result);
217
    }
218
219
    /**
220
     * @param mixed|Promise|null $data
221
     *
222
     * @return ExecutionResult|Promise
223
     */
224 210
    private function buildResponse($data)
225
    {
226 210
        if ($this->isPromise($data)) {
227
            return $data->then(function ($resolved) {
0 ignored issues
show
Bug introduced by
The method then() does not exist on null. ( Ignorable by Annotation )

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

227
            return $data->/** @scrutinizer ignore-call */ then(function ($resolved) {

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...
228 40
                return $this->buildResponse($resolved);
229 40
            });
230
        }
231 210
        if ($data !== null) {
232 206
            $data = (array) $data;
233
        }
234
235 210
        return new ExecutionResult($data, $this->exeContext->errors);
236
    }
237
238
    /**
239
     * Implements the "Evaluating operations" section of the spec.
240
     *
241
     * @param  mixed $rootValue
242
     *
243
     * @return Promise|stdClass|mixed[]
244
     */
245 210
    private function executeOperation(OperationDefinitionNode $operation, $rootValue)
246
    {
247 210
        $type   = $this->getOperationRootType($this->exeContext->schema, $operation);
248 210
        $fields = $this->collectFields($type, $operation->selectionSet, new ArrayObject(), new ArrayObject());
249 210
        $path   = [];
250
        // Errors from sub-fields of a NonNull type may propagate to the top level,
251
        // at which point we still log the error and null the parent field, which
252
        // in this case is the entire response.
253
        //
254
        // Similar to completeValueCatchingError.
255
        try {
256 210
            $result = $operation->operation === 'mutation'
257 6
                ? $this->executeFieldsSerially($type, $rootValue, $path, $fields)
258 210
                : $this->executeFields($type, $rootValue, $path, $fields);
259 208
            if ($this->isPromise($result)) {
260 40
                return $result->then(
0 ignored issues
show
Bug introduced by
The method then() does not exist on stdClass. ( Ignorable by Annotation )

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

260
                return $result->/** @scrutinizer ignore-call */ then(

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...
261 40
                    null,
262
                    function ($error) {
263 2
                        if ($error instanceof Error) {
264 2
                            $this->exeContext->addError($error);
265
266 2
                            return $this->exeContext->promiseAdapter->createFulfilled(null);
267
                        }
268 40
                    }
269
                );
270
            }
271
272 169
            return $result;
273 2
        } catch (Error $error) {
274 2
            $this->exeContext->addError($error);
275
276 2
            return null;
277
        }
278
    }
279
280
    /**
281
     * Extracts the root type of the operation from the schema.
282
     *
283
     * @return ObjectType
284
     *
285
     * @throws Error
286
     */
287 210
    private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation)
288
    {
289 210
        switch ($operation->operation) {
290 210
            case 'query':
291 202
                $queryType = $schema->getQueryType();
292 202
                if ($queryType === null) {
293
                    throw new Error(
294
                        'Schema does not define the required query root type.',
295
                        [$operation]
296
                    );
297
                }
298
299 202
                return $queryType;
300 8
            case 'mutation':
301 6
                $mutationType = $schema->getMutationType();
302 6
                if ($mutationType === null) {
303
                    throw new Error(
304
                        'Schema is not configured for mutations.',
305
                        [$operation]
306
                    );
307
                }
308
309 6
                return $mutationType;
310 2
            case 'subscription':
311 2
                $subscriptionType = $schema->getSubscriptionType();
312 2
                if ($subscriptionType === null) {
313
                    throw new Error(
314
                        'Schema is not configured for subscriptions.',
315
                        [$operation]
316
                    );
317
                }
318
319 2
                return $subscriptionType;
320
            default:
321
                throw new Error(
322
                    'Can only execute queries, mutations and subscriptions.',
323
                    [$operation]
324
                );
325
        }
326
    }
327
328
    /**
329
     * Given a selectionSet, adds all of the fields in that selection to
330
     * the passed in map of fields, and returns it at the end.
331
     *
332
     * CollectFields requires the "runtime type" of an object. For a field which
333
     * returns an Interface or Union type, the "runtime type" will be the actual
334
     * Object type returned by that field.
335
     *
336
     * @param ArrayObject $fields
337
     * @param ArrayObject $visitedFragmentNames
338
     *
339
     * @return ArrayObject
340
     */
341 210
    private function collectFields(
342
        ObjectType $runtimeType,
343
        SelectionSetNode $selectionSet,
344
        $fields,
345
        $visitedFragmentNames
346
    ) {
347 210
        $exeContext = $this->exeContext;
348 210
        foreach ($selectionSet->selections as $selection) {
349
            switch (true) {
350 210
                case $selection instanceof FieldNode:
351 210
                    if (! $this->shouldIncludeNode($selection)) {
352 2
                        break;
353
                    }
354 210
                    $name = self::getFieldEntryKey($selection);
355 210
                    if (! isset($fields[$name])) {
356 210
                        $fields[$name] = new ArrayObject();
357
                    }
358 210
                    $fields[$name][] = $selection;
359 210
                    break;
360 31
                case $selection instanceof InlineFragmentNode:
361 22
                    if (! $this->shouldIncludeNode($selection) ||
362 22
                        ! $this->doesFragmentConditionMatch($selection, $runtimeType)
363
                    ) {
364 20
                        break;
365
                    }
366 22
                    $this->collectFields(
367 22
                        $runtimeType,
368 22
                        $selection->selectionSet,
369 22
                        $fields,
370 22
                        $visitedFragmentNames
371
                    );
372 22
                    break;
373 11
                case $selection instanceof FragmentSpreadNode:
374 11
                    $fragName = $selection->name->value;
375 11
                    if (! empty($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) {
376 2
                        break;
377
                    }
378 11
                    $visitedFragmentNames[$fragName] = true;
379
                    /** @var FragmentDefinitionNode|null $fragment */
380 11
                    $fragment = $exeContext->fragments[$fragName] ?? null;
381 11
                    if ($fragment === null || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
382
                        break;
383
                    }
384 11
                    $this->collectFields(
385 11
                        $runtimeType,
386 11
                        $fragment->selectionSet,
387 11
                        $fields,
388 11
                        $visitedFragmentNames
389
                    );
390 210
                    break;
391
            }
392
        }
393
394 210
        return $fields;
395
    }
396
397
    /**
398
     * Determines if a field should be included based on the @include and @skip
399
     * directives, where @skip has higher precedence than @include.
400
     *
401
     * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
402
     */
403 210
    private function shouldIncludeNode($node) : bool
404
    {
405 210
        $variableValues = $this->exeContext->variableValues;
406 210
        $skipDirective  = Directive::skipDirective();
407 210
        $skip           = Values::getDirectiveValues(
408 210
            $skipDirective,
409 210
            $node,
410 210
            $variableValues
411
        );
412 210
        if (isset($skip['if']) && $skip['if'] === true) {
413 5
            return false;
414
        }
415 210
        $includeDirective = Directive::includeDirective();
416 210
        $include          = Values::getDirectiveValues(
417 210
            $includeDirective,
418 210
            $node,
419 210
            $variableValues
420
        );
421
422 210
        return ! isset($include['if']) || $include['if'] !== false;
423
    }
424
425
    /**
426
     * Implements the logic to compute the key of a given fields entry
427
     */
428 210
    private static function getFieldEntryKey(FieldNode $node) : string
429
    {
430 210
        return $node->alias === null ? $node->name->value : $node->alias->value;
431
    }
432
433
    /**
434
     * Determines if a fragment is applicable to the given type.
435
     *
436
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
437
     *
438
     * @return bool
439
     */
440 31
    private function doesFragmentConditionMatch(
441
        $fragment,
442
        ObjectType $type
443
    ) {
444 31
        $typeConditionNode = $fragment->typeCondition;
445 31
        if ($typeConditionNode === null) {
446 1
            return true;
447
        }
448 30
        $conditionalType = TypeInfo::typeFromAST($this->exeContext->schema, $typeConditionNode);
449 30
        if ($conditionalType === $type) {
0 ignored issues
show
introduced by
The condition $conditionalType === $type is always false.
Loading history...
450 30
            return true;
451
        }
452 18
        if ($conditionalType instanceof AbstractType) {
453 1
            return $this->exeContext->schema->isPossibleType($conditionalType, $type);
454
        }
455
456 18
        return false;
457
    }
458
459
    /**
460
     * Implements the "Evaluating selection sets" section of the spec
461
     * for "write" mode.
462
     *
463
     * @param mixed       $rootValue
464
     * @param mixed[]     $path
465
     * @param ArrayObject $fields
466
     *
467
     * @return Promise|stdClass|mixed[]
468
     */
469 6
    private function executeFieldsSerially(ObjectType $parentType, $rootValue, $path, $fields)
470
    {
471 6
        $result = $this->promiseReduce(
472 6
            array_keys($fields->getArrayCopy()),
473
            function ($results, $responseName) use ($path, $parentType, $rootValue, $fields) {
474 6
                $fieldNodes  = $fields[$responseName];
475 6
                $fieldPath   = $path;
476 6
                $fieldPath[] = $responseName;
477 6
                $result      = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
478 6
                if ($result === self::$UNDEFINED) {
479 1
                    return $results;
480
                }
481 5
                $promise = $this->getPromise($result);
482 5
                if ($promise !== null) {
483
                    return $promise->then(static function ($resolvedResult) use ($responseName, $results) {
484 2
                        $results[$responseName] = $resolvedResult;
485
486 2
                        return $results;
487 2
                    });
488
                }
489 5
                $results[$responseName] = $result;
490
491 5
                return $results;
492 6
            },
493 6
            []
494
        );
495
496 6
        if ($this->isPromise($result)) {
497
            return $result->then(static function ($resolvedResults) {
498 2
                return self::fixResultsIfEmptyArray($resolvedResults);
499 2
            });
500
        }
501
502 4
        return self::fixResultsIfEmptyArray($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type GraphQL\Executor\Promise\Promise; however, parameter $results of GraphQL\Executor\Referen...ixResultsIfEmptyArray() 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

502
        return self::fixResultsIfEmptyArray(/** @scrutinizer ignore-type */ $result);
Loading history...
503
    }
504
505
    /**
506
     * Resolves the field on the given root value.
507
     *
508
     * In particular, this figures out the value that the field returns
509
     * by calling its resolve function, then calls completeValue to complete promises,
510
     * serialize scalars, or execute the sub-selection-set for objects.
511
     *
512
     * @param mixed       $rootValue
513
     * @param FieldNode[] $fieldNodes
514
     * @param mixed[]     $path
515
     *
516
     * @return mixed[]|Exception|mixed|null
517
     */
518 210
    private function resolveField(ObjectType $parentType, $rootValue, $fieldNodes, $path)
519
    {
520 210
        $exeContext = $this->exeContext;
521 210
        $fieldNode  = $fieldNodes[0];
522 210
        $fieldName  = $fieldNode->name->value;
523 210
        $fieldDef   = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
524 210
        if ($fieldDef === null) {
525 7
            return self::$UNDEFINED;
526
        }
527 206
        $returnType = $fieldDef->getType();
528
        // The resolve function's optional 3rd argument is a context value that
529
        // is provided to every resolve function within an execution. It is commonly
530
        // used to represent an authenticated user, or request-specific caches.
531 206
        $context = $exeContext->contextValue;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
532
        // The resolve function's optional 4th argument is a collection of
533
        // information about the current execution state.
534 206
        $info = new ResolveInfo(
535 206
            $fieldName,
536 206
            $fieldNodes,
537 206
            $returnType,
538 206
            $parentType,
539 206
            $path,
540 206
            $exeContext->schema,
541 206
            $exeContext->fragments,
542 206
            $exeContext->rootValue,
543 206
            $exeContext->operation,
544 206
            $exeContext->variableValues
545
        );
546 206
        if ($fieldDef->resolveFn !== null) {
547 154
            $resolveFn = $fieldDef->resolveFn;
548 99
        } elseif ($parentType->resolveFieldFn !== null) {
549
            $resolveFn = $parentType->resolveFieldFn;
550
        } else {
551 99
            $resolveFn = $this->exeContext->fieldResolver;
552
        }
553
        // Get the resolve function, regardless of if its result is normal
554
        // or abrupt (error).
555 206
        $result = $this->resolveFieldValueOrError(
556 206
            $fieldDef,
557 206
            $fieldNode,
558 206
            $resolveFn,
559 206
            $rootValue,
560 206
            $info
561
        );
562 206
        $result = $this->completeValueCatchingError(
563 206
            $returnType,
564 206
            $fieldNodes,
565 206
            $info,
566 206
            $path,
567 206
            $result
568
        );
569
570 204
        return $result;
571
    }
572
573
    /**
574
     * This method looks up the field on the given type definition.
575
     *
576
     * It has special casing for the two introspection fields, __schema
577
     * and __typename. __typename is special because it can always be
578
     * queried as a field, even in situations where no other fields
579
     * are allowed, like on a Union. __schema could get automatically
580
     * added to the query type, but that would require mutating type
581
     * definitions, which would cause issues.
582
     */
583 210
    private function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
584
    {
585 210
        static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
586 210
        $schemaMetaFieldDef   = $schemaMetaFieldDef ?: Introspection::schemaMetaFieldDef();
587 210
        $typeMetaFieldDef     = $typeMetaFieldDef ?: Introspection::typeMetaFieldDef();
588 210
        $typeNameMetaFieldDef = $typeNameMetaFieldDef ?: Introspection::typeNameMetaFieldDef();
589 210
        if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
590 6
            return $schemaMetaFieldDef;
591
        }
592
593 210
        if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
594 15
            return $typeMetaFieldDef;
595
        }
596
597 210
        if ($fieldName === $typeNameMetaFieldDef->name) {
598 7
            return $typeNameMetaFieldDef;
599
        }
600 210
        $tmp = $parentType->getFields();
601
602 210
        return $tmp[$fieldName] ?? null;
603
    }
604
605
    /**
606
     * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function.
607
     * Returns the result of resolveFn or the abrupt-return Error object.
608
     *
609
     * @param FieldDefinition $fieldDef
610
     * @param FieldNode       $fieldNode
611
     * @param callable        $resolveFn
612
     * @param mixed           $rootValue
613
     * @param ResolveInfo     $info
614
     *
615
     * @return Throwable|Promise|mixed
616
     */
617 206
    private function resolveFieldValueOrError($fieldDef, $fieldNode, $resolveFn, $rootValue, $info)
618
    {
619
        try {
620
            // Build a map of arguments from the field.arguments AST, using the
621
            // variables scope to fulfill any variable references.
622 206
            $args         = Values::getArgumentValues(
623 206
                $fieldDef,
624 206
                $fieldNode,
625 206
                $this->exeContext->variableValues
626
            );
627 199
            $contextValue = $this->exeContext->contextValue;
628
629 199
            return $resolveFn($rootValue, $args, $contextValue, $info);
630 21
        } catch (Exception $error) {
631 21
            return $error;
632
        } catch (Throwable $error) {
633
            return $error;
634
        }
635
    }
636
637
    /**
638
     * This is a small wrapper around completeValue which detects and logs errors
639
     * in the execution context.
640
     *
641
     * @param FieldNode[] $fieldNodes
642
     * @param string[]    $path
643
     * @param mixed       $result
644
     *
645
     * @return mixed[]|Promise|null
646
     */
647 206
    private function completeValueCatchingError(
648
        Type $returnType,
649
        $fieldNodes,
650
        ResolveInfo $info,
651
        $path,
652
        $result
653
    ) {
654
        // Otherwise, error protection is applied, logging the error and resolving
655
        // a null value for this field if one is encountered.
656
        try {
657 206
            $promise = $this->getPromise($result);
658 206
            if ($promise !== null) {
659
                $completed = $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
660 31
                    return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
661 34
                });
662
            } else {
663 198
                $completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $result);
664
            }
665
666 187
            $promise = $this->getPromise($completed);
667 187
            if ($promise !== null) {
668
                return $promise->then(null, function ($error) use ($fieldNodes, $path, $returnType) {
669 26
                    return $this->handleFieldError($error, $fieldNodes, $path, $returnType);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleFieldError(...es, $path, $returnType) targeting GraphQL\Executor\Referen...tor::handleFieldError() 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...
670 40
                });
671
            }
672
673 166
            return $completed;
674 41
        } catch (Throwable $err) {
675 41
            return $this->handleFieldError($err, $fieldNodes, $path, $returnType);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleFieldError(...es, $path, $returnType) targeting GraphQL\Executor\Referen...tor::handleFieldError() 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...
676
        }
677
    }
678
679 57
    private function handleFieldError($rawError, $fieldNodes, $path, $returnType)
680
    {
681 57
        $error = Error::createLocatedError(
682 57
            $rawError,
683 57
            $fieldNodes,
684 57
            $path
685
        );
686
687
        // If the field type is non-nullable, then it is resolved without any
688
        // protection from errors, however it still properly locates the error.
689 57
        if ($returnType instanceof NonNull) {
690 22
            throw $error;
691
        }
692
        // Otherwise, error protection is applied, logging the error and resolving
693
        // a null value for this field if one is encountered.
694 53
        $this->exeContext->addError($error);
695
696 53
        return null;
697
    }
698
699
    /**
700
     * Implements the instructions for completeValue as defined in the
701
     * "Field entries" section of the spec.
702
     *
703
     * If the field type is Non-Null, then this recursively completes the value
704
     * for the inner type. It throws a field error if that completion returns null,
705
     * as per the "Nullability" section of the spec.
706
     *
707
     * If the field type is a List, then this recursively completes the value
708
     * for the inner type on each item in the list.
709
     *
710
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
711
     * value of the type by calling the `serialize` method of GraphQL type
712
     * definition.
713
     *
714
     * If the field is an abstract type, determine the runtime type of the value
715
     * and then complete based on that type
716
     *
717
     * Otherwise, the field type expects a sub-selection set, and will complete the
718
     * value by evaluating all sub-selections.
719
     *
720
     * @param FieldNode[] $fieldNodes
721
     * @param string[]    $path
722
     * @param mixed       $result
723
     *
724
     * @return mixed[]|mixed|Promise|null
725
     *
726
     * @throws Error
727
     * @throws Throwable
728
     */
729 204
    private function completeValue(
730
        Type $returnType,
731
        $fieldNodes,
732
        ResolveInfo $info,
733
        $path,
734
        &$result
735
    ) {
736
        // If result is an Error, throw a located error.
737 204
        if ($result instanceof Throwable) {
738 21
            throw $result;
739
        }
740
741
        // If field type is NonNull, complete for inner type, and throw field error
742
        // if result is null.
743 192
        if ($returnType instanceof NonNull) {
744 48
            $completed = $this->completeValue(
745 48
                $returnType->getWrappedType(),
746 48
                $fieldNodes,
747 48
                $info,
748 48
                $path,
749 48
                $result
750
            );
751 48
            if ($completed === null) {
752 15
                throw new InvariantViolation(
753 15
                    'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.'
754
                );
755
            }
756
757 42
            return $completed;
758
        }
759
        // If result is null-like, return null.
760 192
        if ($result === null) {
761 49
            return null;
762
        }
763
        // If field type is List, complete each item in the list with the inner type
764 174
        if ($returnType instanceof ListOfType) {
765 60
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
766
        }
767
        // Account for invalid schema definition when typeLoader returns different
768
        // instance than `resolveType` or $field->getType() or $arg->getType()
769 173
        if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
770 1
            $hint = '';
771 1
            if ($this->exeContext->schema->getConfig()->typeLoader !== null) {
772 1
                $hint = sprintf(
773 1
                    'Make sure that type loader returns the same instance as defined in %s.%s',
774 1
                    $info->parentType,
775 1
                    $info->fieldName
776
                );
777
            }
778 1
            throw new InvariantViolation(
779 1
                sprintf(
780
                    'Schema must contain unique named types but contains multiple types named "%s". %s ' .
781 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
782 1
                    $returnType,
783 1
                    $hint
784
                )
785
            );
786
        }
787
        // If field type is Scalar or Enum, serialize to a valid value, returning
788
        // null if serialization is not possible.
789 172
        if ($returnType instanceof LeafType) {
790 156
            return $this->completeLeafValue($returnType, $result);
791
        }
792 96
        if ($returnType instanceof AbstractType) {
793 33
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
794
        }
795
        // Field type must be Object, Interface or Union and expect sub-selections.
796 64
        if ($returnType instanceof ObjectType) {
797 64
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
798
        }
799
        throw new RuntimeException(sprintf('Cannot complete value of unexpected type "%s".', $returnType));
800
    }
801
802
    /**
803
     * @param mixed $value
804
     *
805
     * @return bool
806
     */
807 210
    private function isPromise($value)
808
    {
809 210
        return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
810
    }
811
812
    /**
813
     * Only returns the value if it acts like a Promise, i.e. has a "then" function,
814
     * otherwise returns null.
815
     *
816
     * @param mixed $value
817
     *
818
     * @return Promise|null
819
     */
820 207
    private function getPromise($value)
821
    {
822 207
        if ($value === null || $value instanceof Promise) {
823 77
            return $value;
824
        }
825 190
        if ($this->exeContext->promiseAdapter->isThenable($value)) {
826 39
            $promise = $this->exeContext->promiseAdapter->convertThenable($value);
827 39
            if (! $promise instanceof Promise) {
0 ignored issues
show
introduced by
$promise is always a sub-type of GraphQL\Executor\Promise\Promise.
Loading history...
828
                throw new InvariantViolation(sprintf(
829
                    '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
830
                    get_class($this->exeContext->promiseAdapter),
831
                    Utils::printSafe($promise)
832
                ));
833
            }
834
835 39
            return $promise;
836
        }
837
838 182
        return null;
839
    }
840
841
    /**
842
     * Similar to array_reduce(), however the reducing callback may return
843
     * a Promise, in which case reduction will continue after each promise resolves.
844
     *
845
     * If the callback does not return a Promise, then this function will also not
846
     * return a Promise.
847
     *
848
     * @param mixed[]            $values
849
     * @param Promise|mixed|null $initialValue
850
     *
851
     * @return Promise|mixed|null
852
     */
853 6
    private function promiseReduce(array $values, callable $callback, $initialValue)
854
    {
855 6
        return array_reduce(
856 6
            $values,
857
            function ($previous, $value) use ($callback) {
858 6
                $promise = $this->getPromise($previous);
859 6
                if ($promise !== null) {
860
                    return $promise->then(static function ($resolved) use ($callback, $value) {
861 2
                        return $callback($resolved, $value);
862 2
                    });
863
                }
864
865 6
                return $callback($previous, $value);
866 6
            },
867 6
            $initialValue
868
        );
869
    }
870
871
    /**
872
     * Complete a list value by completing each item in the list with the inner type.
873
     *
874
     * @param FieldNode[]         $fieldNodes
875
     * @param mixed[]             $path
876
     * @param mixed[]|Traversable $results
877
     *
878
     * @return mixed[]|Promise
879
     *
880
     * @throws Exception
881
     */
882 60
    private function completeListValue(ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$results)
883
    {
884 60
        $itemType = $returnType->getWrappedType();
885 60
        Utils::invariant(
886 60
            is_array($results) || $results instanceof Traversable,
887 60
            'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
888
        );
889 60
        $containsPromise = false;
890 60
        $i               = 0;
891 60
        $completedItems  = [];
892 60
        foreach ($results as $item) {
893 59
            $fieldPath     = $path;
894 59
            $fieldPath[]   = $i++;
895 59
            $info->path    = $fieldPath;
0 ignored issues
show
Documentation Bug introduced by
$fieldPath is of type array<mixed,integer|mixed>, but the property $path was declared to be of type array<mixed,string[]>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
896 59
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
897 59
            if (! $containsPromise && $this->getPromise($completedItem) !== null) {
898 13
                $containsPromise = true;
899
            }
900 59
            $completedItems[] = $completedItem;
901
        }
902
903 60
        return $containsPromise
904 13
            ? $this->exeContext->promiseAdapter->all($completedItems)
905 60
            : $completedItems;
906
    }
907
908
    /**
909
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
910
     *
911
     * @param  mixed $result
912
     *
913
     * @return mixed
914
     *
915
     * @throws Exception
916
     */
917 156
    private function completeLeafValue(LeafType $returnType, &$result)
918
    {
919
        try {
920 156
            return $returnType->serialize($result);
921 3
        } catch (Exception $error) {
922 3
            throw new InvariantViolation(
923 3
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
924 3
                0,
925 3
                $error
926
            );
927
        } catch (Throwable $error) {
928
            throw new InvariantViolation(
929
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
930
                0,
931
                $error
932
            );
933
        }
934
    }
935
936
    /**
937
     * Complete a value of an abstract type by determining the runtime object type
938
     * of that value, then complete the value for that type.
939
     *
940
     * @param FieldNode[] $fieldNodes
941
     * @param mixed[]     $path
942
     * @param mixed[]     $result
943
     *
944
     * @return mixed
945
     *
946
     * @throws Error
947
     */
948 33
    private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
949
    {
950 33
        $exeContext  = $this->exeContext;
951 33
        $runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
0 ignored issues
show
Bug introduced by
$result of type array<mixed,mixed> is incompatible with the type object expected by parameter $objectValue of GraphQL\Type\Definition\...ractType::resolveType(). ( Ignorable by Annotation )

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

951
        $runtimeType = $returnType->resolveType(/** @scrutinizer ignore-type */ $result, $exeContext->contextValue, $info);
Loading history...
952 33
        if ($runtimeType === null) {
953 11
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
0 ignored issues
show
Bug Best Practice introduced by
The method GraphQL\Executor\Referen...::defaultTypeResolver() is not static, but was called statically. ( Ignorable by Annotation )

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

953
            /** @scrutinizer ignore-call */ 
954
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
954
        }
955 33
        $promise = $this->getPromise($runtimeType);
956 33
        if ($promise !== null) {
957
            return $promise->then(function ($resolvedRuntimeType) use (
958 5
                $returnType,
959 5
                $fieldNodes,
960 5
                $info,
961 5
                $path,
962 5
                &$result
963
            ) {
964 5
                return $this->completeObjectValue(
965 5
                    $this->ensureValidRuntimeType(
966 5
                        $resolvedRuntimeType,
967 5
                        $returnType,
968 5
                        $info,
969 5
                        $result
970
                    ),
971 5
                    $fieldNodes,
972 5
                    $info,
973 5
                    $path,
974 5
                    $result
975
                );
976 7
            });
977
        }
978
979 26
        return $this->completeObjectValue(
980 26
            $this->ensureValidRuntimeType(
981 26
                $runtimeType,
0 ignored issues
show
Bug introduced by
It seems like $runtimeType can also be of type GraphQL\Executor\Promise\Promise; however, parameter $runtimeTypeOrName of GraphQL\Executor\Referen...nsureValidRuntimeType() does only seem to accept GraphQL\Type\Definition\ObjectType|null|string, 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

981
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
982 26
                $returnType,
983 26
                $info,
984 26
                $result
985
            ),
986 24
            $fieldNodes,
987 24
            $info,
988 24
            $path,
989 24
            $result
990
        );
991
    }
992
993
    /**
994
     * If a resolveType function is not given, then a default resolve behavior is
995
     * used which attempts two strategies:
996
     *
997
     * First, See if the provided value has a `__typename` field defined, if so, use
998
     * that value as name of the resolved type.
999
     *
1000
     * Otherwise, test each possible type for the abstract type by calling
1001
     * isTypeOf for the object being coerced, returning the first type that matches.
1002
     *
1003
     * @param mixed|null              $value
1004
     * @param mixed|null              $contextValue
1005
     * @param InterfaceType|UnionType $abstractType
1006
     *
1007
     * @return ObjectType|Promise|null
1008
     */
1009 11
    private function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
1010
    {
1011
        // First, look for `__typename`.
1012 11
        if ($value !== null &&
1013 11
            (is_array($value) || $value instanceof ArrayAccess) &&
1014 11
            isset($value['__typename']) &&
1015 11
            is_string($value['__typename'])
1016
        ) {
1017 2
            return $value['__typename'];
1018
        }
1019
1020 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader !== null) {
1021 1
            Warning::warnOnce(
1022 1
                sprintf(
1023
                    'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
1024
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1025
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1026 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1027 1
                    $abstractType->name,
1028 1
                    Utils::printSafe($value)
1029
                ),
1030 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1031
            );
1032
        }
1033
        // Otherwise, test each possible type.
1034 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1035 9
        $promisedIsTypeOfResults = [];
1036 9
        foreach ($possibleTypes as $index => $type) {
1037 9
            $isTypeOfResult = $type->isTypeOf($value, $contextValue, $info);
1038 9
            if ($isTypeOfResult === null) {
1039
                continue;
1040
            }
1041 9
            $promise = $this->getPromise($isTypeOfResult);
1042 9
            if ($promise !== null) {
1043 3
                $promisedIsTypeOfResults[$index] = $promise;
1044 6
            } elseif ($isTypeOfResult) {
1045 9
                return $type;
1046
            }
1047
        }
1048 3
        if (! empty($promisedIsTypeOfResults)) {
1049 3
            return $this->exeContext->promiseAdapter->all($promisedIsTypeOfResults)
1050
                ->then(static function ($isTypeOfResults) use ($possibleTypes) {
1051 2
                    foreach ($isTypeOfResults as $index => $result) {
1052 2
                        if ($result) {
1053 2
                            return $possibleTypes[$index];
1054
                        }
1055
                    }
1056
1057
                    return null;
1058 3
                });
1059
        }
1060
1061
        return null;
1062
    }
1063
1064
    /**
1065
     * Complete an Object value by executing all sub-selections.
1066
     *
1067
     * @param FieldNode[] $fieldNodes
1068
     * @param mixed[]     $path
1069
     * @param mixed       $result
1070
     *
1071
     * @return mixed[]|Promise|stdClass
1072
     *
1073
     * @throws Error
1074
     */
1075 92
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1076
    {
1077
        // If there is an isTypeOf predicate function, call it with the
1078
        // current result. If isTypeOf returns false, then raise an error rather
1079
        // than continuing execution.
1080 92
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1081 92
        if ($isTypeOf !== null) {
1082 11
            $promise = $this->getPromise($isTypeOf);
1083 11
            if ($promise !== null) {
1084
                return $promise->then(function ($isTypeOfResult) use (
1085 2
                    $returnType,
1086 2
                    $fieldNodes,
1087 2
                    $path,
1088 2
                    &$result
1089
                ) {
1090 2
                    if (! $isTypeOfResult) {
1091
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1092
                    }
1093
1094 2
                    return $this->collectAndExecuteSubfields(
1095 2
                        $returnType,
1096 2
                        $fieldNodes,
1097 2
                        $path,
1098 2
                        $result
1099
                    );
1100 2
                });
1101
            }
1102 9
            if (! $isTypeOf) {
1103 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1104
            }
1105
        }
1106
1107 90
        return $this->collectAndExecuteSubfields(
1108 90
            $returnType,
1109 90
            $fieldNodes,
1110 90
            $path,
1111 90
            $result
1112
        );
1113
    }
1114
1115
    /**
1116
     * @param mixed[]     $result
1117
     * @param FieldNode[] $fieldNodes
1118
     *
1119
     * @return Error
1120
     */
1121 1
    private function invalidReturnTypeError(
1122
        ObjectType $returnType,
1123
        $result,
1124
        $fieldNodes
1125
    ) {
1126 1
        return new Error(
1127 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1128 1
            $fieldNodes
1129
        );
1130
    }
1131
1132
    /**
1133
     * @param FieldNode[] $fieldNodes
1134
     * @param mixed[]     $path
1135
     * @param mixed       $result
1136
     *
1137
     * @return mixed[]|Promise|stdClass
1138
     *
1139
     * @throws Error
1140
     */
1141 92
    private function collectAndExecuteSubfields(
1142
        ObjectType $returnType,
1143
        $fieldNodes,
1144
        $path,
1145
        &$result
1146
    ) {
1147 92
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
0 ignored issues
show
Bug introduced by
$fieldNodes of type GraphQL\Language\AST\FieldNode[] is incompatible with the type object expected by parameter $fieldNodes of GraphQL\Executor\Referen...tor::collectSubFields(). ( Ignorable by Annotation )

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

1147
        $subFieldNodes = $this->collectSubFields($returnType, /** @scrutinizer ignore-type */ $fieldNodes);
Loading history...
1148
1149 92
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1150
    }
1151
1152
    /**
1153
     * A memoized collection of relevant subfields with regard to the return
1154
     * type. Memoizing ensures the subfields are not repeatedly calculated, which
1155
     * saves overhead when resolving lists of values.
1156
     *
1157
     * @param object $fieldNodes
1158
     */
1159 92
    private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayObject
1160
    {
1161 92
        if (! isset($this->subFieldCache[$returnType])) {
1162 92
            $this->subFieldCache[$returnType] = new SplObjectStorage();
1163
        }
1164 92
        if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
1165
            // Collect sub-fields to execute to complete this value.
1166 92
            $subFieldNodes        = new ArrayObject();
1167 92
            $visitedFragmentNames = new ArrayObject();
1168 92
            foreach ($fieldNodes as $fieldNode) {
1169 92
                if (! isset($fieldNode->selectionSet)) {
1170
                    continue;
1171
                }
1172 92
                $subFieldNodes = $this->collectFields(
1173 92
                    $returnType,
1174 92
                    $fieldNode->selectionSet,
1175 92
                    $subFieldNodes,
1176 92
                    $visitedFragmentNames
1177
                );
1178
            }
1179 92
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
1180
        }
1181
1182 92
        return $this->subFieldCache[$returnType][$fieldNodes];
1183
    }
1184
1185
    /**
1186
     * Implements the "Evaluating selection sets" section of the spec
1187
     * for "read" mode.
1188
     *
1189
     * @param mixed       $rootValue
1190
     * @param mixed[]     $path
1191
     * @param ArrayObject $fields
1192
     *
1193
     * @return Promise|stdClass|mixed[]
1194
     */
1195 206
    private function executeFields(ObjectType $parentType, $rootValue, $path, $fields)
1196
    {
1197 206
        $containsPromise = false;
1198 206
        $results         = [];
1199 206
        foreach ($fields as $responseName => $fieldNodes) {
1200 206
            $fieldPath   = $path;
1201 206
            $fieldPath[] = $responseName;
1202 206
            $result      = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
1203 204
            if ($result === self::$UNDEFINED) {
1204 6
                continue;
1205
            }
1206 201
            if (! $containsPromise && $this->isPromise($result)) {
1207 38
                $containsPromise = true;
1208
            }
1209 201
            $results[$responseName] = $result;
1210
        }
1211
        // If there are no promises, we can just return the object
1212 204
        if (! $containsPromise) {
1213 175
            return self::fixResultsIfEmptyArray($results);
1214
        }
1215
1216
        // Otherwise, results is a map from field name to the result of resolving that
1217
        // field, which is possibly a promise. Return a promise that will return this
1218
        // same map, but with any promises replaced with the values they resolved to.
1219 38
        return $this->promiseForAssocArray($results);
1220
    }
1221
1222
    /**
1223
     * @see https://github.com/webonyx/graphql-php/issues/59
1224
     *
1225
     * @param mixed[] $results
1226
     *
1227
     * @return stdClass|mixed[]
1228
     */
1229 206
    private static function fixResultsIfEmptyArray($results)
1230
    {
1231 206
        if ($results === []) {
1232 5
            return new stdClass();
1233
        }
1234
1235 202
        return $results;
1236
    }
1237
1238
    /**
1239
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1240
     * a `Promise<array<key,scalar|array>>`
1241
     *
1242
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1243
     * any promises.
1244
     *
1245
     * @param (string|Promise)[] $assoc
1246
     *
1247
     * @return mixed
1248
     */
1249 38
    private function promiseForAssocArray(array $assoc)
1250
    {
1251 38
        $keys              = array_keys($assoc);
1252 38
        $valuesAndPromises = array_values($assoc);
1253 38
        $promise           = $this->exeContext->promiseAdapter->all($valuesAndPromises);
1254
1255
        return $promise->then(static function ($values) use ($keys) {
1256 36
            $resolvedResults = [];
1257 36
            foreach ($values as $i => $value) {
1258 36
                $resolvedResults[$keys[$i]] = $value;
1259
            }
1260
1261 36
            return self::fixResultsIfEmptyArray($resolvedResults);
1262 38
        });
1263
    }
1264
1265
    /**
1266
     * @param string|ObjectType|null  $runtimeTypeOrName
1267
     * @param InterfaceType|UnionType $returnType
1268
     * @param mixed                   $result
1269
     *
1270
     * @return ObjectType
1271
     */
1272 31
    private function ensureValidRuntimeType(
1273
        $runtimeTypeOrName,
1274
        AbstractType $returnType,
1275
        ResolveInfo $info,
1276
        &$result
1277
    ) {
1278 31
        $runtimeType = is_string($runtimeTypeOrName)
1279 4
            ? $this->exeContext->schema->getType($runtimeTypeOrName)
1280 31
            : $runtimeTypeOrName;
1281 31
        if (! $runtimeType instanceof ObjectType) {
1282 1
            throw new InvariantViolation(
1283 1
                sprintf(
1284
                    'Abstract type %s must resolve to an Object type at ' .
1285
                    'runtime for field %s.%s with value "%s", received "%s". ' .
1286
                    'Either the %s type should provide a "resolveType" ' .
1287 1
                    'function or each possible type should provide an "isTypeOf" function.',
1288 1
                    $returnType,
0 ignored issues
show
Bug introduced by
$returnType of type GraphQL\Type\Definition\AbstractType is incompatible with the type string expected by parameter $args of sprintf(). ( Ignorable by Annotation )

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

1288
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1289 1
                    $info->parentType,
1290 1
                    $info->fieldName,
1291 1
                    Utils::printSafe($result),
1292 1
                    Utils::printSafe($runtimeType),
1293 1
                    $returnType
1294
                )
1295
            );
1296
        }
1297 30
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1298 4
            throw new InvariantViolation(
1299 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1300
            );
1301
        }
1302 30
        if ($runtimeType !== $this->exeContext->schema->getType($runtimeType->name)) {
0 ignored issues
show
introduced by
The condition $runtimeType !== $this->...ype($runtimeType->name) is always true.
Loading history...
1303 1
            throw new InvariantViolation(
1304 1
                sprintf(
1305
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1306
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1307
                    'type instance as referenced anywhere else within the schema ' .
1308 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1309 1
                    $runtimeType,
1310 1
                    $returnType
1311
                )
1312
            );
1313
        }
1314
1315 29
        return $runtimeType;
1316
    }
1317
}
1318