Failed Conditions
Push — master ( 6e7cf2...804daa )
by Vladimir
04:26
created

Executor   F

Complexity

Total Complexity 179

Size/Duplication

Total Lines 1442
Duplicated Lines 0 %

Test Coverage

Coverage 94.25%

Importance

Changes 0
Metric Value
wmc 179
eloc 590
dl 0
loc 1442
rs 2
c 0
b 0
f 0
ccs 574
cts 609
cp 0.9425

38 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
F buildExecutionContext() 0 77 19
A setDefaultFieldResolver() 0 3 1
A setPromiseAdapter() 0 3 1
A promiseToExecute() 0 28 2
A execute() 0 28 2
A getPromiseAdapter() 0 3 2
A completeAbstractValue() 0 44 3
A getPromise() 0 19 5
A executeOperation() 0 31 4
A collectSubFields() 0 25 5
B getFieldDef() 0 19 9
B getOperationRootType() 0 37 7
A completeLeafValue() 0 15 3
A shouldIncludeNode() 0 28 5
A doExecute() 0 17 2
A doesFragmentConditionMatch() 0 19 4
A executeFields() 0 28 6
A collectAndExecuteSubfields() 0 9 1
C defaultTypeResolver() 0 58 14
A invalidReturnTypeError() 0 8 1
C completeValue() 0 86 13
A resolveField() 0 62 4
A promiseReduce() 0 11 2
A resolveOrError() 0 16 3
A completeValueWithLocatedError() 0 34 4
A completeObjectValue() 0 41 5
A completeValueCatchingError() 0 51 4
A fixResultsIfEmptyArray() 0 7 2
A completeListValue() 0 22 6
A buildResponse() 0 11 3
A promiseForAssocArray() 0 14 2
A executeFieldsSerially() 0 30 4
C collectFields() 0 55 13
B defaultFieldResolver() 0 16 7
A isPromise() 0 3 2
A ensureValidRuntimeType() 0 47 5
A getFieldEntryKey() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Executor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Executor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Executor;
6
7
use ArrayObject;
8
use GraphQL\Error\Error;
9
use GraphQL\Error\InvariantViolation;
10
use GraphQL\Error\Warning;
11
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
12
use GraphQL\Executor\Promise\Promise;
13
use GraphQL\Executor\Promise\PromiseAdapter;
14
use GraphQL\Language\AST\DocumentNode;
15
use GraphQL\Language\AST\FieldNode;
16
use GraphQL\Language\AST\FragmentDefinitionNode;
17
use GraphQL\Language\AST\FragmentSpreadNode;
18
use GraphQL\Language\AST\InlineFragmentNode;
19
use GraphQL\Language\AST\NodeKind;
20
use GraphQL\Language\AST\OperationDefinitionNode;
21
use GraphQL\Language\AST\SelectionSetNode;
22
use GraphQL\Type\Definition\AbstractType;
23
use GraphQL\Type\Definition\Directive;
24
use GraphQL\Type\Definition\FieldDefinition;
25
use GraphQL\Type\Definition\InterfaceType;
26
use GraphQL\Type\Definition\LeafType;
27
use GraphQL\Type\Definition\ListOfType;
28
use GraphQL\Type\Definition\NonNull;
29
use GraphQL\Type\Definition\ObjectType;
30
use GraphQL\Type\Definition\ResolveInfo;
31
use GraphQL\Type\Definition\Type;
32
use GraphQL\Type\Introspection;
33
use GraphQL\Type\Schema;
34
use GraphQL\Utils\TypeInfo;
35
use GraphQL\Utils\Utils;
36
use function array_keys;
37
use function array_merge;
38
use function array_values;
39
use function get_class;
40
use function is_array;
41
use function is_object;
42
use function is_string;
43
use function sprintf;
44
45
/**
46
 * Implements the "Evaluating requests" section of the GraphQL specification.
47
 */
