Passed
Push — master ( 5fb970...6cce67 )
by Vladimir
08:12 queued 11s
created

Executor::buildExecutionContext()   F

Complexity

Conditions 19
Paths 168

Size

Total Lines 77
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 19

Importance

Changes 0
Metric Value
eloc 48
dl 0
loc 77
ccs 47
cts 47
cp 1
rs 3.95
c 0
b 0
f 0
cc 19
nc 168
nop 8
crap 19

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

258
                /** @scrutinizer ignore-type */ $rawVariableValues ?: []
Loading history...
259
            );
260
261 205
            if (empty($coercedVariableValues['errors'])) {
262 195
                $variableValues = $coercedVariableValues['coerced'];
263
            } else {
264 11
                $errors = array_merge($errors, $coercedVariableValues['errors']);
265
            }
266
        }
267
268 208
        if (! empty($errors)) {
269 15
            return $errors;
270
        }
271
272 194
        Utils::invariant($operation, 'Has operation if no errors.');
273 194
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
274
275 194
        return new ExecutionContext(
276 194
            $schema,
277 194
            $fragments,
278 194
            $rootValue,
279 194
            $contextValue,
280 194
            $operation,
281 194
            $variableValues,
282 194
            $errors,
283 194
            $fieldResolver ?: self::$defaultFieldResolver,
284 194
            $promiseAdapter ?: self::getPromiseAdapter()
285
        );
286
    }
287
288
    /**
289
     * @return Promise
290
     */
291 194
    private function doExecute()
292
    {
293
        // Return a Promise that will eventually resolve to the data described by
294
        // The "Response" section of the GraphQL specification.
295
        //
296
        // If errors are encountered while executing a GraphQL field, only that
297
        // field and its descendants will be omitted, and sibling fields will still
298
        // be executed. An execution which encounters errors will still result in a
299
        // resolved Promise.
300 194
        $data   = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
301 194
        $result = $this->buildResponse($data);
302
303
        // Note: we deviate here from the reference implementation a bit by always returning promise
304
        // But for the "sync" case it is always fulfilled
305 194
        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...
306 39
            ? $result
307 194
            : $this->exeContext->promises->createFulfilled($result);
308
    }
309
310
    /**
311
     * @param mixed|Promise|null $data
312
     *
313
     * @return ExecutionResult|Promise
314
     */
315 194
    private function buildResponse($data)
316
    {
317 194
        if ($this->isPromise($data)) {
318
            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

318
            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...
319 39
                return $this->buildResponse($resolved);
320 39
            });
321
        }
322 194
        if ($data !== null) {
323 190
            $data = (array) $data;
324
        }
325
326 194
        return new ExecutionResult($data, $this->exeContext->errors);
327
    }
328
329
    /**
330
     * Implements the "Evaluating operations" section of the spec.
331
     *
332
     * @param  mixed[] $rootValue
333
     *
334
     * @return Promise|stdClass|mixed[]
335
     */
336 194
    private function executeOperation(OperationDefinitionNode $operation, $rootValue)
