Passed
Push — master ( 8435c3...5821ca )
by Vladimir
10:05 queued 11s
created

ReferenceExecutor::buildExecutionContext()

Size

Total Lines 71
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 48
dl 0
loc 71
ccs 47
cts 47
cp 1
c 0
b 0
f 0
nc 168
nop 8

How to fix   Long Method    Many Parameters   

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:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\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\Introspection;
34
use GraphQL\Type\Schema;
35
use GraphQL\Utils\TypeInfo;
36
use GraphQL\Utils\Utils;
37
use RuntimeException;
38
use SplObjectStorage;
39
use stdClass;
40
use Throwable;
41
use Traversable;
42
use function array_keys;
43
use function array_merge;
44
use function array_reduce;
45
use function array_values;
46
use function get_class;
47
use function is_array;
48
use function is_object;
49
use function is_string;
50
use function sprintf;
51
52
class ReferenceExecutor implements ExecutorImplementation
53
{
54
    /** @var object */
55
    private static $UNDEFINED;
56
57
    /** @var ExecutionContext */
58
    private $exeContext;
59
60
    /** @var SplObjectStorage */
61
    private $subFieldCache;
62
63 197
    private function __construct(ExecutionContext $context)
64
    {
65 197
        if (! self::$UNDEFINED) {
66 1
            self::$UNDEFINED = Utils::undefined();
67
        }
68 197
        $this->exeContext    = $context;
69 197
        $this->subFieldCache = new SplObjectStorage();
70 197
    }
71
72 211
    public static function create(
73
        PromiseAdapter $promiseAdapter,
74
        Schema $schema,
75
        DocumentNode $documentNode,
76
        $rootValue,
77
        $contextValue,
78
        $variableValues,
79
        ?string $operationName,
80
        callable $fieldResolver
81
    ) {
82 211
        $exeContext = self::buildExecutionContext(
83 211
            $schema,
84 211
            $documentNode,
85 211
            $rootValue,
86 211
            $contextValue,
87 211
            $variableValues,
88 211
            $operationName,
89 211
            $fieldResolver,
90 211
            $promiseAdapter
91
        );
92
93 211
        if (is_array($exeContext)) {
94
            return new class($promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext))) implements ExecutorImplementation
95
            {
96
                /** @var Promise */
97
                private $result;
98
99 15
                public function __construct(Promise $result)
100
                {
101 15
                    $this->result = $result;
102 15
                }
103
104 15
                public function doExecute() : Promise
105
                {
106 15
                    return $this->result;
107
                }
108
            };
109
        }
110
111 197
        return new self($exeContext);
112
    }
113
114
    /**
115
     * Constructs an ExecutionContext object from the arguments passed to
116
     * execute, which we will pass throughout the other execution methods.
117
     *
118
     * @param mixed               $rootValue
119
     * @param mixed               $contextValue
120
     * @param mixed[]|Traversable $rawVariableValues
121
     * @param string|null         $operationName
122
     *
123
     * @return ExecutionContext|Error[]
124
     */
125 211
    private static function buildExecutionContext(
126
        Schema $schema,
127
        DocumentNode $documentNode,
128
        $rootValue,
129
        $contextValue,
130
        $rawVariableValues,
131
        $operationName = null,
132
        ?callable $fieldResolver = null,
133
        ?PromiseAdapter $promiseAdapter = null
134
    ) {
135 211
        $errors    = [];
136 211
        $fragments = [];
137
        /** @var OperationDefinitionNode $operation */
138 211
        $operation                    = null;
139 211
        $hasMultipleAssumedOperations = false;
140 211
        foreach ($documentNode->definitions as $definition) {
141 211
            switch ($definition->kind) {
0 ignored issues
show
Bug introduced by
Accessing kind on the interface GraphQL\Language\AST\DefinitionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
142 211
                case NodeKind::OPERATION_DEFINITION:
143 209
                    if (! $operationName && $operation) {
144 1
                        $hasMultipleAssumedOperations = true;
145
                    }
146 209
                    if (! $operationName ||
147 209
                        (isset($definition->name) && $definition->name->value === $operationName)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface GraphQL\Language\AST\DefinitionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
148 208
                        $operation = $definition;
149
                    }
150 209
                    break;
151 17
                case NodeKind::FRAGMENT_DEFINITION:
152 16
                    $fragments[$definition->name->value] = $definition;
153 211
                    break;
154
            }
155
        }
156 211
        if ($operation === null) {
157 3
            if ($operationName) {
158 1
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
159
            } else {
160 3
                $errors[] = new Error('Must provide an operation.');
161
            }
162 208
        } elseif ($hasMultipleAssumedOperations) {
163 1
            $errors[] = new Error(
164 1
                'Must provide operation name if query contains multiple operations.'
165
            );
166
        }
167 211
        $variableValues = null;
168 211
        if ($operation !== null) {
169 208
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
170 208
                $schema,
171 208
                $operation->variableDefinitions ?: [],
172 208
                $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

172
                /** @scrutinizer ignore-type */ $rawVariableValues ?: []
Loading history...
173
            );
174 208
            if (empty($coercionErrors)) {
175 198
                $variableValues = $coercedVariableValues;
176
            } else {
177 11
                $errors = array_merge($errors, $coercionErrors);
178
            }
179
        }
