Passed
Pull Request — master (#314)
by Jakub
08:46 queued 02:41
created

ReferenceExecutor::buildExecutionContext()

Size

Total Lines 70
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 48
dl 0
loc 70
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 195
    private function __construct(ExecutionContext $context)
64
    {
65 195
        if (! self::$UNDEFINED) {
66 1
            self::$UNDEFINED = Utils::undefined();
67
        }
68 195
        $this->exeContext    = $context;
69 195
        $this->subFieldCache = new SplObjectStorage();
70 195
    }
71
72 209
    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 209
        $exeContext = self::buildExecutionContext(
83 209
            $schema,
84 209
            $documentNode,
85 209
            $rootValue,
86 209
            $contextValue,
87 209
            $variableValues,
88 209
            $operationName,
89 209
            $fieldResolver,
90 209
            $promiseAdapter
91
        );
92
93 209
        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 195
        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 209
    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 209
        $errors    = [];
136 209
        $fragments = [];
137
        /** @var OperationDefinitionNode $operation */
138 209
        $operation                    = null;
139 209
        $hasMultipleAssumedOperations = false;
140 209
        foreach ($documentNode->definitions as $definition) {
141 209
            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 209
                case NodeKind::OPERATION_DEFINITION:
143 207
                    if (! $operationName && $operation) {
144 1
                        $hasMultipleAssumedOperations = true;
145
                    }
146 207
                    if (! $operationName ||
147 207
                        (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 206
                        $operation = $definition;
149
                    }
150 207
                    break;
151 15
                case NodeKind::FRAGMENT_DEFINITION:
152 14
                    $fragments[$definition->name->value] = $definition;
153 209
                    break;
154
            }
155
        }
156 209
        if (! $operation) {
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 206
        } elseif ($hasMultipleAssumedOperations) {
163 1
            $errors[] = new Error(
164 1
                'Must provide operation name if query contains multiple operations.'
165
            );
166
        }
167 209
        $variableValues = null;
168 209
        if ($operation) {
169 206
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
170 206
                $schema,
171 206
                $operation->variableDefinitions ?: [],
172 206
                $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 206
            if (empty($coercionErrors)) {
175 196
                $variableValues = $coercedVariableValues;
176
            } else {
177 11
                $errors = array_merge($errors, $coercionErrors);
178
            }
179
        }
180 209
        if (! empty($errors)) {
181 15
            return $errors;
182
        }
183 195
        Utils::invariant($operation, 'Has operation if no errors.');
184 195
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
185 195
        return new ExecutionContext(
186 195
            $schema,
187 195
            $fragments,
188 195
            $rootValue,
189 195
            $contextValue,
190 195
            $operation,
191 195
            $variableValues,
192 195
            $errors,
193 195
            $fieldResolver,
194 195
            $promiseAdapter
195
        );
196
    }
197
198 195
    public function doExecute() : Promise
199
    {
200
        // Return a Promise that will eventually resolve to the data described by
201
        // The "Response" section of the GraphQL specification.
202
        //
203
        // If errors are encountered while executing a GraphQL field, only that
204
        // field and its descendants will be omitted, and sibling fields will still
205
        // be executed. An execution which encounters errors will still result in a
206
        // resolved Promise.
207 195
        $data   = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
208 195
        $result = $this->buildResponse($data);
209
        // Note: we deviate here from the reference implementation a bit by always returning promise
210
        // But for the "sync" case it is always fulfilled
211 195
        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...
212 39
            ? $result
213 195
            : $this->exeContext->promises->createFulfilled($result);
214
    }
215
216
    /**
217
     * @param mixed|Promise|null $data
218
     *
219
     * @return ExecutionResult|Promise
220
     */
221 195
    private function buildResponse($data)