48
class Executor
49
{
50
    /** @var object */
51
    private static $UNDEFINED;
52
53
    /** @var callable|string[] */
54
    private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
55
56
    /** @var PromiseAdapter */
57
    private static $promiseAdapter;
58
59
    /** @var ExecutionContext */
60
    private $exeContext;
61
62
    /** @var \SplObjectStorage */
63
    private $subFieldCache;
64
65 192
    private function __construct(ExecutionContext $context)
66
    {
67 192
        if (! self::$UNDEFINED) {
68 1
            self::$UNDEFINED = Utils::undefined();
69
        }
70
71 192
        $this->exeContext = $context;
72 192
        $this->subFieldCache = new \SplObjectStorage();
73 192
    }
74
75
    /**
76
     * Custom default resolve function
77
     *
78
     * @throws \Exception
79
     */
80
    public static function setDefaultFieldResolver(callable $fn)
81
    {
82
        self::$defaultFieldResolver = $fn;
83
    }
84
85
    /**
86
     * Executes DocumentNode against given $schema.
87
     *
88
     * Always returns ExecutionResult and never throws. All errors which occur during operation
89
     * execution are collected in `$result->errors`.
90
     *
91
     * @api
92
     * @param mixed|null                $rootValue
93
     * @param mixed[]|null              $contextValue
94
     * @param mixed[]|\ArrayAccess|null $variableValues
95
     * @param string|null               $operationName
96
     *
97
     * @return ExecutionResult|Promise
98
     */
99 116
    public static function execute(
100
        Schema $schema,
101
        DocumentNode $ast,
102
        $rootValue = null,
103
        $contextValue = null,
104
        $variableValues = null,
105
        $operationName = null,
106
        ?callable $fieldResolver = null
107
    ) {
108
        // TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
109 116
        $promiseAdapter = self::getPromiseAdapter();
110 116
        $result         = self::promiseToExecute(
111 116
            $promiseAdapter,
112 116
            $schema,
113 116
            $ast,
114 116
            $rootValue,
115 116
            $contextValue,
116 116
            $variableValues,
0 ignored issues
show
Bug introduced by
It seems like $variableValues can also be of type ArrayAccess; however, parameter $variableValues of GraphQL\Executor\Executor::promiseToExecute() does only seem to accept null|array<mixed,mixed>, maybe add an additional type check? ( Ignorable by Annotation )

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

116
            /** @scrutinizer ignore-type */ $variableValues,
Loading history...
117 116
            $operationName,
118 116
            $fieldResolver
119
        );
120
121
        // Wait for promised results when using sync promises
122 116
        if ($promiseAdapter instanceof SyncPromiseAdapter) {
123 116
            $result = $promiseAdapter->wait($result);
124
        }
125
126 116
        return $result;
127
    }
128
129
    /**
130
     * @return PromiseAdapter
131
     */
132 143
    public static function getPromiseAdapter()
133
    {
134 143
        return self::$promiseAdapter ?: (self::$promiseAdapter = new SyncPromiseAdapter());
135
    }
136
137 24
    public static function setPromiseAdapter(?PromiseAdapter $promiseAdapter = null)
138
    {
139 24
        self::$promiseAdapter = $promiseAdapter;
140 24
    }
141
142
    /**
143
     * Same as execute(), but requires promise adapter and returns a promise which is always
144
     * fulfilled with an instance of ExecutionResult and never rejected.
145
     *
146
     * Useful for async PHP platforms.
147
     *
148
     * @api
149
     * @param mixed[]|null $rootValue
150
     * @param mixed[]|null $contextValue
151
     * @param mixed[]|null $variableValues
152
     * @param string|null  $operationName
153
     * @return Promise
154
     */
155 206
    public static function promiseToExecute(
156
        PromiseAdapter $promiseAdapter,
157
        Schema $schema,
158
        DocumentNode $ast,
159
        $rootValue = null,
160
        $contextValue = null,
161
        $variableValues = null,
162
        $operationName = null,
163
        ?callable $fieldResolver = null
164
    ) {
165 206
        $exeContext = self::buildExecutionContext(
166 206
            $schema,
167 206
            $ast,
168 206
            $rootValue,
169 206
            $contextValue,
170 206
            $variableValues,
171 206
            $operationName,
172 206
            $fieldResolver,
173 206
            $promiseAdapter
174
        );
175
176 206
        if (is_array($exeContext)) {
177 15
            return $promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext));
178
        }
179
180 192
        $executor = new self($exeContext);
181
182 192
        return $executor->doExecute();
183
    }
184
185
    /**
186
     * Constructs an ExecutionContext object from the arguments passed to
187
     * execute, which we will pass throughout the other execution methods.
188
     *
189
     * @param mixed[]              $rootValue
190
     * @param mixed[]              $contextValue
191
     * @param mixed[]|\Traversable $rawVariableValues
192
     * @param string|null          $operationName
193
     *
194
     * @return ExecutionContext|Error[]
195
     */