180 211
        if (! empty($errors)) {
181 15
            return $errors;
182
        }
183 197
        Utils::invariant($operation, 'Has operation if no errors.');
184 197
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
185
186 197
        return new ExecutionContext(
187 197
            $schema,
188 197
            $fragments,
189 197
            $rootValue,
190 197
            $contextValue,
191 197
            $operation,
192 197
            $variableValues,
193 197
            $errors,
194 197
            $fieldResolver,
195 197
            $promiseAdapter
196
        );
197
    }
198
199 197
    public function doExecute() : Promise
200
    {
201
        // Return a Promise that will eventually resolve to the data described by
202
        // The "Response" section of the GraphQL specification.
203
        //
204
        // If errors are encountered while executing a GraphQL field, only that
205
        // field and its descendants will be omitted, and sibling fields will still
206
        // be executed. An execution which encounters errors will still result in a
207
        // resolved Promise.
208 197
        $data   = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
209 197
        $result = $this->buildResponse($data);
210
211
        // Note: we deviate here from the reference implementation a bit by always returning promise
212
        // But for the "sync" case it is always fulfilled
213 197
        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...
214 39
            ? $result
215 197
            : $this->exeContext->promises->createFulfilled($result);
216
    }
217
218
    /**
219
     * @param mixed|Promise|null $data
220
     *
221
     * @return ExecutionResult|Promise
222
     */
223 197
    private function buildResponse($data)
224
    {
225 197
        if ($this->isPromise($data)) {
226
            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

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

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

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

478
                $result      = $this->resolveField($parentType, /** @scrutinizer ignore-type */ $sourceValue, $fieldNodes, $fieldPath);
Loading history...
479 6
                if ($result === self::$UNDEFINED) {
480 1
                    return $results;
481
                }
482 5
                $promise = $this->getPromise($result);
483 5
                if ($promise) {
484
                    return $promise->then(static function ($resolvedResult) use ($responseName, $results) {
485 2
                        $results[$responseName] = $resolvedResult;
486
487 2
                        return $results;
488 2
                    });
489
                }
490 5
                $results[$responseName] = $result;
491
492 5
                return $results;
493 6
            },
494 6
            []
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);
503
    }
504
505
    /**
506
     * Resolves the field on the given source object. In particular, this
507
     * figures out the value that the field returns by calling its resolve function,
508
     * then calls completeValue to complete promises, serialize scalars, or execute
509
     * the sub-selection-set for objects.
510
     *
511
     * @param object|null $source
512
     * @param FieldNode[] $fieldNodes
513
     * @param mixed[]     $path
514
     *
515
     * @return mixed[]|Exception|mixed|null
516
     */
517 197
    private function resolveField(ObjectType $parentType, $source, $fieldNodes, $path)
518
    {
519 197
        $exeContext = $this->exeContext;
520 197
        $fieldNode  = $fieldNodes[0];
521 197
        $fieldName  = $fieldNode->name->value;
522 197
        $fieldDef   = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
523 197
        if (! $fieldDef) {
0 ignored issues
show
introduced by
$fieldDef is of type GraphQL\Type\Definition\FieldDefinition, thus it always evaluated to true.
Loading history...
524 7
            return self::$UNDEFINED;
525
        }
526 193
        $returnType = $fieldDef->getType();
527
        // The resolve function's optional third argument is a collection of
528
        // information about the current execution state.
529 193
        $info = new ResolveInfo(
530 193
            $fieldName,
531 193
            $fieldNodes,
532 193
            $returnType,
533 193
            $parentType,
534 193
            $path,
535 193
            $exeContext->schema,
536 193
            $exeContext->fragments,
537 193
            $exeContext->rootValue,
538 193
            $exeContext->operation,
539 193
            $exeContext->variableValues
540
        );
541 193
        if ($fieldDef->resolveFn !== null) {
542 141
            $resolveFn = $fieldDef->resolveFn;
543 117
        } elseif ($parentType->resolveFieldFn !== null) {
544
            $resolveFn = $parentType->resolveFieldFn;
545
        } else {
546 117
            $resolveFn = $this->exeContext->fieldResolver;
547
        }
548
        // The resolve function's optional third argument is a context value that
549
        // is provided to every resolve function within an execution. It is commonly
550
        // used to represent an authenticated user, or request-specific caches.
551 193
        $context = $exeContext->contextValue;
552
        // Get the resolve function, regardless of if its result is normal
553
        // or abrupt (error).
554 193
        $result = $this->resolveOrError(
555 193
            $fieldDef,
556 193
            $fieldNode,
557 193
            $resolveFn,
558 193
            $source,
559 193
            $context,
560 193
            $info
561
        );
562 193
        $result = $this->completeValueCatchingError(
563 193
            $returnType,
564 193
            $fieldNodes,
565 193
            $info,
566 193
            $path,
567 193
            $result
568
        );
569
570 191
        return $result;
571
    }