337
    {
338 194
        $type   = $this->getOperationRootType($this->exeContext->schema, $operation);
339 194
        $fields = $this->collectFields($type, $operation->selectionSet, new ArrayObject(), new ArrayObject());
340
341 194
        $path = [];
342
343
        // Errors from sub-fields of a NonNull type may propagate to the top level,
344
        // at which point we still log the error and null the parent field, which
345
        // in this case is the entire response.
346
        //
347
        // Similar to completeValueCatchingError.
348
        try {
349 194
            $result = $operation->operation === 'mutation' ?
350 6
                $this->executeFieldsSerially($type, $rootValue, $path, $fields) :
351 194
                $this->executeFields($type, $rootValue, $path, $fields);
352
353 192
            if ($this->isPromise($result)) {
354 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

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

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

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

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

1135
            /** @scrutinizer ignore-call */ 
1136
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
1136
        }
1137
1138 31
        $promise = $this->getPromise($runtimeType);
1139 31
        if ($promise) {
1140
            return $promise->then(function ($resolvedRuntimeType) use (
1141 5
                $returnType,
1142 5
                $fieldNodes,
1143 5
                $info,
1144 5
                $path,
1145 5
                &$result
1146
            ) {
1147 5
                return $this->completeObjectValue(
1148 5
                    $this->ensureValidRuntimeType(
1149 5
                        $resolvedRuntimeType,
1150 5
                        $returnType,
1151 5
                        $info,
1152 5
                        $result
1153
                    ),
1154 5
                    $fieldNodes,
1155 5
                    $info,
1156 5
                    $path,
1157 5
                    $result
1158
                );
1159 7
            });
1160
        }
1161
1162 24
        return $this->completeObjectValue(
1163 24
            $this->ensureValidRuntimeType(
1164 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 GraphQL\Type\Definition\ObjectType|null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

1164
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
1165 24
                $returnType,
1166 24
                $info,
1167 24
                $result
1168
            ),
1169 22
            $fieldNodes,
1170 22
            $info,
1171 22
            $path,
1172 22
            $result
1173
        );
1174
    }
1175
1176
    /**
1177
     * If a resolveType function is not given, then a default resolve behavior is
1178
     * used which attempts two strategies:
1179
     *
1180
     * First, See if the provided value has a `__typename` field defined, if so, use
1181
     * that value as name of the resolved type.
1182
     *
1183
     * Otherwise, test each possible type for the abstract type by calling
1184
     * isTypeOf for the object being coerced, returning the first type that matches.
1185
     *
1186
     * @param mixed|null $value
1187
     * @param mixed|null $context
1188
     *
1189
     * @return ObjectType|Promise|null
1190
     */
1191 11
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
1192
    {
1193
        // First, look for `__typename`.
1194 11
        if ($value !== null &&
1195 11
            (is_array($value) || $value instanceof ArrayAccess) &&
1196 11
            isset($value['__typename']) &&
1197 11
            is_string($value['__typename'])
1198
        ) {
1199 2
            return $value['__typename'];
1200
        }
1201
1202 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
0 ignored issues
show
Bug introduced by
The method getConfig() does not exist on null. ( Ignorable by Annotation )

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

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

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1203 1
            Warning::warnOnce(
1204 1
                sprintf(
1205
                    'GraphQL Interface Type `%s` returned `null` from it`s `resolveType` function ' .
1206
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1207
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1208 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1209 1
                    $abstractType->name,
1210 1
                    Utils::printSafe($value)
1211
                ),
1212 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1213
            );
1214
        }
1215
1216
        // Otherwise, test each possible type.
1217 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1218 9
        $promisedIsTypeOfResults = [];
1219
1220 9
        foreach ($possibleTypes as $index => $type) {
1221 9
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
1222
1223 9
            if ($isTypeOfResult === null) {
1224
                continue;
1225
            }
1226
1227 9
            $promise = $this->getPromise($isTypeOfResult);
1228 9
            if ($promise) {
1229 3
                $promisedIsTypeOfResults[$index] = $promise;
1230 6
            } elseif ($isTypeOfResult) {
1231 9
                return $type;
1232
            }
1233
        }
1234
1235 3
        if (! empty($promisedIsTypeOfResults)) {
1236 3
            return $this->exeContext->promises->all($promisedIsTypeOfResults)
1237
                ->then(static function ($isTypeOfResults) use ($possibleTypes) {
1238 2
                    foreach ($isTypeOfResults as $index => $result) {
1239 2
                        if ($result) {
1240 2
                            return $possibleTypes[$index];
1241
                        }
1242
                    }
1243
1244
                    return null;
1245 3
                });
1246
        }
1247
1248
        return null;
1249
    }
1250
1251
    /**
1252
     * Complete an Object value by executing all sub-selections.
1253
     *
1254
     * @param FieldNode[] $fieldNodes
1255
     * @param mixed[]     $path
1256
     * @param mixed       $result
1257
     *
1258
     * @return mixed[]|Promise|stdClass
1259
     *
1260
     * @throws Error
1261
     */
