Completed
Push — master ( bedbd3...2f2b54 )
by Vladimir
04:33
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 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 187
    private function __construct(ExecutionContext $context)
66
    {
67 187
        if (! self::$UNDEFINED) {
68 1
            self::$UNDEFINED = Utils::undefined();
69
        }
70
71 187
        $this->exeContext = $context;
72 187
        $this->subFieldCache = new \SplObjectStorage();
73 187
    }
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 200
    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 200
        $exeContext = self::buildExecutionContext(
166 200
            $schema,
167 200
            $ast,
168 200
            $rootValue,
169 200
            $contextValue,
170 200
            $variableValues,
171 200
            $operationName,
172 200
            $fieldResolver,
173 200
            $promiseAdapter
174
        );
175
176 200
        if (is_array($exeContext)) {
177 14
            return $promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext));
178
        }
179
180 187
        $executor = new self($exeContext);
181
182 187
        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 200
    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 200
        $errors    = [];
207 200
        $fragments = [];
208
        /** @var OperationDefinitionNode $operation */
209 200
        $operation                    = null;
210 200
        $hasMultipleAssumedOperations = false;
211
212 200
        foreach ($documentNode->definitions as $definition) {
213 200
            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 200
                case NodeKind::OPERATION_DEFINITION:
215 199
                    if (! $operationName && $operation) {
216 1
                        $hasMultipleAssumedOperations = true;
217
                    }
218 199
                    if (! $operationName ||
219 199
                        (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 198
                        $operation = $definition;
221
                    }
222 199
                    break;
223 14
                case NodeKind::FRAGMENT_DEFINITION:
224 13
                    $fragments[$definition->name->value] = $definition;
225 200
                    break;
226
            }
227
        }
228
229 200
        if (! $operation) {
230 2
            if ($operationName) {
231 1
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
232
            } else {
233 2
                $errors[] = new Error('Must provide an operation.');
234
            }
235 198
        } elseif ($hasMultipleAssumedOperations) {
236 1
            $errors[] = new Error(
237 1
                'Must provide operation name if query contains multiple operations.'
238
            );
239
        }
240
241 200
        $variableValues = null;
242 200
        if ($operation) {
243 198
            $coercedVariableValues = Values::getVariableValues(
244 198
                $schema,
245 198
                $operation->variableDefinitions ?: [],
246 198
                $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 198
            if ($coercedVariableValues['errors']) {
250 11
                $errors = array_merge($errors, $coercedVariableValues['errors']);
251
            } else {
252 188
                $variableValues = $coercedVariableValues['coerced'];
253
            }
254
        }
255
256 200
        if ($errors) {
257 14
            return $errors;
258
        }
259
260 187
        Utils::invariant($operation, 'Has operation if no errors.');
261 187
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
262
263 187
        return new ExecutionContext(
264 187
            $schema,
265 187
            $fragments,
266 187
            $rootValue,
267 187
            $contextValue,
268 187
            $operation,
269 187
            $variableValues,
270 187
            $errors,
271 187
            $fieldResolver ?: self::$defaultFieldResolver,
272 187
            $promiseAdapter ?: self::getPromiseAdapter()
273
        );
274
    }
275
276
    /**
277
     * @return Promise
278
     */
279 187
    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
        $result = $this->exeContext->promises->create(function (callable $resolve) {
289 187
            return $resolve($this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue));
290 187
        });
291
292
        return $result
293 187
            ->then(
294 187
                null,
295
                function ($error) {
296
                    // Errors from sub-fields of a NonNull type may propagate to the top level,
297
                    // at which point we still log the error and null the parent field, which
298
                    // in this case is the entire response.
299
                    $this->exeContext->addError($error);
300
301
                    return null;
302 187
                }
303
            )
304
            ->then(function ($data) {
305 187
                if ($data !== null) {
306 183
                    $data = (array) $data;
307
                }
308
309 187
                return new ExecutionResult($data, $this->exeContext->errors);
310 187
            });
311
    }
312
313
    /**
314
     * Implements the "Evaluating operations" section of the spec.
315
     *
316
     * @param  mixed[] $rootValue
317
     * @return Promise|\stdClass|mixed[]
318
     */
319 187
    private function executeOperation(OperationDefinitionNode $operation, $rootValue)