572
573
    /**
574
     * This method looks up the field on the given type definition.
575
     * It has special casing for the two introspection fields, __schema
576
     * and __typename. __typename is special because it can always be
577
     * queried as a field, even in situations where no other fields
578
     * are allowed, like on a Union. __schema could get automatically
579
     * added to the query type, but that would require mutating type
580
     * definitions, which would cause issues.
581
     *
582
     * @param string $fieldName
583
     *
584
     * @return FieldDefinition
585
     */
586 197
    private function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
587
    {
588 197
        static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
589 197
        $schemaMetaFieldDef   = $schemaMetaFieldDef ?: Introspection::schemaMetaFieldDef();
590 197
        $typeMetaFieldDef     = $typeMetaFieldDef ?: Introspection::typeMetaFieldDef();
591 197
        $typeNameMetaFieldDef = $typeNameMetaFieldDef ?: Introspection::typeNameMetaFieldDef();
592 197
        if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
593 5
            return $schemaMetaFieldDef;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $schemaMetaFieldDef also could return the type GraphQL\Type\Definition\Type which is incompatible with the documented return type GraphQL\Type\Definition\FieldDefinition.
Loading history...
594
        }
595
596 197
        if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
597 15
            return $typeMetaFieldDef;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $typeMetaFieldDef also could return the type GraphQL\Type\Definition\Type which is incompatible with the documented return type GraphQL\Type\Definition\FieldDefinition.
Loading history...
598
        }
599
600 197
        if ($fieldName === $typeNameMetaFieldDef->name) {
601 7
            return $typeNameMetaFieldDef;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $typeNameMetaFieldDef also could return the type GraphQL\Type\Definition\Type which is incompatible with the documented return type GraphQL\Type\Definition\FieldDefinition.
Loading history...
602
        }
603 197
        $tmp = $parentType->getFields();
604
605 197
        return $tmp[$fieldName] ?? null;
606
    }
607
608
    /**
609
     * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
610
     * function. Returns the result of resolveFn or the abrupt-return Error object.
611
     *
612
     * @param FieldDefinition $fieldDef
613
     * @param FieldNode       $fieldNode
614
     * @param callable        $resolveFn
615
     * @param mixed           $source
616
     * @param mixed           $context
617
     * @param ResolveInfo     $info
618
     *
619
     * @return Throwable|Promise|mixed
620
     */
621 193
    private function resolveOrError($fieldDef, $fieldNode, $resolveFn, $source, $context, $info)
622
    {
623
        try {
624
            // Build hash of arguments from the field.arguments AST, using the
625
            // variables scope to fulfill any variable references.
626 193
            $args = Values::getArgumentValues(
627 193
                $fieldDef,
628 193
                $fieldNode,
629 193
                $this->exeContext->variableValues
630
            );
631
632 190
            return $resolveFn($source, $args, $context, $info);
633 17
        } catch (Exception $error) {
634 17
            return $error;
635
        } catch (Throwable $error) {
636
            return $error;
637
        }
638
    }
639
640
    /**
641
     * This is a small wrapper around completeValue which detects and logs errors
642
     * in the execution context.
643
     *
644
     * @param FieldNode[] $fieldNodes
645
     * @param string[]    $path
646
     * @param mixed       $result
647
     *
648
     * @return mixed[]|Promise|null
649
     */