196 206
    private static function buildExecutionContext(
197
        Schema $schema,
198
        DocumentNode $documentNode,
199
        $rootValue,
200
        $contextValue,
201
        $rawVariableValues,
202
        $operationName = null,
203
        ?callable $fieldResolver = null,
204
        ?PromiseAdapter $promiseAdapter = null
205
    ) {
206 206
        $errors    = [];
207 206
        $fragments = [];
208
        /** @var OperationDefinitionNode $operation */
209 206
        $operation                    = null;
210 206
        $hasMultipleAssumedOperations = false;
211
212 206
        foreach ($documentNode->definitions as $definition) {
213 206
            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...
214 206
                case NodeKind::OPERATION_DEFINITION:
215 204
                    if (! $operationName && $operation) {
216 1
                        $hasMultipleAssumedOperations = true;
217
                    }
218 204
                    if (! $operationName ||
219 204
                        (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...
220 203
                        $operation = $definition;
221
                    }
222 204
                    break;
223 15
                case NodeKind::FRAGMENT_DEFINITION:
224 14
                    $fragments[$definition->name->value] = $definition;
225 206
                    break;
226
            }
227
        }
228
229 206
        if (! $operation) {
230 3
            if ($operationName) {
231 1
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
232
            } else {
233 3
                $errors[] = new Error('Must provide an operation.');
234
            }
235 203
        } elseif ($hasMultipleAssumedOperations) {
236 1
            $errors[] = new Error(
237 1
                'Must provide operation name if query contains multiple operations.'
238
            );
239
        }
240
241 206
        $variableValues = null;
242 206
        if ($operation) {
243 203
            $coercedVariableValues = Values::getVariableValues(
244 203
                $schema,
245 203
                $operation->variableDefinitions ?: [],
246 203
                $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

246
                /** @scrutinizer ignore-type */ $rawVariableValues ?: []
Loading history...
247
            );
248
249 203
            if ($coercedVariableValues['errors']) {
250 11
                $errors = array_merge($errors, $coercedVariableValues['errors']);
251
            } else {
252 193
                $variableValues = $coercedVariableValues['coerced'];
253
            }
254
        }
255
256 206
        if ($errors) {
257 15
            return $errors;
258
        }
259
260 192
        Utils::invariant($operation, 'Has operation if no errors.');
261 192
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
262
263 192
        return new ExecutionContext(
264 192
            $schema,
265 192
            $fragments,
266 192
            $rootValue,
267 192
            $contextValue,
268 192
            $operation,
269 192
            $variableValues,
270 192
            $errors,
271 192
            $fieldResolver ?: self::$defaultFieldResolver,
272 192
            $promiseAdapter ?: self::getPromiseAdapter()
273
        );
274
    }
275
276
    /**
277
     * @return Promise
278
     */
279 192
    private function doExecute()
280
    {
281
        // Return a Promise that will eventually resolve to the data described by
282
        // The "Response" section of the GraphQL specification.
283
        //
284
        // If errors are encountered while executing a GraphQL field, only that
285
        // field and its descendants will be omitted, and sibling fields will still
286
        // be executed. An execution which encounters errors will still result in a
287
        // resolved Promise.
288 192
        $data = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
289 192
        $result = $this->buildResponse($data);
290
291
        // Note: we deviate here from the reference implementation a bit by always returning promise
292
        // But for the "sync" case it is always fulfilled
293 192
        return $this->isPromise($result)
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->isPromise(...reateFulfilled($result) also could return the type GraphQL\Executor\ExecutionResult which is incompatible with the documented return type GraphQL\Executor\Promise\Promise.
Loading history...
294 39
            ? $result
295 192
            : $this->exeContext->promises->createFulfilled($result);
296
    }
297
298
    /**
299
     * @param mixed|null|Promise $data
300
     * @return ExecutionResult|Promise
301
     */
302 192
    private function buildResponse($data)
303
    {
304 192
        if ($this->isPromise($data)) {
305
            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

305
            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...
306 39
                return $this->buildResponse($resolved);
307 39
            });
308
        }
309 192
        if ($data !== null) {
310 188
            $data = (array) $data;
311
        }
312 192
        return new ExecutionResult($data, $this->exeContext->errors);
313
    }