222
    {
223 195
        if ($this->isPromise($data)) {
224
            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

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

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

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

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

979
            /** @scrutinizer ignore-call */ 
980
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
980
        }
981 31
        $promise = $this->getPromise($runtimeType);
982 31
        if ($promise) {
983
            return $promise->then(function ($resolvedRuntimeType) use (
984 5
                $returnType,
985 5
                $fieldNodes,
986 5
                $info,
987 5
                $path,
988 5
                &$result
989
            ) {
990 5
                return $this->completeObjectValue(
991 5
                    $this->ensureValidRuntimeType(
992 5
                        $resolvedRuntimeType,
993 5
                        $returnType,
994 5
                        $info,
995 5
                        $result
996
                    ),
997 5
                    $fieldNodes,
998 5
                    $info,
999 5
                    $path,
1000 5
                    $result
1001
                );
1002 7
            });
1003
        }
1004 24
        return $this->completeObjectValue(
1005 24
            $this->ensureValidRuntimeType(
1006 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

1006
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
1007 24
                $returnType,
1008 24
                $info,
1009 24
                $result
1010
            ),
1011 22
            $fieldNodes,
1012 22
            $info,
1013 22
            $path,
1014 22
            $result
1015
        );
1016
    }
1017
1018
    /**
1019
     * If a resolveType function is not given, then a default resolve behavior is
1020
     * used which attempts two strategies:
1021
     *
1022
     * First, See if the provided value has a `__typename` field defined, if so, use
1023
     * that value as name of the resolved type.
1024
     *
1025
     * Otherwise, test each possible type for the abstract type by calling
1026
     * isTypeOf for the object being coerced, returning the first type that matches.
1027
     *
1028
     * @param mixed|null $value
1029
     * @param mixed|null $context
1030
     *
1031
     * @return ObjectType|Promise|null
1032
     */
1033 11
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
1034
    {
1035
        // First, look for `__typename`.
1036 11
        if ($value !== null &&
1037 11
            (is_array($value) || $value instanceof ArrayAccess) &&
1038 11
            isset($value['__typename']) &&
1039 11
            is_string($value['__typename'])
1040
        ) {
1041 2
            return $value['__typename'];
1042
        }
1043 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
0 ignored issues
show
Bug introduced by
The method getConfig() 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

1043
        if ($abstractType instanceof InterfaceType && $info->schema->/** @scrutinizer ignore-call */ getConfig()->typeLoader) {

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...
1044 1
            Warning::warnOnce(
1045 1
                sprintf(
1046
                    'GraphQL Interface Type `%s` returned `null` from it`s `resolveType` function ' .
1047
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1048
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1049 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1050 1
                    $abstractType->name,
1051 1
                    Utils::printSafe($value)
1052
                ),
1053 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1054
            );
1055
        }
1056
        // Otherwise, test each possible type.
1057 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1058 9
        $promisedIsTypeOfResults = [];
1059 9
        foreach ($possibleTypes as $index => $type) {
1060 9
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
1061 9
            if ($isTypeOfResult === null) {
1062
                continue;
1063
            }
1064 9
            $promise = $this->getPromise($isTypeOfResult);
1065 9
            if ($promise) {
1066 3
                $promisedIsTypeOfResults[$index] = $promise;
1067 6
            } elseif ($isTypeOfResult) {
1068 9
                return $type;
1069
            }
1070
        }
1071 3
        if (! empty($promisedIsTypeOfResults)) {
1072 3
            return $this->exeContext->promises->all($promisedIsTypeOfResults)
1073
                ->then(static function ($isTypeOfResults) use ($possibleTypes) {
1074 2
                    foreach ($isTypeOfResults as $index => $result) {
1075 2
                        if ($result) {
1076 2
                            return $possibleTypes[$index];
1077
                        }
1078
                    }
1079
                    return null;
1080 3
                });
1081
        }
1082
        return null;
1083
    }
1084
1085
    /**
1086
     * Complete an Object value by executing all sub-selections.
1087
     *
1088
     * @param FieldNode[] $fieldNodes
1089
     * @param mixed[]     $path
1090
     * @param mixed       $result
1091
     *
1092
     * @return mixed[]|Promise|stdClass
1093
     *
1094
     * @throws Error
1095
     */