650 193
    private function completeValueCatchingError(
651
        Type $returnType,
652
        $fieldNodes,
653
        ResolveInfo $info,
654
        $path,
655
        $result
656
    ) {
657 193
        $exeContext = $this->exeContext;
658
        // If the field type is non-nullable, then it is resolved without any
659
        // protection from errors.
660 193
        if ($returnType instanceof NonNull) {
661 50
            return $this->completeValueWithLocatedError(
662 50
                $returnType,
663 50
                $fieldNodes,
664 50
                $info,
665 50
                $path,
666 50
                $result
667
            );
668
        }
669
        // Otherwise, error protection is applied, logging the error and resolving
670
        // a null value for this field if one is encountered.
671
        try {
672 189
            $completed = $this->completeValueWithLocatedError(
673 189
                $returnType,
674 189
                $fieldNodes,
675 189
                $info,
676 189
                $path,
677 189
                $result
678
            );
679 176
            $promise   = $this->getPromise($completed);
680 176
            if ($promise) {
0 ignored issues
show
introduced by
$promise is of type GraphQL\Executor\Promise\Promise, thus it always evaluated to true.
Loading history...
681 37
                return $promise->then(
682 37
                    null,
683
                    function ($error) use ($exeContext) {
684 24
                        $exeContext->addError($error);
685
686 24
                        return $this->exeContext->promises->createFulfilled(null);
687 37
                    }
688
                );
689
            }
690
691 156
            return $completed;
692 29
        } catch (Error $err) {
693
            // If `completeValueWithLocatedError` returned abruptly (threw an error), log the error
694
            // and return null.
695 29
            $exeContext->addError($err);
696
697 29
            return null;
698
        }
699
    }
700
701
    /**
702
     * This is a small wrapper around completeValue which annotates errors with
703
     * location information.
704
     *
705
     * @param FieldNode[] $fieldNodes
706
     * @param string[]    $path
707
     * @param mixed       $result
708
     *
709
     * @return mixed[]|mixed|Promise|null
710
     *
711
     * @throws Error
712
     */
713 193
    public function completeValueWithLocatedError(
714
        Type $returnType,
715
        $fieldNodes,
716
        ResolveInfo $info,
717
        $path,
718
        $result
719
    ) {
720
        try {
721 193
            $completed = $this->completeValue(
722 193
                $returnType,
723 193
                $fieldNodes,
724 193
                $info,
725 193
                $path,
726 193
                $result
727
            );
728 178
            $promise   = $this->getPromise($completed);
729 178
            if ($promise) {
730 39
                return $promise->then(
731 39
                    null,
732
                    function ($error) use ($fieldNodes, $path) {
733 26
                        return $this->exeContext->promises->createRejected(Error::createLocatedError(
734 26
                            $error,
735 26
                            $fieldNodes,
736 26
                            $path
737
                        ));
738 39
                    }
739
                );
740
            }
741
742 158
            return $completed;
743 37
        } catch (Exception $error) {
744 37
            throw Error::createLocatedError($error, $fieldNodes, $path);
745
        } catch (Throwable $error) {
746
            throw Error::createLocatedError($error, $fieldNodes, $path);
747
        }
748
    }
749
750
    /**
751
     * Implements the instructions for completeValue as defined in the
752
     * "Field entries" section of the spec.
753
     *
754
     * If the field type is Non-Null, then this recursively completes the value
755
     * for the inner type. It throws a field error if that completion returns null,
756
     * as per the "Nullability" section of the spec.
757
     *
758
     * If the field type is a List, then this recursively completes the value
759
     * for the inner type on each item in the list.
760
     *
761
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
762
     * value of the type by calling the `serialize` method of GraphQL type
763
     * definition.
764
     *
765
     * If the field is an abstract type, determine the runtime type of the value
766
     * and then complete based on that type
767
     *
768
     * Otherwise, the field type expects a sub-selection set, and will complete the
769
     * value by evaluating all sub-selections.
770
     *
771
     * @param FieldNode[] $fieldNodes
772
     * @param string[]    $path
773
     * @param mixed       $result
774
     *
775
     * @return mixed[]|mixed|Promise|null
776
     *
777
     * @throws Error
778
     * @throws Throwable
779
     */
780 193
    private function completeValue(
781
        Type $returnType,
782
        $fieldNodes,
783
        ResolveInfo $info,
784
        $path,
785
        &$result
786
    ) {
787 193
        $promise = $this->getPromise($result);
788
        // If result is a Promise, apply-lift over completeValue.
789 193
        if ($promise) {
790
            return $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
791 30
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
792 33
            });
793
        }
794 191
        if ($result instanceof Exception || $result instanceof Throwable) {
795 17
            throw $result;
796
        }
797
        // If field type is NonNull, complete for inner type, and throw field error
798
        // if result is null.
