Failed Conditions
Push — master ( 84a52c...ed1746 )
by Vladimir
10:32
created

ReferenceExecutor::create()

Size

Total Lines 40
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 40
ccs 17
cts 17
cp 1
c 0
b 0
f 0
nc 2
nop 8

2 Methods

Rating   Name   Duplication   Size   Complexity  
A ReferenceExecutor.php$0 ➔ __construct() 0 3 1
A ReferenceExecutor.php$0 ➔ doExecute() 0 3 2

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Executor;
6
7
use ArrayAccess;
8
use ArrayObject;
9
use Exception;
10
use GraphQL\Error\Error;
11
use GraphQL\Error\InvariantViolation;
12
use GraphQL\Error\Warning;
13
use GraphQL\Executor\Promise\Promise;
14
use GraphQL\Executor\Promise\PromiseAdapter;
15
use GraphQL\Language\AST\DocumentNode;
16
use GraphQL\Language\AST\FieldNode;
17
use GraphQL\Language\AST\FragmentDefinitionNode;
18
use GraphQL\Language\AST\FragmentSpreadNode;
19
use GraphQL\Language\AST\InlineFragmentNode;
20
use GraphQL\Language\AST\NodeKind;
21
use GraphQL\Language\AST\OperationDefinitionNode;
22
use GraphQL\Language\AST\SelectionSetNode;
23
use GraphQL\Type\Definition\AbstractType;
24
use GraphQL\Type\Definition\Directive;
25
use GraphQL\Type\Definition\FieldDefinition;
26
use GraphQL\Type\Definition\InterfaceType;
27
use GraphQL\Type\Definition\LeafType;
28
use GraphQL\Type\Definition\ListOfType;
29
use GraphQL\Type\Definition\NonNull;
30
use GraphQL\Type\Definition\ObjectType;
31
use GraphQL\Type\Definition\ResolveInfo;
32
use GraphQL\Type\Definition\Type;
33
use GraphQL\Type\Introspection;
34
use GraphQL\Type\Schema;
35
use GraphQL\Utils\TypeInfo;
36
use GraphQL\Utils\Utils;
37
use RuntimeException;
38
use SplObjectStorage;
39
use stdClass;
40
use Throwable;
41
use Traversable;
42
use function array_keys;
43
use function array_merge;
44
use function array_reduce;
45
use function array_values;
46
use function get_class;
47
use function is_array;
48
use function is_object;
49
use function is_string;
50
use function sprintf;
51
52
class ReferenceExecutor implements ExecutorImplementation
53
{
54
    /** @var object */
55
    private static $UNDEFINED;
56
57
    /** @var ExecutionContext */
58
    private $exeContext;
59
60
    /** @var SplObjectStorage */
61
    private $subFieldCache;
62
63 201
    private function __construct(ExecutionContext $context)
64
    {
65 201
        if (! self::$UNDEFINED) {
66 1
            self::$UNDEFINED = Utils::undefined();
67
        }
68 201
        $this->exeContext    = $context;
69 201
        $this->subFieldCache = new SplObjectStorage();
70 201
    }
71
72 215
    public static function create(
73
        PromiseAdapter $promiseAdapter,
74
        Schema $schema,
75
        DocumentNode $documentNode,
76
        $rootValue,
77
        $contextValue,
78
        $variableValues,
79
        ?string $operationName,
80
        callable $fieldResolver
81
    ) {
82 215
        $exeContext = self::buildExecutionContext(
83 215
            $schema,
84 215
            $documentNode,
85 215
            $rootValue,
86 215
            $contextValue,
87 215
            $variableValues,
88 215
            $operationName,
89 215
            $fieldResolver,
90 215
            $promiseAdapter
91
        );
92
93 215
        if (is_array($exeContext)) {
94
            return new class($promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext))) implements ExecutorImplementation
95
            {
96
                /** @var Promise */
97
                private $result;
98
99 15
                public function __construct(Promise $result)
100
                {
101 15
                    $this->result = $result;
102 15
                }
103
104 15
                public function doExecute() : Promise
105
                {
106 15
                    return $this->result;
107
                }
108
            };
109
        }
110
111 201
        return new self($exeContext);
112
    }
113
114
    /**
115
     * Constructs an ExecutionContext object from the arguments passed to
116
     * execute, which we will pass throughout the other execution methods.
117
     *
118
     * @param mixed               $rootValue
119
     * @param mixed               $contextValue
120
     * @param mixed[]|Traversable $rawVariableValues
121
     * @param string|null         $operationName
122
     *
123
     * @return ExecutionContext|Error[]
124
     */