1096 89
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1097
    {
1098
        // If there is an isTypeOf predicate function, call it with the
1099
        // current result. If isTypeOf returns false, then raise an error rather
1100
        // than continuing execution.
1101 89
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1102 89
        if ($isTypeOf !== null) {
1103 11
            $promise = $this->getPromise($isTypeOf);
1104 11
            if ($promise) {
1105
                return $promise->then(function ($isTypeOfResult) use (
1106 2
                    $returnType,
1107 2
                    $fieldNodes,
1108 2
                    $info,
1109 2
                    $path,
1110 2
                    &$result
1111
                ) {
1112 2
                    if (! $isTypeOfResult) {
1113
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1114
                    }
1115 2
                    return $this->collectAndExecuteSubfields(
1116 2
                        $returnType,
1117 2
                        $fieldNodes,
1118 2
                        $info,
1119 2
                        $path,
1120 2
                        $result
1121
                    );
1122 2
                });
1123
            }
1124 9
            if (! $isTypeOf) {
1125 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1126
            }
1127
        }
1128 87
        return $this->collectAndExecuteSubfields(
1129 87
            $returnType,
1130 87
            $fieldNodes,
1131 87
            $info,
1132 87
            $path,
1133 87
            $result
1134
        );
1135
    }
1136
1137
    /**
1138
     * @param mixed[]     $result
1139
     * @param FieldNode[] $fieldNodes
1140
     *
1141
     * @return Error
1142
     */
1143 1
    private function invalidReturnTypeError(
1144
        ObjectType $returnType,
1145
        $result,
1146
        $fieldNodes
1147
    ) {
1148 1
        return new Error(
1149 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1150 1
            $fieldNodes
1151
        );
1152
    }
1153
1154
    /**
1155
     * @param FieldNode[] $fieldNodes
1156
     * @param mixed[]     $path
1157
     * @param mixed[]     $result
1158
     *
1159
     * @return mixed[]|Promise|stdClass
1160
     *
1161
     * @throws Error
1162
     */
1163 89
    private function collectAndExecuteSubfields(
1164
        ObjectType $returnType,
1165
        $fieldNodes,
1166
        ResolveInfo $info,
0 ignored issues
show
Unused Code introduced by
The parameter $info is not used and could be removed. ( Ignorable by Annotation )

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

1166
        /** @scrutinizer ignore-unused */ ResolveInfo $info,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1167
        $path,
1168
        &$result
1169
    ) {
1170 89
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
1171 89
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1172
    }
1173
1174 89
    private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayObject
1175
    {
1176 89
        if (! isset($this->subFieldCache[$returnType])) {
1177 89
            $this->subFieldCache[$returnType] = new SplObjectStorage();
1178
        }
1179 89
        if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
1180
            // Collect sub-fields to execute to complete this value.
1181 89
            $subFieldNodes        = new ArrayObject();
1182 89
            $visitedFragmentNames = new ArrayObject();
1183 89
            foreach ($fieldNodes as $fieldNode) {
1184 89
                if (! isset($fieldNode->selectionSet)) {
1185
                    continue;
1186
                }
1187 89
                $subFieldNodes = $this->collectFields(
1188 89
                    $returnType,
1189 89
                    $fieldNode->selectionSet,
1190 89
                    $subFieldNodes,
1191 89
                    $visitedFragmentNames
1192
                );
1193
            }
1194 89
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
1195
        }
1196 89
        return $this->subFieldCache[$returnType][$fieldNodes];
1197
    }
1198
1199
    /**
1200
     * Implements the "Evaluating selection sets" section of the spec
1201
     * for "read" mode.
1202
     *
1203
     * @param mixed|null  $source
1204
     * @param mixed[]     $path
1205
     * @param ArrayObject $fields
1206
     *
1207
     * @return Promise|stdClass|mixed[]
1208
     */
1209 191
    private function executeFields(ObjectType $parentType, $source, $path, $fields)