799 183
        if ($returnType instanceof NonNull) {
800 44
            $completed = $this->completeValue(
801 44
                $returnType->getWrappedType(),
802 44
                $fieldNodes,
803 44
                $info,
804 44
                $path,
805 44
                $result
806
            );
807 44
            if ($completed === null) {
808 15
                throw new InvariantViolation(
809 15
                    'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.'
810
                );
811
            }
812
813 38
            return $completed;
814
        }
815
        // If result is null-like, return null.
816 183
        if ($result === null) {
817 49
            return null;
818
        }
819
        // If field type is List, complete each item in the list with the inner type
820 163
        if ($returnType instanceof ListOfType) {
821 56
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
822
        }
823
        // Account for invalid schema definition when typeLoader returns different
824
        // instance than `resolveType` or $field->getType() or $arg->getType()
825 163
        if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
826 1
            $hint = '';
827 1
            if ($this->exeContext->schema->getConfig()->typeLoader) {
828 1
                $hint = sprintf(
829 1
                    'Make sure that type loader returns the same instance as defined in %s.%s',
830 1
                    $info->parentType,
831 1
                    $info->fieldName
832
                );
833
            }
834 1
            throw new InvariantViolation(
835 1
                sprintf(
836
                    'Schema must contain unique named types but contains multiple types named "%s". %s ' .
837 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
838 1
                    $returnType,
839 1
                    $hint
840
                )
841
            );
842
        }
843
        // If field type is Scalar or Enum, serialize to a valid value, returning
844
        // null if serialization is not possible.
845 162
        if ($returnType instanceof LeafType) {
846 146
            return $this->completeLeafValue($returnType, $result);
847
        }
848 93
        if ($returnType instanceof AbstractType) {
849 31
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
850
        }
851
        // Field type must be Object, Interface or Union and expect sub-selections.
852 63
        if ($returnType instanceof ObjectType) {
853 63
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
854
        }
855
        throw new RuntimeException(sprintf('Cannot complete value of unexpected type "%s".', $returnType));
856
    }
857
858
    /**
859
     * @param mixed $value
860
     *
861
     * @return bool
862
     */
863 197
    private function isPromise($value)
864
    {
865 197
        return $value instanceof Promise || $this->exeContext->promises->isThenable($value);
866
    }
867
868
    /**
869
     * Only returns the value if it acts like a Promise, i.e. has a "then" function,
870
     * otherwise returns null.
871
     *
872
     * @param mixed $value
873
     *
874
     * @return Promise|null
875
     */
876 194
    private function getPromise($value)
877
    {
878 194
        if ($value === null || $value instanceof Promise) {
879 93
            return $value;
880
        }
881 176
        if ($this->exeContext->promises->isThenable($value)) {
882 39
            $promise = $this->exeContext->promises->convertThenable($value);
883 39
            if (! $promise instanceof Promise) {
0 ignored issues
show
introduced by
$promise is always a sub-type of GraphQL\Executor\Promise\Promise.
Loading history...
884
                throw new InvariantViolation(sprintf(
885
                    '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
886
                    get_class($this->exeContext->promises),
887
                    Utils::printSafe($promise)
888
                ));
889
            }
890
891 39
            return $promise;
892
        }
893
894 172
        return null;
895
    }
896
897
    /**
898
     * Similar to array_reduce(), however the reducing callback may return
899
     * a Promise, in which case reduction will continue after each promise resolves.
900
     *
901
     * If the callback does not return a Promise, then this function will also not
902
     * return a Promise.
903
     *
904
     * @param mixed[]            $values
905
     * @param Promise|mixed|null $initialValue
906
     *
907
     * @return mixed[]
908
     */
909 6
    private function promiseReduce(array $values, callable $callback, $initialValue)
910
    {
911 6
        return array_reduce(
912 6
            $values,
913
            function ($previous, $value) use ($callback) {
914 6
                $promise = $this->getPromise($previous);
915 6
                if ($promise) {
916
                    return $promise->then(static function ($resolved) use ($callback, $value) {
917 2
                        return $callback($resolved, $value);
918 2
                    });
919
                }
920
921 6
                return $callback($previous, $value);
922 6
            },
923 6
            $initialValue
924
        );
925
    }
926
927
    /**
928
     * Complete a list value by completing each item in the list with the
929
     * inner type
930
     *
931
     * @param FieldNode[] $fieldNodes
932
     * @param mixed[]     $path
933
     * @param mixed       $result
934
     *
935
     * @return mixed[]|Promise
936
     *
937
     * @throws Exception
938
     */