314
315
    /**
316
     * Implements the "Evaluating operations" section of the spec.
317
     *
318
     * @param  mixed[] $rootValue
319
     * @return Promise|\stdClass|mixed[]
320
     */
321 192
    private function executeOperation(OperationDefinitionNode $operation, $rootValue)
322
    {
323 192
        $type   = $this->getOperationRootType($this->exeContext->schema, $operation);
324 192
        $fields = $this->collectFields($type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
325
326 192
        $path = [];
327
328
        // Errors from sub-fields of a NonNull type may propagate to the top level,
329
        // at which point we still log the error and null the parent field, which
330
        // in this case is the entire response.
331
        //
332
        // Similar to completeValueCatchingError.
333
        try {
334 192
            $result = $operation->operation === 'mutation' ?
335 6
                $this->executeFieldsSerially($type, $rootValue, $path, $fields) :
336 192
                $this->executeFields($type, $rootValue, $path, $fields);
337
338 190
            if ($this->isPromise($result)) {
339 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

339
                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...
340 39
                    null,
341
                    function ($error) {
342 2
                        $this->exeContext->addError($error);
343 2
                        return $this->exeContext->promises->createFulfilled(null);
344 39
                    }
345
                );
346
            }
347
348 152
            return $result;
349 2
        } catch (Error $error) {
350 2
            $this->exeContext->addError($error);
351 2
            return null;
352
        }
353
    }
354
355
    /**
356
     * Extracts the root type of the operation from the schema.
357
     *
358
     * @return ObjectType
359
     * @throws Error
360
     */
361 192
    private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation)
362
    {
363 192
        switch ($operation->operation) {
364 192
            case 'query':
365 184
                $queryType = $schema->getQueryType();
366 184
                if (! $queryType) {
0 ignored issues
show
introduced by
$queryType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
367
                    throw new Error(
368
                        'Schema does not define the required query root type.',
369
                        [$operation]
370
                    );
371
                }
372
373 184
                return $queryType;
374 8
            case 'mutation':
375 6
                $mutationType = $schema->getMutationType();
376 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...
377
                    throw new Error(
378
                        'Schema is not configured for mutations.',
379
                        [$operation]
380
                    );
381
                }
382
383 6
                return $mutationType;
384 2
            case 'subscription':
385 2
                $subscriptionType = $schema->getSubscriptionType();
386 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...
387
                    throw new Error(
388
                        'Schema is not configured for subscriptions.',
389
                        [$operation]
390
                    );
391
                }
392
393 2
                return $subscriptionType;
394
            default:
395
                throw new Error(
396
                    'Can only execute queries, mutations and subscriptions.',
397
                    [$operation]
398
                );
399
        }
400
    }
401
402
    /**
403
     * Given a selectionSet, adds all of the fields in that selection to
404
     * the passed in map of fields, and returns it at the end.
405
     *
406
     * CollectFields requires the "runtime type" of an object. For a field which
407
     * returns an Interface or Union type, the "runtime type" will be the actual
408
     * Object type returned by that field.
409
     *
410
     * @param ArrayObject $fields
411
     * @param ArrayObject $visitedFragmentNames
412
     *
413
     * @return \ArrayObject
414
     */
415 192
    private function collectFields(
416
        ObjectType $runtimeType,
417
        SelectionSetNode $selectionSet,
418
        $fields,
419
        $visitedFragmentNames
420
    ) {
421 192
        $exeContext = $this->exeContext;
422 192
        foreach ($selectionSet->selections as $selection) {
423 192
            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...
424 192
                case NodeKind::FIELD:
425 192
                    if (! $this->shouldIncludeNode($selection)) {
426 2
                        break;
427
                    }
428 192
                    $name = self::getFieldEntryKey($selection);
429 192
                    if (! isset($fields[$name])) {
430 192
                        $fields[$name] = new \ArrayObject();
431
                    }
432 192
                    $fields[$name][] = $selection;
433 192
                    break;
434 29
                case NodeKind::INLINE_FRAGMENT:
435 21
                    if (! $this->shouldIncludeNode($selection) ||
436 21
                        ! $this->doesFragmentConditionMatch($selection, $runtimeType)
437
                    ) {
438 19
                        break;
439
                    }
440 21
                    $this->collectFields(
441 21
                        $runtimeType,
442 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...
443 21
                        $fields,
444 21
                        $visitedFragmentNames
445
                    );
446 21
                    break;
447 10
                case NodeKind::FRAGMENT_SPREAD:
448 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...
449 10
                    if (! empty($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) {
450 2
                        break;
451
                    }
452 10
                    $visitedFragmentNames[$fragName] = true;
453
454
                    /** @var FragmentDefinitionNode|null $fragment */
455 10
                    $fragment = $exeContext->fragments[$fragName] ?? null;
456 10
                    if (! $fragment || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
457 1
                        break;
458
                    }
459 9
                    $this->collectFields(
460 9
                        $runtimeType,
461 9
                        $fragment->selectionSet,
462 9
                        $fields,
463 9
                        $visitedFragmentNames
464
                    );
465 192
                    break;
466
            }
467
        }
468
469 192
        return $fields;
470
    }