1210
    {
1211 191
        $containsPromise = false;
1212 191
        $finalResults    = [];
1213 191
        foreach ($fields as $responseName => $fieldNodes) {
1214 191
            $fieldPath   = $path;
1215 191
            $fieldPath[] = $responseName;
1216 191
            $result      = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath);
1217 189
            if ($result === self::$UNDEFINED) {
1218 6
                continue;
1219
            }
1220 186
            if (! $containsPromise && $this->getPromise($result)) {
1221 37
                $containsPromise = true;
1222
            }
1223 186
            $finalResults[$responseName] = $result;
1224
        }
1225
        // If there are no promises, we can just return the object
1226 189
        if (! $containsPromise) {
1227 161
            return self::fixResultsIfEmptyArray($finalResults);
1228
        }
1229
        // Otherwise, results is a map from field name to the result
1230
        // of resolving that field, which is possibly a promise. Return
1231
        // a promise that will return this same map, but with any
1232
        // promises replaced with the values they resolved to.
1233 37
        return $this->promiseForAssocArray($finalResults);
1234
    }
1235
1236
    /**
1237
     * @see https://github.com/webonyx/graphql-php/issues/59
1238
     *
1239
     * @param mixed[] $results
1240
     *
1241
     * @return stdClass|mixed[]
1242
     */
1243 191
    private static function fixResultsIfEmptyArray($results)
1244
    {
1245 191
        if ($results === []) {
1246 5
            return new stdClass();
1247
        }
1248 187
        return $results;
1249
    }
1250
1251
    /**
1252
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1253
     * a `Promise<array<key,scalar|array>>`
1254
     *
1255
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1256
     * any promises.
1257
     *
1258
     * @param (string|Promise)[] $assoc
1259
     *
1260
     * @return mixed
1261
     */
1262 37
    private function promiseForAssocArray(array $assoc)
1263
    {
1264 37
        $keys              = array_keys($assoc);
1265 37
        $valuesAndPromises = array_values($assoc);
1266 37
        $promise           = $this->exeContext->promises->all($valuesAndPromises);
1267
        return $promise->then(static function ($values) use ($keys) {
1268 35
            $resolvedResults = [];
1269 35
            foreach ($values as $i => $value) {
1270 35
                $resolvedResults[$keys[$i]] = $value;
1271
            }
1272 35
            return self::fixResultsIfEmptyArray($resolvedResults);
1273 37
        });
1274
    }
1275
1276
    /**
1277
     * @param string|ObjectType|null $runtimeTypeOrName
1278
     * @param FieldNode[]            $fieldNodes
1279
     * @param mixed                  $result
1280
     *
1281
     * @return ObjectType
1282
     */
1283 29
    private function ensureValidRuntimeType(
1284
        $runtimeTypeOrName,
1285
        AbstractType $returnType,
1286
        ResolveInfo $info,
1287
        &$result
1288
    ) {
1289 29
        $runtimeType = is_string($runtimeTypeOrName) ?
1290 4
            $this->exeContext->schema->getType($runtimeTypeOrName) :
1291 29
            $runtimeTypeOrName;
1292 29
        if (! $runtimeType instanceof ObjectType) {
1293 1
            throw new InvariantViolation(
1294 1
                sprintf(
1295
                    'Abstract type %s must resolve to an Object type at ' .
1296
                    'runtime for field %s.%s with value "%s", received "%s". ' .
1297
                    'Either the %s type should provide a "resolveType" ' .
1298 1
                    'function or each possible type should provide an "isTypeOf" function.',
1299 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

1299
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1300 1
                    $info->parentType,
1301 1
                    $info->fieldName,
1302 1
                    Utils::printSafe($result),
1303 1
                    Utils::printSafe($runtimeType),
1304 1
                    $returnType
1305
                )
1306
            );
1307
        }
1308 28
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1309 4
            throw new InvariantViolation(
1310 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1311
            );
1312
        }
1313 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...
1314 1
            throw new InvariantViolation(
1315 1
                sprintf(
1316
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1317
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1318
                    'type instance as referenced anywhere else within the schema ' .
1319 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1320 1
                    $runtimeType,
1321 1
                    $returnType
1322
                )
1323
            );
1324
        }
1325 27
        return $runtimeType;
1326
    }
1327
}
1328