320
    {
321 187
        $type   = $this->getOperationRootType($this->exeContext->schema, $operation);
322 187
        $fields = $this->collectFields($type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
323
324 187
        $path = [];
325
326
        // Errors from sub-fields of a NonNull type may propagate to the top level,
327
        // at which point we still log the error and null the parent field, which
328
        // in this case is the entire response.
329
        //
330
        // Similar to completeValueCatchingError.
331
        try {
332 187
            $result = $operation->operation === 'mutation' ?
333 5
                $this->executeFieldsSerially($type, $rootValue, $path, $fields) :
334 187
                $this->executeFields($type, $rootValue, $path, $fields);
335
336 185
            $promise = $this->getPromise($result);
337 185
            if ($promise) {
338 41
                return $promise->then(
339 41
                    null,
340
                    function ($error) {
341 2
                        $this->exeContext->addError($error);
342
343 2
                        return null;
344 41
                    }
345
                );
346
            }
347
348 145
            return $result;
349 2
        } catch (Error $error) {
350 2
            $this->exeContext->addError($error);
351
352 2
            return null;
353
        }
354
    }
355
356
    /**
357
     * Extracts the root type of the operation from the schema.
358
     *
359
     * @return ObjectType
360
     * @throws Error
361
     */
362 187
    private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation)
363
    {
364 187
        switch ($operation->operation) {
365 187
            case 'query':
366 180
                $queryType = $schema->getQueryType();
367 180
                if (! $queryType) {
0 ignored issues
show
introduced by
$queryType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
368
                    throw new Error(
369
                        'Schema does not define the required query root type.',
370
                        [$operation]
371
                    );
372
                }
373
374 180
                return $queryType;
375 7
            case 'mutation':
376 5
                $mutationType = $schema->getMutationType();
377 5
                if (! $mutationType) {
0 ignored issues
show
introduced by
$mutationType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
378
                    throw new Error(
379
                        'Schema is not configured for mutations.',
380
                        [$operation]
381
                    );
382
                }
383
384 5
                return $mutationType;
385 2
            case 'subscription':
386 2
                $subscriptionType = $schema->getSubscriptionType();
387 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...
388
                    throw new Error(
389
                        'Schema is not configured for subscriptions.',
390
                        [$operation]
391
                    );
392
                }
393
394 2
                return $subscriptionType;
395
            default:
396
                throw new Error(
397
                    'Can only execute queries, mutations and subscriptions.',
398
                    [$operation]
399
                );
400
        }
401
    }
402
403
    /**
404
     * Given a selectionSet, adds all of the fields in that selection to
405
     * the passed in map of fields, and returns it at the end.
406
     *
407
     * CollectFields requires the "runtime type" of an object. For a field which
408
     * returns an Interface or Union type, the "runtime type" will be the actual
409
     * Object type returned by that field.
410
     *
411
     * @param ArrayObject $fields
412
     * @param ArrayObject $visitedFragmentNames
413
     *
414
     * @return \ArrayObject
415
     */
416 187
    private function collectFields(
417
        ObjectType $runtimeType,
418
        SelectionSetNode $selectionSet,
419
        $fields,
420
        $visitedFragmentNames
421
    ) {
422 187
        $exeContext = $this->exeContext;
423 187
        foreach ($selectionSet->selections as $selection) {
424 187
            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...
425 187
                case NodeKind::FIELD:
426 187
                    if (! $this->shouldIncludeNode($selection)) {
427 2
                        break;
428
                    }
429 187
                    $name = self::getFieldEntryKey($selection);
430 187
                    if (! isset($fields[$name])) {
431 187
                        $fields[$name] = new \ArrayObject();
432
                    }
433 187
                    $fields[$name][] = $selection;
434 187
                    break;
435 29
                case NodeKind::INLINE_FRAGMENT:
436 21
                    if (! $this->shouldIncludeNode($selection) ||
437 21
                        ! $this->doesFragmentConditionMatch($selection, $runtimeType)
438
                    ) {
439 19
                        break;
440
                    }
441 21
                    $this->collectFields(
442 21
                        $runtimeType,
443 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...
444 21
                        $fields,
445 21
                        $visitedFragmentNames
446
                    );
447 21
                    break;
448 10
                case NodeKind::FRAGMENT_SPREAD:
449 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...
450 10
                    if (! empty($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) {
451 2
                        break;
452
                    }
453 10
                    $visitedFragmentNames[$fragName] = true;
454
455
                    /** @var FragmentDefinitionNode|null $fragment */
456 10
                    $fragment = $exeContext->fragments[$fragName] ?? null;
457 10
                    if (! $fragment || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
458 1
                        break;
459
                    }
460 9
                    $this->collectFields(
461 9
                        $runtimeType,
462 9
                        $fragment->selectionSet,
463 9
                        $fields,
464 9
                        $visitedFragmentNames
465
                    );
466 187
                    break;
467
            }
468
        }
469
470 187
        return $fields;
471
    }