471
472
    /**
473
     * Determines if a field should be included based on the @include and @skip
474
     * directives, where @skip has higher precedence than @include.
475
     *
476
     * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
477
     * @return bool
478
     */
479 192
    private function shouldIncludeNode($node)
480
    {
481 192
        $variableValues = $this->exeContext->variableValues;
482 192
        $skipDirective  = Directive::skipDirective();
483
484 192
        $skip = Values::getDirectiveValues(
485 192
            $skipDirective,
486 192
            $node,
487 192
            $variableValues
488
        );
489
490 192
        if (isset($skip['if']) && $skip['if'] === true) {
491 5
            return false;
492
        }
493
494 192
        $includeDirective = Directive::includeDirective();
495
496 192
        $include = Values::getDirectiveValues(
497 192
            $includeDirective,
498 192
            $node,
499 192
            $variableValues
500
        );
501
502 192
        if (isset($include['if']) && $include['if'] === false) {
503 5
            return false;
504
        }
505
506 192
        return true;
507
    }
508
509
    /**
510
     * Implements the logic to compute the key of a given fields entry
511
     *
512
     * @return string
513
     */
514 192
    private static function getFieldEntryKey(FieldNode $node)
515
    {
516 192
        return $node->alias ? $node->alias->value : $node->name->value;
517
    }
518
519
    /**
520
     * Determines if a fragment is applicable to the given type.
521
     *
522
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
523
     * @return bool
524
     */
525 29
    private function doesFragmentConditionMatch(
526
        $fragment,
527
        ObjectType $type
528
    ) {
529 29
        $typeConditionNode = $fragment->typeCondition;
530
531 29
        if ($typeConditionNode === null) {
532 1
            return true;
533
        }
534
535 28
        $conditionalType = TypeInfo::typeFromAST($this->exeContext->schema, $typeConditionNode);
536 28
        if ($conditionalType === $type) {
0 ignored issues
show
introduced by
The condition $conditionalType === $type is always false.
Loading history...
537 27
            return true;
538
        }
539 18
        if ($conditionalType instanceof AbstractType) {
540 1
            return $this->exeContext->schema->isPossibleType($conditionalType, $type);
541
        }
542
543 18
        return false;
544
    }
545
546
    /**
547
     * Implements the "Evaluating selection sets" section of the spec
548
     * for "write" mode.
549
     *
550
     * @param mixed[]     $sourceValue
551
     * @param mixed[]     $path
552
     * @param ArrayObject $fields
553
     * @return Promise|\stdClass|mixed[]
554
     */
555 6
    private function executeFieldsSerially(ObjectType $parentType, $sourceValue, $path, $fields)