125 215
    private static function buildExecutionContext(
126
        Schema $schema,
127
        DocumentNode $documentNode,
128
        $rootValue,
129
        $contextValue,
130
        $rawVariableValues,
131
        $operationName = null,
132
        ?callable $fieldResolver = null,
133
        ?PromiseAdapter $promiseAdapter = null
134
    ) {
135 215
        $errors    = [];
136 215
        $fragments = [];
137
        /** @var OperationDefinitionNode $operation */
138 215
        $operation                    = null;
139 215
        $hasMultipleAssumedOperations = false;
140 215
        foreach ($documentNode->definitions as $definition) {
141
            switch (true) {
142 215
                case $definition instanceof OperationDefinitionNode:
143 213
                    if (! $operationName && $operation) {
144 1
                        $hasMultipleAssumedOperations = true;
145
                    }
146 213
                    if (! $operationName ||
147 213
                        (isset($definition->name) && $definition->name->value === $operationName)) {
148 212
                        $operation = $definition;
149
                    }
150 213
                    break;
151 17
                case $definition instanceof FragmentDefinitionNode:
152 16
                    $fragments[$definition->name->value] = $definition;
153 215
                    break;
154
            }
155
        }
156 215
        if ($operation === null) {
157 3
            if ($operationName) {
158 1
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
159
            } else {
160 3
                $errors[] = new Error('Must provide an operation.');
161
            }
162 212
        } elseif ($hasMultipleAssumedOperations) {
163 1
            $errors[] = new Error(
164 1
                'Must provide operation name if query contains multiple operations.'
165
            );
166
        }
167 215
        $variableValues = null;
168 215
        if ($operation !== null) {
169 212
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
170 212
                $schema,
171 212
                $operation->variableDefinitions ?: [],
172 212
                $rawVariableValues ?: []
0 ignored issues
show
Bug introduced by
It seems like $rawVariableValues ?: array() can also be of type Traversable; however, parameter $inputs of GraphQL\Executor\Values::getVariableValues() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

172
                /** @scrutinizer ignore-type */ $rawVariableValues ?: []
Loading history...
173
            );
174 212
            if (empty($coercionErrors)) {
175 202
                $variableValues = $coercedVariableValues;
176
            } else {
177 11
                $errors = array_merge($errors, $coercionErrors);
178
            }
179
        }
180 215
        if (! empty($errors)) {
181 15
            return $errors;
182
        }
183 201
        Utils::invariant($operation, 'Has operation if no errors.');
184 201
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
185
186 201
        return new ExecutionContext(
187 201
            $schema,
188 201
            $fragments,
189 201
            $rootValue,
190 201
            $contextValue,
191 201
            $operation,
192 201
            $variableValues,
193 201
            $errors,
194 201
            $fieldResolver,
195 201
            $promiseAdapter
196
        );
197
    }
198
199 201
    public function doExecute() : Promise