472
473
    /**
474
     * Determines if a field should be included based on the @include and @skip
475
     * directives, where @skip has higher precedence than @include.
476
     *
477
     * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
478
     * @return bool
479
     */
480 187
    private function shouldIncludeNode($node)
481
    {
482 187
        $variableValues = $this->exeContext->variableValues;
483 187
        $skipDirective  = Directive::skipDirective();
484
485 187
        $skip = Values::getDirectiveValues(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $skip is correct as GraphQL\Executor\Values:...$node, $variableValues) targeting GraphQL\Executor\Values::getDirectiveValues() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
486 187
            $skipDirective,
487 187
            $node,
488 187
            $variableValues
489
        );
490
491 187
        if (isset($skip['if']) && $skip['if'] === true) {
492 5
            return false;
493
        }
494
495 187
        $includeDirective = Directive::includeDirective();
496
497 187
        $include = Values::getDirectiveValues(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $include is correct as GraphQL\Executor\Values:...$node, $variableValues) targeting GraphQL\Executor\Values::getDirectiveValues() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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

1069
            /** @scrutinizer ignore-call */ 
1070
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
1070
        }
1071
1072 30
        $promise = $this->getPromise($runtimeType);
1073 30
        if ($promise) {
1074
            return $promise->then(function ($resolvedRuntimeType) use (
1075 5
                $returnType,
1076 5
                $fieldNodes,
1077 5
                $info,
1078 5
                $path,
1079 5
                &$result
1080
            ) {
1081 5
                return $this->completeObjectValue(
1082 5
                    $this->ensureValidRuntimeType(
1083 5
                        $resolvedRuntimeType,
1084 5
                        $returnType,
1085 5
                        $info,
1086 5
                        $result
1087
                    ),
1088 5
                    $fieldNodes,
1089 5
                    $info,
1090 5
                    $path,
1091 5
                    $result
1092
                );
1093 7
            });
1094
        }
1095
1096 23
        return $this->completeObjectValue(
1097 23
            $this->ensureValidRuntimeType(
1098 23
                $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

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

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

1407
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1408
                    $info->parentType,
1409
                    $info->fieldName,
1410
                    Utils::printSafe($result),
1411
                    Utils::printSafe($runtimeType)
1412
                )
1413
            );
1414
        }
1415
1416 28
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1417 4
            throw new InvariantViolation(
1418 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1419
            );
1420
        }
1421
1422 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...
1423 1
            throw new InvariantViolation(
1424 1
                sprintf(
1425
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1426
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1427
                    'type instance as referenced anywhere else within the schema ' .
1428 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1429 1
                    $runtimeType,
1430 1
                    $returnType
1431
                )
1432
            );
1433
        }
1434
1435 27
        return $runtimeType;
1436
    }
1437
1438
    /**
1439
     * If a resolve function is not given, then a default resolve behavior is used
1440
     * which takes the property of the source object of the same name as the field
1441
     * and returns it as the result, or if it's a function, returns the result
1442
     * of calling that function while passing along args and context.
1443
     *
1444
     * @param mixed        $source
1445
     * @param mixed[]      $args
1446
     * @param mixed[]|null $context
1447
     *
1448
     * @return mixed|null
1449
     */
1450 115
    public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info)
1451
    {
1452 115
        $fieldName = $info->fieldName;
1453 115
        $property  = null;
1454
1455 115
        if (is_array($source) || $source instanceof \ArrayAccess) {
1456 73
            if (isset($source[$fieldName])) {
1457 73
                $property = $source[$fieldName];
1458
            }
1459 42
        } elseif (is_object($source)) {
1460 41
            if (isset($source->{$fieldName})) {
1461 41
                $property = $source->{$fieldName};
1462
            }
1463
        }
1464
1465 115
        return $property instanceof \Closure ? $property($source, $args, $context, $info) : $property;
1466
    }
1467
}
1468