939 56
    private function completeListValue(ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
940
    {
941 56
        $itemType = $returnType->getWrappedType();
942 56
        Utils::invariant(
943 56
            is_array($result) || $result instanceof Traversable,
944 56
            'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
945
        );
946 56
        $containsPromise = false;
947 56
        $i               = 0;
948 56
        $completedItems  = [];
949 56
        foreach ($result as $item) {
950 56
            $fieldPath     = $path;
951 56
            $fieldPath[]   = $i++;
952 56
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
953 56
            if (! $containsPromise && $this->getPromise($completedItem)) {
954 13
                $containsPromise = true;
955
            }
956 56
            $completedItems[] = $completedItem;
957
        }
958
959 56
        return $containsPromise ? $this->exeContext->promises->all($completedItems) : $completedItems;
960
    }
961
962
    /**
963
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
964
     *
965
     * @param  mixed $result
966
     *
967
     * @return mixed
968
     *
969
     * @throws Exception
970
     */
971 146
    private function completeLeafValue(LeafType $returnType, &$result)
972
    {
973
        try {
974 146
            return $returnType->serialize($result);
975 3
        } catch (Exception $error) {
976 3
            throw new InvariantViolation(
977 3
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
978 3
                0,
979 3
                $error
980
            );
981
        } catch (Throwable $error) {
982
            throw new InvariantViolation(
983
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
984
                0,
985
                $error
986
            );
987
        }
988
    }
989
990
    /**
991
     * Complete a value of an abstract type by determining the runtime object type
992
     * of that value, then complete the value for that type.
993
     *
994
     * @param FieldNode[] $fieldNodes
995
     * @param mixed[]     $path
996
     * @param mixed[]     $result
997
     *
998
     * @return mixed
999
     *
1000
     * @throws Error
1001
     */
1002 31
    private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1003
    {
1004 31
        $exeContext  = $this->exeContext;
1005 31
        $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

1005
        $runtimeType = $returnType->resolveType(/** @scrutinizer ignore-type */ $result, $exeContext->contextValue, $info);
Loading history...
1006 31
        if ($runtimeType === null) {
1007 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

1007
            /** @scrutinizer ignore-call */ 
1008
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
1008
        }
1009 31
        $promise = $this->getPromise($runtimeType);
1010 31
        if ($promise) {
1011
            return $promise->then(function ($resolvedRuntimeType) use (
1012 5
                $returnType,
1013 5
                $fieldNodes,
1014 5
                $info,
1015 5
                $path,
1016 5
                &$result
1017
            ) {
1018 5
                return $this->completeObjectValue(
1019 5
                    $this->ensureValidRuntimeType(
1020 5
                        $resolvedRuntimeType,
1021 5
                        $returnType,
1022 5
                        $info,
1023 5
                        $result
1024
                    ),
1025 5
                    $fieldNodes,
1026 5
                    $info,
1027 5
                    $path,
1028 5
                    $result
1029
                );
1030 7
            });
1031
        }
1032
1033 24
        return $this->completeObjectValue(
1034 24
            $this->ensureValidRuntimeType(
1035 24
                $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

1035
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
1036 24
                $returnType,
1037 24
                $info,
1038 24
                $result
1039
            ),
1040 22
            $fieldNodes,
1041 22
            $info,
1042 22
            $path,
1043 22
            $result
1044
        );
1045
    }
1046
1047
    /**
1048
     * If a resolveType function is not given, then a default resolve behavior is
1049
     * used which attempts two strategies:
1050
     *
1051
     * First, See if the provided value has a `__typename` field defined, if so, use
1052
     * that value as name of the resolved type.
1053
     *
1054
     * Otherwise, test each possible type for the abstract type by calling
1055
     * isTypeOf for the object being coerced, returning the first type that matches.
1056
     *
1057
     * @param mixed|null $value
1058
     * @param mixed|null $context
1059
     *
1060
     * @return ObjectType|Promise|null
1061
     */
1062 11
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
1063
    {
1064
        // First, look for `__typename`.
1065 11
        if ($value !== null &&
1066 11
            (is_array($value) || $value instanceof ArrayAccess) &&
1067 11
            isset($value['__typename']) &&
1068 11
            is_string($value['__typename'])
1069
        ) {
1070 2
            return $value['__typename'];
1071
        }
1072 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
1073 1
            Warning::warnOnce(
1074 1
                sprintf(
1075
                    'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
1076
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1077
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1078 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1079 1
                    $abstractType->name,
1080 1
                    Utils::printSafe($value)
1081
                ),
1082 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1083
            );
1084
        }
1085
        // Otherwise, test each possible type.
1086 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1087 9
        $promisedIsTypeOfResults = [];
1088 9
        foreach ($possibleTypes as $index => $type) {
1089 9
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
1090 9
            if ($isTypeOfResult === null) {
1091
                continue;
1092
            }
1093 9
            $promise = $this->getPromise($isTypeOfResult);
1094 9
            if ($promise) {
1095 3
                $promisedIsTypeOfResults[$index] = $promise;
1096 6
            } elseif ($isTypeOfResult) {
1097 9
                return $type;
1098
            }
1099
        }
1100 3
        if (! empty($promisedIsTypeOfResults)) {
1101 3
            return $this->exeContext->promises->all($promisedIsTypeOfResults)
1102
                ->then(static function ($isTypeOfResults) use ($possibleTypes) {
1103 2
                    foreach ($isTypeOfResults as $index => $result) {
1104 2
                        if ($result) {
1105 2
                            return $possibleTypes[$index];
1106
                        }
1107
                    }
1108
1109
                    return null;
1110 3
                });
1111
        }
1112
1113
        return null;
1114
    }