200
    {
201
        // Return a Promise that will eventually resolve to the data described by
202
        // The "Response" section of the GraphQL specification.
203
        //
204
        // If errors are encountered while executing a GraphQL field, only that
205
        // field and its descendants will be omitted, and sibling fields will still
206
        // be executed. An execution which encounters errors will still result in a
207
        // resolved Promise.
208 201
        $data   = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
209 201
        $result = $this->buildResponse($data);
210
211
        // Note: we deviate here from the reference implementation a bit by always returning promise
212
        // But for the "sync" case it is always fulfilled
213 201
        return $this->isPromise($result)
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->isPromise(...reateFulfilled($result) could return the type GraphQL\Executor\ExecutionResult which is incompatible with the type-hinted return GraphQL\Executor\Promise\Promise. Consider adding an additional type-check to rule them out.
Loading history...
214 40
            ? $result
215 201
            : $this->exeContext->promises->createFulfilled($result);
216
    }
217
218
    /**
219
     * @param mixed|Promise|null $data
220
     *
221
     * @return ExecutionResult|Promise
222
     */
223 201
    private function buildResponse($data)
224
    {
225 201
        if ($this->isPromise($data)) {
226
            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

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

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

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

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

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
955 58
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
956 58
            if (! $containsPromise && $this->getPromise($completedItem)) {
957 13
                $containsPromise = true;
958
            }
959 58
            $completedItems[] = $completedItem;
960
        }
961
962 59
        return $containsPromise ? $this->exeContext->promises->all($completedItems) : $completedItems;
963
    }
964
965
    /**
966
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
967
     *
968
     * @param  mixed $result
969
     *
970
     * @return mixed
971
     *
972
     * @throws Exception
973
     */
974 149
    private function completeLeafValue(LeafType $returnType, &$result)
975
    {
976
        try {
977 149
            return $returnType->serialize($result);
978 3
        } catch (Exception $error) {
979 3
            throw new InvariantViolation(
980 3
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
981 3
                0,
982 3
                $error
983
            );
984
        } catch (Throwable $error) {
985
            throw new InvariantViolation(
986
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
987
                0,
988
                $error
989
            );
990
        }
991
    }
992
993
    /**
994
     * Complete a value of an abstract type by determining the runtime object type
995
     * of that value, then complete the value for that type.
996
     *
997
     * @param FieldNode[] $fieldNodes
998
     * @param mixed[]     $path
999
     * @param mixed[]     $result
1000
     *
1001
     * @return mixed
1002
     *
1003
     * @throws Error
1004
     */
1005 33
    private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1006
    {
1007 33
        $exeContext  = $this->exeContext;
1008 33
        $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

1008
        $runtimeType = $returnType->resolveType(/** @scrutinizer ignore-type */ $result, $exeContext->contextValue, $info);
Loading history...
1009 33
        if ($runtimeType === null) {
1010 11
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
0 ignored issues
show
Bug Best Practice introduced by
The method GraphQL\Executor\Referen...::defaultTypeResolver() is not static, but was called statically. ( Ignorable by Annotation )

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

1010
            /** @scrutinizer ignore-call */ 
1011
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
1011
        }
1012 33
        $promise = $this->getPromise($runtimeType);
1013 33
        if ($promise) {
1014
            return $promise->then(function ($resolvedRuntimeType) use (
1015 5
                $returnType,
1016 5
                $fieldNodes,
1017 5
                $info,
1018 5
                $path,
1019 5
                &$result
1020
            ) {
1021 5
                return $this->completeObjectValue(
1022 5
                    $this->ensureValidRuntimeType(
1023 5
                        $resolvedRuntimeType,
1024 5
                        $returnType,
1025 5
                        $info,
1026 5
                        $result
1027
                    ),
1028 5
                    $fieldNodes,
1029 5
                    $info,
1030 5
                    $path,
1031 5
                    $result
1032
                );
1033 7
            });
1034
        }
1035
1036 26
        return $this->completeObjectValue(
1037 26
            $this->ensureValidRuntimeType(
1038 26
                $runtimeType,
0 ignored issues
show
Bug introduced by
It seems like $runtimeType can also be of type GraphQL\Executor\Promise\Promise; however, parameter $runtimeTypeOrName of GraphQL\Executor\Referen...nsureValidRuntimeType() does only seem to accept GraphQL\Type\Definition\ObjectType|null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

1038
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
1039 26
                $returnType,
1040 26
                $info,
1041 26
                $result
1042
            ),
1043 24
            $fieldNodes,
1044 24
            $info,
1045 24
            $path,
1046 24
            $result
1047
        );
1048
    }
1049
1050
    /**
1051
     * If a resolveType function is not given, then a default resolve behavior is
1052
     * used which attempts two strategies:
1053
     *
1054
     * First, See if the provided value has a `__typename` field defined, if so, use
1055
     * that value as name of the resolved type.
1056
     *
1057
     * Otherwise, test each possible type for the abstract type by calling
1058
     * isTypeOf for the object being coerced, returning the first type that matches.
1059
     *
1060
     * @param mixed|null $value
1061
     * @param mixed|null $context
1062
     *
1063
     * @return ObjectType|Promise|null
1064
     */
1065 11
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
1066
    {
1067
        // First, look for `__typename`.
1068 11
        if ($value !== null &&
1069 11
            (is_array($value) || $value instanceof ArrayAccess) &&
1070 11
            isset($value['__typename']) &&
1071 11
            is_string($value['__typename'])
1072
        ) {
1073 2
            return $value['__typename'];
1074
        }
1075 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
1076 1
            Warning::warnOnce(
1077 1
                sprintf(
1078
                    'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
1079
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1080
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1081 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1082 1
                    $abstractType->name,
1083 1
                    Utils::printSafe($value)
1084
                ),
1085 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1086
            );
1087
        }
1088
        // Otherwise, test each possible type.
1089 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1090 9
        $promisedIsTypeOfResults = [];
1091 9
        foreach ($possibleTypes as $index => $type) {
1092 9
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
1093 9
            if ($isTypeOfResult === null) {
1094
                continue;
1095
            }
1096 9
            $promise = $this->getPromise($isTypeOfResult);
1097 9
            if ($promise) {
1098 3
                $promisedIsTypeOfResults[$index] = $promise;
1099 6
            } elseif ($isTypeOfResult) {
1100 9
                return $type;
1101
            }
1102
        }
1103 3
        if (! empty($promisedIsTypeOfResults)) {
1104 3
            return $this->exeContext->promises->all($promisedIsTypeOfResults)
1105
                ->then(static function ($isTypeOfResults) use ($possibleTypes) {
1106 2
                    foreach ($isTypeOfResults as $index => $result) {
1107 2
                        if ($result) {
1108 2
                            return $possibleTypes[$index];
1109
                        }
1110
                    }
1111
1112
                    return null;
1113 3
                });
1114
        }
1115
1116
        return null;
1117
    }
