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

Executor::promiseToExecute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 28
c 0
b 0
f 0
rs 9.8333
ccs 14
cts 14
cp 1
cc 2
nc 2
nop 8
crap 2

How to fix   Many Parameters   

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 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