1115
1116
    /**
1117
     * Complete an Object value by executing all sub-selections.
1118
     *
1119
     * @param FieldNode[] $fieldNodes
1120
     * @param mixed[]     $path
1121
     * @param mixed       $result
1122
     *
1123
     * @return mixed[]|Promise|stdClass
1124
     *
1125
     * @throws Error
1126
     */
1127 89
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1128
    {
1129
        // If there is an isTypeOf predicate function, call it with the
1130
        // current result. If isTypeOf returns false, then raise an error rather
1131
        // than continuing execution.
1132 89
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1133 89
        if ($isTypeOf !== null) {
1134 11
            $promise = $this->getPromise($isTypeOf);
1135 11
            if ($promise) {
1136
                return $promise->then(function ($isTypeOfResult) use (
1137 2
                    $returnType,
1138 2
                    $fieldNodes,
1139 2
                    $path,
1140 2
                    &$result
1141
                ) {
1142 2
                    if (! $isTypeOfResult) {
1143
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1144
                    }
1145
1146 2
                    return $this->collectAndExecuteSubfields(
1147 2
                        $returnType,
1148 2
                        $fieldNodes,
1149 2
                        $path,
1150 2
                        $result
1151
                    );
1152 2
                });
1153
            }
1154 9
            if (! $isTypeOf) {
1155 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1156
            }
1157
        }
1158
1159 87
        return $this->collectAndExecuteSubfields(
1160 87
            $returnType,
1161 87
            $fieldNodes,
1162 87
            $path,
1163 87
            $result
1164
        );
1165
    }
1166
1167
    /**
1168
     * @param mixed[]     $result
1169
     * @param FieldNode[] $fieldNodes
1170
     *
1171
     * @return Error
1172
     */
1173 1
    private function invalidReturnTypeError(
1174
        ObjectType $returnType,
1175
        $result,
1176
        $fieldNodes
1177
    ) {
1178 1
        return new Error(
1179 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1180 1
            $fieldNodes
1181
        );
1182
    }
1183
1184
    /**
1185
     * @param FieldNode[] $fieldNodes
1186
     * @param mixed[]     $path
1187
     * @param mixed[]     $result
1188
     *
1189
     * @return mixed[]|Promise|stdClass
1190
     *
1191
     * @throws Error
1192
     */
1193 89
    private function collectAndExecuteSubfields(
1194
        ObjectType $returnType,
1195
        $fieldNodes,
1196
        $path,
1197
        &$result
1198
    ) {
1199 89
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
1200
1201 89
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1202
    }
1203
1204 89
    private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayObject
1205
    {
1206 89
        if (! isset($this->subFieldCache[$returnType])) {
1207 89
            $this->subFieldCache[$returnType] = new SplObjectStorage();
1208
        }
1209 89
        if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
1210
            // Collect sub-fields to execute to complete this value.
1211 89
            $subFieldNodes        = new ArrayObject();
1212 89
            $visitedFragmentNames = new ArrayObject();
1213 89
            foreach ($fieldNodes as $fieldNode) {
1214 89
                if (! isset($fieldNode->selectionSet)) {
1215
                    continue;
1216
                }
1217 89
                $subFieldNodes = $this->collectFields(
1218 89
                    $returnType,
1219 89
                    $fieldNode->selectionSet,
1220 89
                    $subFieldNodes,
1221 89
                    $visitedFragmentNames
1222
                );
1223
            }
1224 89
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
1225
        }
1226
1227 89
        return $this->subFieldCache[$returnType][$fieldNodes];
1228
    }