1118
1119
    /**
1120
     * Complete an Object value by executing all sub-selections.
1121
     *
1122
     * @param FieldNode[] $fieldNodes
1123
     * @param mixed[]     $path
1124
     * @param mixed       $result
1125
     *
1126
     * @return mixed[]|Promise|stdClass
1127
     *
1128
     * @throws Error
1129
     */
1130 91
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1131
    {
1132
        // If there is an isTypeOf predicate function, call it with the
1133
        // current result. If isTypeOf returns false, then raise an error rather
1134
        // than continuing execution.
1135 91
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1136 91
        if ($isTypeOf !== null) {
1137 11
            $promise = $this->getPromise($isTypeOf);
1138 11
            if ($promise) {
1139
                return $promise->then(function ($isTypeOfResult) use (
1140 2
                    $returnType,
1141 2
                    $fieldNodes,
1142 2
                    $path,
1143 2
                    &$result
1144
                ) {
1145 2
                    if (! $isTypeOfResult) {
1146
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1147
                    }
1148
1149 2
                    return $this->collectAndExecuteSubfields(
1150 2
                        $returnType,
1151 2
                        $fieldNodes,
1152 2
                        $path,
1153 2
                        $result
1154
                    );
1155 2
                });
1156
            }
1157 9
            if (! $isTypeOf) {
1158 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1159
            }
1160
        }
1161
1162 89
        return $this->collectAndExecuteSubfields(
1163 89
            $returnType,
1164 89
            $fieldNodes,
1165 89
            $path,
1166 89
            $result
1167
        );
1168
    }
1169
1170
    /**
1171
     * @param mixed[]     $result
1172
     * @param FieldNode[] $fieldNodes
1173
     *
1174
     * @return Error
1175
     */
1176 1
    private function invalidReturnTypeError(
1177
        ObjectType $returnType,
1178
        $result,
1179
        $fieldNodes
1180
    ) {
1181 1
        return new Error(
1182 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1183 1
            $fieldNodes
1184
        );
1185
    }
1186
1187
    /**
1188
     * @param FieldNode[] $fieldNodes
1189
     * @param mixed[]     $path
1190
     * @param mixed[]     $result
1191
     *
1192
     * @return mixed[]|Promise|stdClass
1193
     *
1194
     * @throws Error
1195
     */
1196 91
    private function collectAndExecuteSubfields(
1197
        ObjectType $returnType,
1198
        $fieldNodes,
1199
        $path,
1200
        &$result
1201
    ) {
1202 91
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
1203
1204 91
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1205
    }
1206
1207 91
    private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayObject
1208
    {
1209 91
        if (! isset($this->subFieldCache[$returnType])) {
1210 91
            $this->subFieldCache[$returnType] = new SplObjectStorage();
1211
        }
1212 91
        if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
1213
            // Collect sub-fields to execute to complete this value.
1214 91
            $subFieldNodes        = new ArrayObject();
1215 91
            $visitedFragmentNames = new ArrayObject();
1216 91
            foreach ($fieldNodes as $fieldNode) {
1217 91
                if (! isset($fieldNode->selectionSet)) {
1218
                    continue;
1219
                }
1220 91
                $subFieldNodes = $this->collectFields(
1221 91
                    $returnType,
1222 91
                    $fieldNode->selectionSet,
1223 91
                    $subFieldNodes,
1224 91
                    $visitedFragmentNames
1225
                );
1226
            }
1227 91
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
1228
        }
1229
1230 91
        return $this->subFieldCache[$returnType][$fieldNodes];
1231
    }
1232
1233
    /**
1234
     * Implements the "Evaluating selection sets" section of the spec
1235
     * for "read" mode.
1236
     *
1237
     * @param mixed|null  $source
1238
     * @param mixed[]     $path
1239
     * @param ArrayObject $fields
1240
     *
1241
     * @return Promise|stdClass|mixed[]
1242
     */