556
    {
557 6
        $result = $this->promiseReduce(
558 6
            array_keys($fields->getArrayCopy()),
559
            function ($results, $responseName) use ($path, $parentType, $sourceValue, $fields) {
560 6
                $fieldNodes  = $fields[$responseName];
561 6
                $fieldPath   = $path;
562 6
                $fieldPath[] = $responseName;
563 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 object|null expected by parameter $source of GraphQL\Executor\Executor::resolveField(). ( Ignorable by Annotation )

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

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

1092
            /** @scrutinizer ignore-call */ 
1093
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
1093
        }
1094
1095 31
        $promise = $this->getPromise($runtimeType);
1096 31
        if ($promise) {
1097
            return $promise->then(function ($resolvedRuntimeType) use (
1098 5
                $returnType,
1099 5
                $fieldNodes,
1100 5
                $info,
1101 5
                $path,
1102 5
                &$result
1103
            ) {
1104 5
                return $this->completeObjectValue(
1105 5
                    $this->ensureValidRuntimeType(
1106 5
                        $resolvedRuntimeType,
1107 5
                        $returnType,
1108 5
                        $info,
1109 5
                        $result
1110
                    ),
1111 5
                    $fieldNodes,
1112 5
                    $info,
1113 5
                    $path,
1114 5
                    $result
1115
                );
1116 7
            });
1117
        }
1118
1119 24
        return $this->completeObjectValue(
1120 24
            $this->ensureValidRuntimeType(
1121 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\Executo...nsureValidRuntimeType() does only seem to accept null|string|GraphQL\Type\Definition\ObjectType, maybe add an additional type check? ( Ignorable by Annotation )

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

1121
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
1122 24
                $returnType,
1123 24
                $info,
1124 24
                $result
1125
            ),
1126 22
            $fieldNodes,
1127 22
            $info,
1128 22
            $path,
1129 22
            $result
1130
        );
1131
    }
1132
1133
    /**
1134
     * If a resolveType function is not given, then a default resolve behavior is
1135
     * used which attempts two strategies:
1136
     *
1137
     * First, See if the provided value has a `__typename` field defined, if so, use
1138
     * that value as name of the resolved type.
1139
     *
1140
     * Otherwise, test each possible type for the abstract type by calling
1141
     * isTypeOf for the object being coerced, returning the first type that matches.
1142
     *
1143
     * @param mixed|null $value
1144
     * @param mixed|null $context
1145
     * @return ObjectType|Promise|null
1146
     */
1147 11
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
1148
    {
1149
        // First, look for `__typename`.
1150 11
        if ($value !== null &&
1151 11
            is_array($value) &&
1152 11
            isset($value['__typename']) &&
1153 11
            is_string($value['__typename'])
1154
        ) {
1155 2
            return $value['__typename'];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value['__typename'] returns the type string which is incompatible with the documented return type null|GraphQL\Executor\Pr...e\Definition\ObjectType.
Loading history...
1156
        }
1157
1158 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
1159 1
            Warning::warnOnce(
1160 1
                sprintf(
1161
                    'GraphQL Interface Type `%s` returned `null` from it`s `resolveType` function ' .
1162
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1163
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1164 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1165 1
                    $abstractType->name,
1166 1
                    Utils::printSafe($value)
1167
                ),
1168 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1169
            );
1170
        }
1171
1172
        // Otherwise, test each possible type.
1173 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1174 9
        $promisedIsTypeOfResults = [];
1175
1176 9
        foreach ($possibleTypes as $index => $type) {
1177 9
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
1178
1179 9
            if ($isTypeOfResult === null) {
1180
                continue;
1181
            }
1182
1183 9
            $promise = $this->getPromise($isTypeOfResult);
1184 9
            if ($promise) {
1185 3
                $promisedIsTypeOfResults[$index] = $promise;
1186 6
            } elseif ($isTypeOfResult) {
1187 9
                return $type;
1188
            }
1189
        }
1190
1191 3
        if (! empty($promisedIsTypeOfResults)) {
1192 3
            return $this->exeContext->promises->all($promisedIsTypeOfResults)
1193
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
1194 2
                    foreach ($isTypeOfResults as $index => $result) {
1195 2
                        if ($result) {
1196 2
                            return $possibleTypes[$index];
1197
                        }
1198
                    }
1199
1200
                    return null;
1201 3
                });
1202
        }
1203
1204
        return null;
1205
    }
1206
1207
    /**
1208
     * Complete an Object value by executing all sub-selections.
1209
     *
1210
     * @param FieldNode[] $fieldNodes
1211
     * @param mixed[]     $path
1212
     * @param mixed       $result
1213
     * @return mixed[]|Promise|\stdClass
1214
     * @throws Error
1215
     */
