Failed Conditions
Push — master ( ed1d83...9704ba )
by Vladimir
19:48 queued 16:12
created

ReferenceExecutor::buildExecutionContext()

Size

Total Lines 71
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 48
c 2
b 0
f 0
dl 0
loc 71
ccs 46
cts 46
cp 1
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 201
    private function __construct(ExecutionContext $context)
64
    {
65 201
        if (! self::$UNDEFINED) {
66 1
            self::$UNDEFINED = Utils::undefined();
67
        }
68 201
        $this->exeContext    = $context;
69 201
        $this->subFieldCache = new SplObjectStorage();
70 201
    }
71
72 216
    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 216
        $exeContext = self::buildExecutionContext(
83 216
            $schema,
84 216
            $documentNode,
85 216
            $rootValue,
86 216
            $contextValue,
87 216
            $variableValues,
88 216
            $operationName,
89 216
            $fieldResolver,
90 216
            $promiseAdapter
91
        );
92
93 216
        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 16
                public function __construct(Promise $result)
100
                {
101 16
                    $this->result = $result;
102 16
                }
103
104 16
                public function doExecute() : Promise
105
                {
106 16
                    return $this->result;
107
                }
108
            };
109
        }
110
111 201
        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 216
    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 216
        $errors    = [];
136 216
        $fragments = [];
137
        /** @var OperationDefinitionNode $operation */
138 216
        $operation                    = null;
139 216
        $hasMultipleAssumedOperations = false;
140 216
        foreach ($documentNode->definitions as $definition) {
141
            switch (true) {
142 216
                case $definition instanceof OperationDefinitionNode:
143 214
                    if (! $operationName && $operation) {
144 1
                        $hasMultipleAssumedOperations = true;
145
                    }
146 214
                    if (! $operationName ||
147 214
                        (isset($definition->name) && $definition->name->value === $operationName)) {
148 213
                        $operation = $definition;
149
                    }
150 214
                    break;
151 17
                case $definition instanceof FragmentDefinitionNode:
152 16
                    $fragments[$definition->name->value] = $definition;
153 216
                    break;
154
            }
155
        }
156 216
        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 213
        } elseif ($hasMultipleAssumedOperations) {
163 1
            $errors[] = new Error(
164 1
                'Must provide operation name if query contains multiple operations.'
165
            );
166
        }
167 216
        $variableValues = null;
168 216
        if ($operation !== null) {
169 213
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
170 213
                $schema,
171 213
                $operation->variableDefinitions ?: [],
172 213
                $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 213
            if (empty($coercionErrors)) {
175 202
                $variableValues = $coercedVariableValues;
176
            } else {
177 12
                $errors = array_merge($errors, $coercionErrors);
178
            }
179
        }
180 216
        if (! empty($errors)) {
181 16
            return $errors;
182
        }
183 201
        Utils::invariant($operation, 'Has operation if no errors.');
184 201
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
185
186 201
        return new ExecutionContext(
187 201
            $schema,
188 201
            $fragments,
189 201
            $rootValue,
190 201
            $contextValue,
191 201
            $operation,
192 201
            $variableValues,
193 201
            $errors,
194 201
            $fieldResolver,
195 201
            $promiseAdapter
196
        );
197
    }
198
199 201
    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 201
        $data   = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
209 201
        $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 201
        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 40
            ? $result
215 201
            : $this->exeContext->promiseAdapter->createFulfilled($result);
216
    }
217
218
    /**
219
     * @param mixed|Promise|null $data
220
     *
221
     * @return ExecutionResult|Promise
222
     */
223 201
    private function buildResponse($data)
224
    {
225 201
        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 40
                return $this->buildResponse($resolved);
228 40
            });
229
        }
230 201
        if ($data !== null) {
231 197
            $data = (array) $data;
232
        }
233
234 201
        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 201
    private function executeOperation(OperationDefinitionNode $operation, $rootValue)
245
    {
246 201
        $type   = $this->getOperationRootType($this->exeContext->schema, $operation);
247 201
        $fields = $this->collectFields($type, $operation->selectionSet, new ArrayObject(), new ArrayObject());
248 201
        $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 201
            $result = $operation->operation === 'mutation'
256 6
                ? $this->executeFieldsSerially($type, $rootValue, $path, $fields)
257 201
                : $this->executeFields($type, $rootValue, $path, $fields);
258 199
            if ($this->isPromise($result)) {
259 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

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

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

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

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

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

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