1262 89
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1263
    {
1264
        // If there is an isTypeOf predicate function, call it with the
1265
        // current result. If isTypeOf returns false, then raise an error rather
1266
        // than continuing execution.
1267 89
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1268
1269 89
        if ($isTypeOf !== null) {
1270 11
            $promise = $this->getPromise($isTypeOf);
1271 11
            if ($promise) {
1272
                return $promise->then(function ($isTypeOfResult) use (
1273 2
                    $returnType,
1274 2
                    $fieldNodes,
1275 2
                    $info,
1276 2
                    $path,
1277 2
                    &$result
1278
                ) {
1279 2
                    if (! $isTypeOfResult) {
1280
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1281
                    }
1282
1283 2
                    return $this->collectAndExecuteSubfields(
1284 2
                        $returnType,
1285 2
                        $fieldNodes,
1286 2
                        $info,
1287 2
                        $path,
1288 2
                        $result
1289
                    );
1290 2
                });
1291
            }
1292 9
            if (! $isTypeOf) {
1293 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1294
            }
1295
        }
1296
1297 87
        return $this->collectAndExecuteSubfields(
1298 87
            $returnType,
1299 87
            $fieldNodes,
1300 87
            $info,
1301 87
            $path,
1302 87
            $result
1303
        );
1304
    }
1305
1306
    /**
1307
     * @param mixed[]     $result
1308
     * @param FieldNode[] $fieldNodes
1309
     *
1310
     * @return Error
1311
     */
1312 1
    private function invalidReturnTypeError(
1313
        ObjectType $returnType,
1314
        $result,
1315
        $fieldNodes
1316
    ) {
1317 1
        return new Error(
1318 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1319 1
            $fieldNodes
1320
        );
1321
    }
1322
1323
    /**
1324
     * @param FieldNode[] $fieldNodes
1325
     * @param mixed[]     $path
1326
     * @param mixed[]     $result
1327
     *
1328
     * @return mixed[]|Promise|stdClass
1329
     *
1330
     * @throws Error
1331
     */
1332 89
    private function collectAndExecuteSubfields(
1333
        ObjectType $returnType,
1334
        $fieldNodes,
1335
        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

1335
        /** @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...
1336
        $path,
1337
        &$result
1338
    ) {
1339 89
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
1340
1341 89
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1342
    }
1343
1344 89
    private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayObject
1345
    {
1346 89
        if (! isset($this->subFieldCache[$returnType])) {
1347 89
            $this->subFieldCache[$returnType] = new SplObjectStorage();
1348
        }
1349 89
        if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
1350
            // Collect sub-fields to execute to complete this value.
1351 89
            $subFieldNodes        = new ArrayObject();
1352 89
            $visitedFragmentNames = new ArrayObject();
1353
1354 89
            foreach ($fieldNodes as $fieldNode) {
1355 89
                if (! isset($fieldNode->selectionSet)) {
1356
                    continue;
1357
                }
1358
1359 89
                $subFieldNodes = $this->collectFields(
1360 89
                    $returnType,
1361 89
                    $fieldNode->selectionSet,
1362 89
                    $subFieldNodes,
1363 89
                    $visitedFragmentNames
1364
                );
1365
            }
1366 89
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
1367
        }
1368
1369 89
        return $this->subFieldCache[$returnType][$fieldNodes];
1370
    }
1371
1372
    /**
1373
     * Implements the "Evaluating selection sets" section of the spec
1374
     * for "read" mode.
1375
     *
1376
     * @param mixed|null  $source
1377
     * @param mixed[]     $path
1378
     * @param ArrayObject $fields
1379
     *
1380
     * @return Promise|stdClass|mixed[]
1381
     */
1382 190
    private function executeFields(ObjectType $parentType, $source, $path, $fields)
1383
    {
1384 190
        $containsPromise = false;
1385 190
        $finalResults    = [];
1386
1387 190
        foreach ($fields as $responseName => $fieldNodes) {
1388 190
            $fieldPath   = $path;
1389 190
            $fieldPath[] = $responseName;
1390 190
            $result      = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath);
1391 188
            if ($result === self::$UNDEFINED) {
1392 6
                continue;
1393
            }
1394 185
            if (! $containsPromise && $this->getPromise($result)) {
1395 37
                $containsPromise = true;
1396
            }
1397 185
            $finalResults[$responseName] = $result;
1398
        }
1399
1400
        // If there are no promises, we can just return the object
1401 188
        if (! $containsPromise) {
1402 160
            return self::fixResultsIfEmptyArray($finalResults);
1403
        }
1404
1405
        // Otherwise, results is a map from field name to the result
1406
        // of resolving that field, which is possibly a promise. Return
1407
        // a promise that will return this same map, but with any
1408
        // promises replaced with the values they resolved to.
1409 37
        return $this->promiseForAssocArray($finalResults);
1410
    }