1216 89
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1217
    {
1218
        // If there is an isTypeOf predicate function, call it with the
1219
        // current result. If isTypeOf returns false, then raise an error rather
1220
        // than continuing execution.
1221 89
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1222
1223 89
        if ($isTypeOf !== null) {
1224 11
            $promise = $this->getPromise($isTypeOf);
1225 11
            if ($promise) {
1226
                return $promise->then(function ($isTypeOfResult) use (
1227 2
                    $returnType,
1228 2
                    $fieldNodes,
1229 2
                    $info,
1230 2
                    $path,
1231 2
                    &$result
1232
                ) {
1233 2
                    if (! $isTypeOfResult) {
1234
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1235
                    }
1236
1237 2
                    return $this->collectAndExecuteSubfields(
1238 2
                        $returnType,
1239 2
                        $fieldNodes,
1240 2
                        $info,
1241 2
                        $path,
1242 2
                        $result
1243
                    );
1244 2
                });
1245
            }
1246 9
            if (! $isTypeOf) {
1247 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1248
            }
1249
        }
1250
1251 87
        return $this->collectAndExecuteSubfields(
1252 87
            $returnType,
1253 87
            $fieldNodes,
1254 87
            $info,
1255 87
            $path,
1256 87
            $result
1257
        );
1258
    }
1259
1260
    /**
1261
     * @param mixed[]     $result
1262
     * @param FieldNode[] $fieldNodes
1263
     * @return Error
1264
     */
1265 1
    private function invalidReturnTypeError(
1266
        ObjectType $returnType,
1267
        $result,
1268
        $fieldNodes
1269
    ) {
1270 1
        return new Error(
1271 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1272 1
            $fieldNodes
1273
        );
1274
    }
1275
1276
    /**
1277
     * @param FieldNode[] $fieldNodes
1278
     * @param mixed[]     $path
1279
     * @param mixed[]     $result
1280
     * @return mixed[]|Promise|\stdClass
1281
     * @throws Error
1282
     */
1283 89
    private function collectAndExecuteSubfields(
1284
        ObjectType $returnType,
1285
        $fieldNodes,
1286
        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

1286
        /** @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...
1287
        $path,
1288
        &$result
1289
    ) {
1290 89
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
1291 89
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1292
    }
1293
1294
    /**
1295
     * @param ObjectType $returnType
1296
     * @param $fieldNodes
1297
     * @return ArrayObject
1298
     */
1299 89
    private function collectSubFields(ObjectType $returnType, $fieldNodes): ArrayObject
1300
    {
1301 89
        if (!isset($this->subFieldCache[$returnType])) {
1302 89
            $this->subFieldCache[$returnType] = new \SplObjectStorage();
1303
        }
1304 89
        if (!isset($this->subFieldCache[$returnType][$fieldNodes])) {
1305
            // Collect sub-fields to execute to complete this value.
1306 89
            $subFieldNodes = new \ArrayObject();
1307 89
            $visitedFragmentNames = new \ArrayObject();
1308
1309 89
            foreach ($fieldNodes as $fieldNode) {
1310 89
                if (!isset($fieldNode->selectionSet)) {
1311
                    continue;
1312
                }
1313
1314 89
                $subFieldNodes = $this->collectFields(
1315 89
                    $returnType,
1316 89
                    $fieldNode->selectionSet,
1317 89
                    $subFieldNodes,
1318 89
                    $visitedFragmentNames
1319
                );
1320
            }
1321 89
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
1322
        }
1323 89
        return $this->subFieldCache[$returnType][$fieldNodes];
1324
    }
1325
1326
    /**
1327
     * Implements the "Evaluating selection sets" section of the spec
1328
     * for "read" mode.
1329
     *
1330
     * @param mixed|null  $source
1331
     * @param mixed[]     $path
1332
     * @param ArrayObject $fields
1333
     * @return Promise|\stdClass|mixed[]
1334
     */
1335 188
    private function executeFields(ObjectType $parentType, $source, $path, $fields)
1336
    {
1337 188
        $containsPromise = false;
1338 188
        $finalResults    = [];
1339
1340 188
        foreach ($fields as $responseName => $fieldNodes) {
1341 188
            $fieldPath   = $path;
1342 188
            $fieldPath[] = $responseName;
1343 188
            $result      = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath);
1344 186
            if ($result === self::$UNDEFINED) {
1345 6
                continue;
1346
            }
1347 183
            if (! $containsPromise && $this->getPromise($result)) {
1348 37
                $containsPromise = true;
1349
            }
1350 183
            $finalResults[$responseName] = $result;
1351
        }
1352
1353
        // If there are no promises, we can just return the object
1354 186
        if (! $containsPromise) {
1355 158
            return self::fixResultsIfEmptyArray($finalResults);
1356
        }
1357
1358
        // Otherwise, results is a map from field name to the result
1359
        // of resolving that field, which is possibly a promise. Return
1360
        // a promise that will return this same map, but with any
1361
        // promises replaced with the values they resolved to.
1362 37
        return $this->promiseForAssocArray($finalResults);
1363
    }