1229
1230
    /**
1231
     * Implements the "Evaluating selection sets" section of the spec
1232
     * for "read" mode.
1233
     *
1234
     * @param mixed|null  $source
1235
     * @param mixed[]     $path
1236
     * @param ArrayObject $fields
1237
     *
1238
     * @return Promise|stdClass|mixed[]
1239
     */
1240 193
    private function executeFields(ObjectType $parentType, $source, $path, $fields)
1241
    {
1242 193
        $containsPromise = false;
1243 193
        $finalResults    = [];
1244 193
        foreach ($fields as $responseName => $fieldNodes) {
1245 193
            $fieldPath   = $path;
1246 193
            $fieldPath[] = $responseName;
1247 193
            $result      = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath);
1248 191
            if ($result === self::$UNDEFINED) {
1249 6
                continue;
1250
            }
1251 188
            if (! $containsPromise && $this->getPromise($result)) {
1252 37
                $containsPromise = true;
1253
            }
1254 188
            $finalResults[$responseName] = $result;
1255
        }
1256
        // If there are no promises, we can just return the object
1257 191
        if (! $containsPromise) {
1258 163
            return self::fixResultsIfEmptyArray($finalResults);
1259
        }
1260
1261
        // Otherwise, results is a map from field name to the result
1262
        // of resolving that field, which is possibly a promise. Return
1263
        // a promise that will return this same map, but with any
1264
        // promises replaced with the values they resolved to.
1265 37
        return $this->promiseForAssocArray($finalResults);
1266
    }
1267
1268
    /**
1269
     * @see https://github.com/webonyx/graphql-php/issues/59
1270
     *
1271
     * @param mixed[] $results
1272
     *
1273
     * @return stdClass|mixed[]
1274
     */
1275 193
    private static function fixResultsIfEmptyArray($results)
1276
    {
1277 193
        if ($results === []) {
1278 5
            return new stdClass();
1279
        }
1280
1281 189
        return $results;
1282
    }
1283
1284
    /**
1285
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1286
     * a `Promise<array<key,scalar|array>>`
1287
     *
1288
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1289
     * any promises.
1290
     *
1291
     * @param (string|Promise)[] $assoc
1292
     *
1293
     * @return mixed
1294
     */
1295 37
    private function promiseForAssocArray(array $assoc)
1296
    {
1297 37
        $keys              = array_keys($assoc);
1298 37
        $valuesAndPromises = array_values($assoc);
1299 37
        $promise           = $this->exeContext->promises->all($valuesAndPromises);
1300
1301
        return $promise->then(static function ($values) use ($keys) {
1302 35
            $resolvedResults = [];
1303 35
            foreach ($values as $i => $value) {
1304 35
                $resolvedResults[$keys[$i]] = $value;
1305
            }
1306
1307 35
            return self::fixResultsIfEmptyArray($resolvedResults);
1308 37
        });
1309
    }
1310
1311
    /**
1312
     * @param string|ObjectType|null $runtimeTypeOrName
1313
     * @param FieldNode[]            $fieldNodes
1314
     * @param mixed                  $result
1315
     *
1316
     * @return ObjectType
1317
     */
1318 29
    private function ensureValidRuntimeType(
1319
        $runtimeTypeOrName,
1320
        AbstractType $returnType,
1321
        ResolveInfo $info,
1322
        &$result
1323
    ) {
1324 29
        $runtimeType = is_string($runtimeTypeOrName) ?
1325 4
            $this->exeContext->schema->getType($runtimeTypeOrName) :
1326 29
            $runtimeTypeOrName;
1327 29
        if (! $runtimeType instanceof ObjectType) {
1328 1
            throw new InvariantViolation(
1329 1
                sprintf(
1330
                    'Abstract type %s must resolve to an Object type at ' .
1331
                    'runtime for field %s.%s with value "%s", received "%s". ' .
1332
                    'Either the %s type should provide a "resolveType" ' .
1333 1
                    'function or each possible type should provide an "isTypeOf" function.',
1334 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

1334
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1335 1
                    $info->parentType,
1336 1
                    $info->fieldName,
1337 1
                    Utils::printSafe($result),
1338 1
                    Utils::printSafe($runtimeType),
1339 1
                    $returnType
1340
                )
1341
            );
1342
        }
1343 28
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1344 4
            throw new InvariantViolation(
1345 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1346
            );
1347
        }
1348 28
        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...
1349 1
            throw new InvariantViolation(
1350 1
                sprintf(
1351
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1352
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1353
                    'type instance as referenced anywhere else within the schema ' .
1354 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1355 1
                    $runtimeType,
1356 1
                    $returnType
1357
                )
1358
            );
1359
        }
1360
1361 27
        return $runtimeType;
1362
    }
1363
}
1364