1411
1412
    /**
1413
     * @see https://github.com/webonyx/graphql-php/issues/59
1414
     *
1415
     * @param mixed[] $results
1416
     *
1417
     * @return stdClass|mixed[]
1418
     */
1419 190
    private static function fixResultsIfEmptyArray($results)
1420
    {
1421 190
        if ($results === []) {
1422 5
            return new stdClass();
1423
        }
1424
1425 186
        return $results;
1426
    }
1427
1428
    /**
1429
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1430
     * a `Promise<array<key,scalar|array>>`
1431
     *
1432
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1433
     * any promises.
1434
     *
1435
     * @param (string|Promise)[] $assoc
1436
     *
1437
     * @return mixed
1438
     */
1439 37
    private function promiseForAssocArray(array $assoc)
1440
    {
1441 37
        $keys              = array_keys($assoc);
1442 37
        $valuesAndPromises = array_values($assoc);
1443
1444 37
        $promise = $this->exeContext->promises->all($valuesAndPromises);
1445
1446
        return $promise->then(static function ($values) use ($keys) {
1447 35
            $resolvedResults = [];
1448 35
            foreach ($values as $i => $value) {
1449 35
                $resolvedResults[$keys[$i]] = $value;
1450
            }
1451
1452 35
            return self::fixResultsIfEmptyArray($resolvedResults);
1453 37
        });
1454
    }
1455
1456
    /**
1457
     * @param string|ObjectType|null $runtimeTypeOrName
1458
     * @param FieldNode[]            $fieldNodes
1459
     * @param mixed                  $result
1460
     *
1461
     * @return ObjectType
1462
     */
1463 29
    private function ensureValidRuntimeType(
1464
        $runtimeTypeOrName,
1465
        AbstractType $returnType,
1466
        ResolveInfo $info,
1467
        &$result
1468
    ) {
1469 29
        $runtimeType = is_string($runtimeTypeOrName) ?
1470 4
            $this->exeContext->schema->getType($runtimeTypeOrName) :
1471 29
            $runtimeTypeOrName;
1472
1473 29
        if (! $runtimeType instanceof ObjectType) {
1474 1
            throw new InvariantViolation(
1475 1
                sprintf(
1476
                    'Abstract type %s must resolve to an Object type at ' .
1477
                    'runtime for field %s.%s with value "%s", received "%s". ' .
1478
                    'Either the %s type should provide a "resolveType" ' .
1479 1
                    'function or each possible type should provide an "isTypeOf" function.',
1480 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

1480
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1481 1
                    $info->parentType,
1482 1
                    $info->fieldName,
1483 1
                    Utils::printSafe($result),
1484 1
                    Utils::printSafe($runtimeType),
1485 1
                    $returnType
1486
                )
1487
            );
1488
        }
1489
1490 28
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1491 4
            throw new InvariantViolation(
1492 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1493
            );
1494
        }
1495
1496 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...
1497 1
            throw new InvariantViolation(
1498 1
                sprintf(
1499
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1500
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1501
                    'type instance as referenced anywhere else within the schema ' .
1502 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1503 1
                    $runtimeType,
1504 1
                    $returnType
1505
                )
1506
            );
1507
        }
1508
1509 27
        return $runtimeType;
1510
    }
1511
1512
    /**
1513
     * If a resolve function is not given, then a default resolve behavior is used
1514
     * which takes the property of the source object of the same name as the field
1515
     * and returns it as the result, or if it's a function, returns the result
1516
     * of calling that function while passing along args and context.
1517
     *
1518
     * @param mixed        $source
1519
     * @param mixed[]      $args
1520
     * @param mixed[]|null $context
1521
     *
1522
     * @return mixed|null
1523
     */
1524 116
    public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info)
1525
    {
1526 116
        $fieldName = $info->fieldName;
1527 116
        $property  = null;
1528
1529 116
        if (is_array($source) || $source instanceof ArrayAccess) {
1530 74
            if (isset($source[$fieldName])) {
1531 74
                $property = $source[$fieldName];
1532
            }
1533 42
        } elseif (is_object($source)) {
1534 41
            if (isset($source->{$fieldName})) {
1535 41
                $property = $source->{$fieldName};
1536
            }
1537
        }
1538
1539
        // Using instanceof vs is_callable() because it is 2-10 times faster
1540 116
        return $property instanceof Closure ? $property($source, $args, $context, $info) : $property;
1541
    }
1542
}
1543