1364
1365
    /**
1366
     * @see https://github.com/webonyx/graphql-php/issues/59
1367
     *
1368
     * @param mixed[] $results
1369
     * @return \stdClass|mixed[]
1370
     */
1371 188
    private static function fixResultsIfEmptyArray($results)
1372
    {
1373 188
        if ($results === []) {
1374 5
            return new \stdClass();
1375
        }
1376
1377 184
        return $results;
1378
    }
1379
1380
    /**
1381
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1382
     * a `Promise<array<key,scalar|array>>`
1383
     *
1384
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1385
     * any promises.
1386
     *
1387
     * @param (string|Promise)[] $assoc
1388
     * @return mixed
1389
     */
1390 37
    private function promiseForAssocArray(array $assoc)
1391
    {
1392 37
        $keys              = array_keys($assoc);
1393 37
        $valuesAndPromises = array_values($assoc);
1394
1395 37
        $promise = $this->exeContext->promises->all($valuesAndPromises);
1396
1397
        return $promise->then(function ($values) use ($keys) {
1398 35
            $resolvedResults = [];
1399 35
            foreach ($values as $i => $value) {
1400 35
                $resolvedResults[$keys[$i]] = $value;
1401
            }
1402
1403 35
            return self::fixResultsIfEmptyArray($resolvedResults);
1404 37
        });
1405
    }
1406
1407
    /**
1408
     * @param string|ObjectType|null $runtimeTypeOrName
1409
     * @param FieldNode[]            $fieldNodes
1410
     * @param mixed                  $result
1411
     * @return ObjectType
1412
     */
1413 29
    private function ensureValidRuntimeType(
1414
        $runtimeTypeOrName,
1415
        AbstractType $returnType,
1416
        ResolveInfo $info,
1417
        &$result
1418
    ) {
1419 29
        $runtimeType = is_string($runtimeTypeOrName) ?
1420 4
            $this->exeContext->schema->getType($runtimeTypeOrName) :
1421 29
            $runtimeTypeOrName;
1422
1423 29
        if (! $runtimeType instanceof ObjectType) {
1424 1
            throw new InvariantViolation(
1425 1
                sprintf(
1426
                    'Abstract type %s must resolve to an Object type at ' .
1427
                    'runtime for field %s.%s with value "%s", received "%s". ' .
1428
                    'Either the %s type should provide a "resolveType" ' .
1429 1
                    'function or each possible type should provide an "isTypeOf" function.',
1430 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

1430
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1431 1
                    $info->parentType,
1432 1
                    $info->fieldName,
1433 1
                    Utils::printSafe($result),
1434 1
                    Utils::printSafe($runtimeType),
1435 1
                    $returnType
1436
                )
1437
            );
1438
        }
1439
1440 28
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1441 4
            throw new InvariantViolation(
1442 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1443
            );
1444
        }
1445
1446 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...
1447 1
            throw new InvariantViolation(
1448 1
                sprintf(
1449
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1450
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1451
                    'type instance as referenced anywhere else within the schema ' .
1452 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1453 1
                    $runtimeType,
1454 1
                    $returnType
1455
                )
1456
            );
1457
        }
1458
1459 27
        return $runtimeType;
1460
    }
1461
1462
    /**
1463
     * If a resolve function is not given, then a default resolve behavior is used
1464
     * which takes the property of the source object of the same name as the field
1465
     * and returns it as the result, or if it's a function, returns the result
1466
     * of calling that function while passing along args and context.
1467
     *
1468
     * @param mixed        $source
1469
     * @param mixed[]      $args
1470
     * @param mixed[]|null $context
1471
     *
1472
     * @return mixed|null
1473
     */
1474 115
    public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info)
1475
    {
1476 115
        $fieldName = $info->fieldName;
1477 115
        $property  = null;
1478
1479 115
        if (is_array($source) || $source instanceof \ArrayAccess) {
1480 73
            if (isset($source[$fieldName])) {
1481 73
                $property = $source[$fieldName];
1482
            }
1483 42
        } elseif (is_object($source)) {
1484 41
            if (isset($source->{$fieldName})) {
1485 41
                $property = $source->{$fieldName};
1486
            }
1487
        }
1488
1489 115
        return $property instanceof \Closure ? $property($source, $args, $context, $info) : $property;
1490
    }
1491
}
1492