1243 197
    private function executeFields(ObjectType $parentType, $source, $path, $fields)
1244
    {
1245 197
        $containsPromise = false;
1246 197
        $finalResults    = [];
1247 197
        foreach ($fields as $responseName => $fieldNodes) {
1248 197
            $fieldPath   = $path;
1249 197
            $fieldPath[] = $responseName;
1250 197
            $result      = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath);
1251 195
            if ($result === self::$UNDEFINED) {
1252 6
                continue;
1253
            }
1254 192
            if (! $containsPromise && $this->getPromise($result)) {
1255 38
                $containsPromise = true;
1256
            }
1257 192
            $finalResults[$responseName] = $result;
1258
        }
1259
        // If there are no promises, we can just return the object
1260 195
        if (! $containsPromise) {
1261 166
            return self::fixResultsIfEmptyArray($finalResults);
1262
        }
1263
1264
        // Otherwise, results is a map from field name to the result
1265
        // of resolving that field, which is possibly a promise. Return
1266
        // a promise that will return this same map, but with any
1267
        // promises replaced with the values they resolved to.
1268 38
        return $this->promiseForAssocArray($finalResults);
1269
    }
1270
1271
    /**
1272
     * @see https://github.com/webonyx/graphql-php/issues/59
1273
     *
1274
     * @param mixed[] $results
1275
     *
1276
     * @return stdClass|mixed[]
1277
     */
1278 197
    private static function fixResultsIfEmptyArray($results)
1279
    {
1280 197
        if ($results === []) {
1281 5
            return new stdClass();
1282
        }
1283
1284 193
        return $results;
1285
    }
1286
1287
    /**
1288
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1289
     * a `Promise<array<key,scalar|array>>`
1290
     *
1291
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1292
     * any promises.
1293
     *
1294
     * @param (string|Promise)[] $assoc
1295
     *
1296
     * @return mixed
1297
     */
1298 38
    private function promiseForAssocArray(array $assoc)
1299
    {
1300 38
        $keys              = array_keys($assoc);
1301 38
        $valuesAndPromises = array_values($assoc);
1302 38
        $promise           = $this->exeContext->promises->all($valuesAndPromises);
1303
1304
        return $promise->then(static function ($values) use ($keys) {
1305 36
            $resolvedResults = [];
1306 36
            foreach ($values as $i => $value) {
1307 36
                $resolvedResults[$keys[$i]] = $value;
1308
            }
1309
1310 36
            return self::fixResultsIfEmptyArray($resolvedResults);
1311 38
        });
1312
    }
1313
1314
    /**
1315
     * @param string|ObjectType|null $runtimeTypeOrName
1316
     * @param FieldNode[]            $fieldNodes
1317
     * @param mixed                  $result
1318
     *
1319
     * @return ObjectType
1320
     */
1321 31
    private function ensureValidRuntimeType(
1322
        $runtimeTypeOrName,
1323
        AbstractType $returnType,
1324
        ResolveInfo $info,
1325
        &$result
1326
    ) {
1327 31
        $runtimeType = is_string($runtimeTypeOrName) ?
1328 4
            $this->exeContext->schema->getType($runtimeTypeOrName) :
1329 31
            $runtimeTypeOrName;
1330 31
        if (! $runtimeType instanceof ObjectType) {
1331 1
            throw new InvariantViolation(
1332 1
                sprintf(
1333
                    'Abstract type %s must resolve to an Object type at ' .
1334
                    'runtime for field %s.%s with value "%s", received "%s". ' .
1335
                    'Either the %s type should provide a "resolveType" ' .
1336 1
                    'function or each possible type should provide an "isTypeOf" function.',
1337 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

1337
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1338 1
                    $info->parentType,
1339 1
                    $info->fieldName,
1340 1
                    Utils::printSafe($result),
1341 1
                    Utils::printSafe($runtimeType),
1342 1
                    $returnType
1343
                )
1344
            );
1345
        }
1346 30
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1347 4
            throw new InvariantViolation(
1348 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1349
            );
1350
        }
1351 30
        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...
1352 1
            throw new InvariantViolation(
1353 1
                sprintf(
1354
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1355
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1356
                    'type instance as referenced anywhere else within the schema ' .
1357 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1358 1
                    $runtimeType,
1359 1
                    $returnType
1360
                )
1361
            );
1362
        }
1363
1364 29
        return $runtimeType;
1365
    }
1366
}
1367