Completed
Pull Request — master (#550)
by Jáchym
26:52 queued 22:59
created

ReferenceExecutor::create()

Size

Total Lines 40
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

2 Methods

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

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\Definition\UnionType;
34
use GraphQL\Type\Introspection;
35
use GraphQL\Type\Schema;
36
use GraphQL\Utils\TypeInfo;
37
use GraphQL\Utils\Utils;
38
use RuntimeException;
39
use SplObjectStorage;
40
use stdClass;
41
use Throwable;
42
use Traversable;
43
use function array_keys;
44
use function array_merge;
45
use function array_reduce;
46
use function array_values;
47
use function get_class;
48
use function is_array;
49
use function is_object;
50
use function is_string;
51
use function sprintf;
52
53
class ReferenceExecutor implements ExecutorImplementation
54
{
55
    /** @var object */
56
    private static $UNDEFINED;
57
58
    /** @var ExecutionContext */
59
    private $exeContext;
60
61
    /** @var SplObjectStorage */
62
    private $subFieldCache;
63
64
    private function __construct(ExecutionContext $context)
65
    {
66
        if (! self::$UNDEFINED) {
67
            self::$UNDEFINED = Utils::undefined();
68
        }
69
        $this->exeContext    = $context;
70
        $this->subFieldCache = new SplObjectStorage();
71
    }
72
73 212
    public static function create(
74
        PromiseAdapter $promiseAdapter,
75
        Schema $schema,
76
        DocumentNode $documentNode,
77
        $rootValue,
78
        $contextValue,
79
        $variableValues,
80
        ?string $operationName,
81
        callable $fieldResolver
82
    ) {
83
        $exeContext = self::buildExecutionContext(
84
            $schema,
85
            $documentNode,
86
            $rootValue,
87
            $contextValue,
88
            $variableValues,
89
            $operationName,
90
            $fieldResolver,
91
            $promiseAdapter
92
        );
93
94
        if (is_array($exeContext)) {
95
            return new class($promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext))) implements ExecutorImplementation
96
            {
97
                /** @var Promise */
98
                private $result;
99
100
                public function __construct(Promise $result)
101
                {
102 16
                    $this->result = $result;
103 16
                }
104
105
                public function doExecute() : Promise
106
                {
107 16
                    return $this->result;
108
                }
109
            };
110
        }
111
112 212
        return new self($exeContext);
113
    }
114
115
    /**
116
     * Constructs an ExecutionContext object from the arguments passed to
117
     * execute, which we will pass throughout the other execution methods.
118
     *
119
     * @param mixed               $rootValue
120
     * @param mixed               $contextValue
121
     * @param mixed[]|Traversable $rawVariableValues
122
     * @param string|null         $operationName
123
     *
124
     * @return ExecutionContext|Error[]
125
     */
126 227
    private static function buildExecutionContext(
127
        Schema $schema,
128
        DocumentNode $documentNode,
129
        $rootValue,
130
        $contextValue,
131
        $rawVariableValues,
132
        $operationName = null,
133
        ?callable $fieldResolver = null,
134
        ?PromiseAdapter $promiseAdapter = null
135
    ) {
136 227
        $errors    = [];
137 227
        $fragments = [];
138
        /** @var OperationDefinitionNode|null $operation */
139 227
        $operation                    = null;
140 227
        $hasMultipleAssumedOperations = false;
141 227
        foreach ($documentNode->definitions as $definition) {
142
            switch (true) {
143 227
                case $definition instanceof OperationDefinitionNode:
144 225
                    if ($operationName === null && $operation !== null) {
145 1
                        $hasMultipleAssumedOperations = true;
146
                    }
147 225
                    if ($operationName === null ||
148 225
                        (isset($definition->name) && $definition->name->value === $operationName)) {
149 224
                        $operation = $definition;
150
                    }
151 225
                    break;
152 18
                case $definition instanceof FragmentDefinitionNode:
153 17
                    $fragments[$definition->name->value] = $definition;
154 227
                    break;
155
            }
156
        }
157 227
        if ($operation === null) {
158 3
            if ($operationName === null) {
159 2
                $errors[] = new Error('Must provide an operation.');
160
            } else {
161 3
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
162
            }
163 224
        } elseif ($hasMultipleAssumedOperations) {
164 1
            $errors[] = new Error(
165 1
                'Must provide operation name if query contains multiple operations.'
166
            );
167
        }
168 227
        $variableValues = null;
169 227
        if ($operation !== null) {
170 224
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
171 224
                $schema,
172 224
                $operation->variableDefinitions ?: [],
173 224
                $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

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

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

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

502
        return self::fixResultsIfEmptyArray(/** @scrutinizer ignore-type */ $result);
Loading history...
503
    }
504
505
    /**
506
     * Resolves the field on the given root value.
507
     *
508
     * In particular, this figures out the value that the field returns
509
     * by calling its resolve function, then calls completeValue to complete promises,
510
     * serialize scalars, or execute the sub-selection-set for objects.
511
     *
512
     * @param mixed       $rootValue
513
     * @param FieldNode[] $fieldNodes
514
     * @param mixed[]     $path
515
     *
516
     * @return mixed[]|Exception|mixed|null
517
     */
518 212
    private function resolveField(ObjectType $parentType, $rootValue, $fieldNodes, $path)
519
    {
520 212
        $exeContext = $this->exeContext;
521 212
        $fieldNode  = $fieldNodes[0];
522 212
        $fieldName  = $fieldNode->name->value;
523 212
        $fieldDef   = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
524 212
        if ($fieldDef === null) {
525 7
            return self::$UNDEFINED;
526
        }
527 208
        $returnType = $fieldDef->getType();
528
        // The resolve function's optional 3rd argument is a context value that
529
        // is provided to every resolve function within an execution. It is commonly
530
        // used to represent an authenticated user, or request-specific caches.
531 208
        $context = $exeContext->contextValue;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
532
        // The resolve function's optional 4th argument is a collection of
533
        // information about the current execution state.
534 208
        $info = new ResolveInfo(
535 208
            $fieldDef,
536 208
            $fieldNodes,
537 208
            $parentType,
538 208
            $path,
539 208
            $exeContext->schema,
540 208
            $exeContext->fragments,
541 208
            $exeContext->rootValue,
542 208
            $exeContext->operation,
543 208
            $exeContext->variableValues
544
        );
545 208
        if ($fieldDef->resolveFn !== null) {
546 156
            $resolveFn = $fieldDef->resolveFn;
547 99
        } elseif ($parentType->resolveFieldFn !== null) {
548
            $resolveFn = $parentType->resolveFieldFn;
549
        } else {
550 99
            $resolveFn = $this->exeContext->fieldResolver;
551
        }
552
        // Get the resolve function, regardless of if its result is normal
553
        // or abrupt (error).
554 208
        $result = $this->resolveFieldValueOrError(
555 208
            $fieldDef,
556 208
            $fieldNode,
557 208
            $resolveFn,
558 208
            $rootValue,
559 208
            $info
560
        );
561 208
        $result = $this->completeValueCatchingError(
562 208
            $returnType,
563 208
            $fieldNodes,
564 208
            $info,
565 208
            $path,
566 208
            $result
567
        );
568
569 206
        return $result;
570
    }
571
572
    /**
573
     * This method looks up the field on the given type definition.
574
     *
575
     * It has special casing for the two introspection fields, __schema
576
     * and __typename. __typename is special because it can always be
577
     * queried as a field, even in situations where no other fields
578
     * are allowed, like on a Union. __schema could get automatically
579
     * added to the query type, but that would require mutating type
580
     * definitions, which would cause issues.
581
     */
582 212
    private function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
583
    {
584 212
        static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
585 212
        $schemaMetaFieldDef   = $schemaMetaFieldDef ?: Introspection::schemaMetaFieldDef();
586 212
        $typeMetaFieldDef     = $typeMetaFieldDef ?: Introspection::typeMetaFieldDef();
587 212
        $typeNameMetaFieldDef = $typeNameMetaFieldDef ?: Introspection::typeNameMetaFieldDef();
588 212
        if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
589 6
            return $schemaMetaFieldDef;
590
        }
591
592 212
        if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
593 15
            return $typeMetaFieldDef;
594
        }
595
596 212
        if ($fieldName === $typeNameMetaFieldDef->name) {
597 7
            return $typeNameMetaFieldDef;
598
        }
599 212
        $tmp = $parentType->getFields();
600
601 212
        return $tmp[$fieldName] ?? null;
602
    }
603
604
    /**
605
     * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function.
606
     * Returns the result of resolveFn or the abrupt-return Error object.
607
     *
608
     * @param FieldDefinition $fieldDef
609
     * @param FieldNode       $fieldNode
610
     * @param callable        $resolveFn
611
     * @param mixed           $rootValue
612
     * @param ResolveInfo     $info
613
     *
614
     * @return Throwable|Promise|mixed
615
     */
616 208
    private function resolveFieldValueOrError($fieldDef, $fieldNode, $resolveFn, $rootValue, $info)
617
    {
618
        try {
619
            // Build a map of arguments from the field.arguments AST, using the
620
            // variables scope to fulfill any variable references.
621 208
            $args         = Values::getArgumentValues(
622 208
                $fieldDef,
623 208
                $fieldNode,
624 208
                $this->exeContext->variableValues
625
            );
626 201
            $contextValue = $this->exeContext->contextValue;
627
628 201
            return $resolveFn($rootValue, $args, $contextValue, $info);
629 21
        } catch (Throwable $error) {
630 21
            return $error;
631
        }
632
    }
633
634
    /**
635
     * This is a small wrapper around completeValue which detects and logs errors
636
     * in the execution context.
637
     *
638
     * @param FieldNode[] $fieldNodes
639
     * @param string[]    $path
640
     * @param mixed       $result
641
     *
642
     * @return mixed[]|Promise|null
643
     */
644 208
    private function completeValueCatchingError(
645
        Type $returnType,
646
        $fieldNodes,
647
        ResolveInfo $info,
648
        $path,
649
        $result
650
    ) {
651
        // Otherwise, error protection is applied, logging the error and resolving
652
        // a null value for this field if one is encountered.
653
        try {
654 208
            $promise = $this->getPromise($result);
655 208
            if ($promise !== null) {
656
                $completed = $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
657 31
                    return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
658 34
                });
659
            } else {
660 200
                $completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $result);
661
            }
662
663 189
            $promise = $this->getPromise($completed);
664 189
            if ($promise !== null) {
665
                return $promise->then(null, function ($error) use ($fieldNodes, $path, $returnType) {
666 26
                    return $this->handleFieldError($error, $fieldNodes, $path, $returnType);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleFieldError(...es, $path, $returnType) targeting GraphQL\Executor\Referen...tor::handleFieldError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
667 40
                });
668
            }
669
670 168
            return $completed;
671 41
        } catch (Throwable $err) {
672 41
            return $this->handleFieldError($err, $fieldNodes, $path, $returnType);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleFieldError(...es, $path, $returnType) targeting GraphQL\Executor\Referen...tor::handleFieldError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
673
        }
674
    }
675
676 57
    private function handleFieldError($rawError, $fieldNodes, $path, $returnType)
677
    {
678 57
        $error = Error::createLocatedError(
679 57
            $rawError,
680 57
            $fieldNodes,
681 57
            $path
682
        );
683
684
        // If the field type is non-nullable, then it is resolved without any
685
        // protection from errors, however it still properly locates the error.
686 57
        if ($returnType instanceof NonNull) {
687 22
            throw $error;
688
        }
689
        // Otherwise, error protection is applied, logging the error and resolving
690
        // a null value for this field if one is encountered.
691 53
        $this->exeContext->addError($error);
692
693 53
        return null;
694
    }
695
696
    /**
697
     * Implements the instructions for completeValue as defined in the
698
     * "Field entries" section of the spec.
699
     *
700
     * If the field type is Non-Null, then this recursively completes the value
701
     * for the inner type. It throws a field error if that completion returns null,
702
     * as per the "Nullability" section of the spec.
703
     *
704
     * If the field type is a List, then this recursively completes the value
705
     * for the inner type on each item in the list.
706
     *
707
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
708
     * value of the type by calling the `serialize` method of GraphQL type
709
     * definition.
710
     *
711
     * If the field is an abstract type, determine the runtime type of the value
712
     * and then complete based on that type
713
     *
714
     * Otherwise, the field type expects a sub-selection set, and will complete the
715
     * value by evaluating all sub-selections.
716
     *
717
     * @param FieldNode[] $fieldNodes
718
     * @param string[]    $path
719
     * @param mixed       $result
720
     *
721
     * @return mixed[]|mixed|Promise|null
722
     *
723
     * @throws Error
724
     * @throws Throwable
725
     */
726 206
    private function completeValue(
727
        Type $returnType,
728
        $fieldNodes,
729
        ResolveInfo $info,
730
        $path,
731
        &$result
732
    ) {
733
        // If result is an Error, throw a located error.
734 206
        if ($result instanceof Throwable) {
735 21
            throw $result;
736
        }
737
738
        // If field type is NonNull, complete for inner type, and throw field error
739
        // if result is null.
740 194
        if ($returnType instanceof NonNull) {
741 48
            $completed = $this->completeValue(
742 48
                $returnType->getWrappedType(),
743 48
                $fieldNodes,
744 48
                $info,
745 48
                $path,
746 48
                $result
747
            );
748 48
            if ($completed === null) {
749 15
                throw new InvariantViolation(
750 15
                    'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.'
751
                );
752
            }
753
754 42
            return $completed;
755
        }
756
        // If result is null-like, return null.
757 194
        if ($result === null) {
758 49
            return null;
759
        }
760
        // If field type is List, complete each item in the list with the inner type
761 176
        if ($returnType instanceof ListOfType) {
762 60
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
763
        }
764
        // Account for invalid schema definition when typeLoader returns different
765
        // instance than `resolveType` or $field->getType() or $arg->getType()
766 175
        if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
767 1
            $hint = '';
768 1
            if ($this->exeContext->schema->getConfig()->typeLoader !== null) {
769 1
                $hint = sprintf(
770 1
                    'Make sure that type loader returns the same instance as defined in %s.%s',
771 1
                    $info->parentType,
772 1
                    $info->fieldName
773
                );
774
            }
775 1
            throw new InvariantViolation(
776 1
                sprintf(
777
                    'Schema must contain unique named types but contains multiple types named "%s". %s ' .
778 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
779 1
                    $returnType,
780 1
                    $hint
781
                )
782
            );
783
        }
784
        // If field type is Scalar or Enum, serialize to a valid value, returning
785
        // null if serialization is not possible.
786 174
        if ($returnType instanceof LeafType) {
787 158
            return $this->completeLeafValue($returnType, $result);
788
        }
789 96
        if ($returnType instanceof AbstractType) {
790 33
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
791
        }
792
        // Field type must be Object, Interface or Union and expect sub-selections.
793 64
        if ($returnType instanceof ObjectType) {
794 64
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
795
        }
796
        throw new RuntimeException(sprintf('Cannot complete value of unexpected type "%s".', $returnType));
797
    }
798
799
    /**
800
     * @param mixed $value
801
     *
802
     * @return bool
803
     */
804 212
    private function isPromise($value)
805
    {
806 212
        return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
807
    }
808
809
    /**
810
     * Only returns the value if it acts like a Promise, i.e. has a "then" function,
811
     * otherwise returns null.
812
     *
813
     * @param mixed $value
814
     *
815
     * @return Promise|null
816
     */
817 209
    private function getPromise($value)
818
    {
819 209
        if ($value === null || $value instanceof Promise) {
820 77
            return $value;
821
        }
822 192
        if ($this->exeContext->promiseAdapter->isThenable($value)) {
823 39
            $promise = $this->exeContext->promiseAdapter->convertThenable($value);
824 39
            if (! $promise instanceof Promise) {
0 ignored issues
show
introduced by
$promise is always a sub-type of GraphQL\Executor\Promise\Promise.
Loading history...
825
                throw new InvariantViolation(sprintf(
826
                    '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
827
                    get_class($this->exeContext->promiseAdapter),
828
                    Utils::printSafe($promise)
829
                ));
830
            }
831
832 39
            return $promise;
833
        }
834
835 184
        return null;
836
    }
837
838
    /**
839
     * Similar to array_reduce(), however the reducing callback may return
840
     * a Promise, in which case reduction will continue after each promise resolves.
841
     *
842
     * If the callback does not return a Promise, then this function will also not
843
     * return a Promise.
844
     *
845
     * @param mixed[]            $values
846
     * @param Promise|mixed|null $initialValue
847
     *
848
     * @return Promise|mixed|null
849
     */
850 6
    private function promiseReduce(array $values, callable $callback, $initialValue)
851
    {
852 6
        return array_reduce(
853 6
            $values,
854
            function ($previous, $value) use ($callback) {
855 6
                $promise = $this->getPromise($previous);
856 6
                if ($promise !== null) {
857
                    return $promise->then(static function ($resolved) use ($callback, $value) {
858 2
                        return $callback($resolved, $value);
859 2
                    });
860
                }
861
862 6
                return $callback($previous, $value);
863 6
            },
864 6
            $initialValue
865
        );
866
    }
867
868
    /**
869
     * Complete a list value by completing each item in the list with the inner type.
870
     *
871
     * @param FieldNode[]         $fieldNodes
872
     * @param mixed[]             $path
873
     * @param mixed[]|Traversable $results
874
     *
875
     * @return mixed[]|Promise
876
     *
877
     * @throws Exception
878
     */
879 60
    private function completeListValue(ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$results)
880
    {
881 60
        $itemType = $returnType->getWrappedType();
882 60
        Utils::invariant(
883 60
            is_array($results) || $results instanceof Traversable,
884 60
            'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
885
        );
886 60
        $containsPromise = false;
887 60
        $i               = 0;
888 60
        $completedItems  = [];
889 60
        foreach ($results as $item) {
890 59
            $fieldPath     = $path;
891 59
            $fieldPath[]   = $i++;
892 59
            $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...
893 59
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
894 59
            if (! $containsPromise && $this->getPromise($completedItem) !== null) {
895 13
                $containsPromise = true;
896
            }
897 59
            $completedItems[] = $completedItem;
898
        }
899
900 60
        return $containsPromise
901 13
            ? $this->exeContext->promiseAdapter->all($completedItems)
902 60
            : $completedItems;
903
    }
904
905
    /**
906
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
907
     *
908
     * @param  mixed $result
909
     *
910
     * @return mixed
911
     *
912
     * @throws Exception
913
     */
914 158
    private function completeLeafValue(LeafType $returnType, &$result)
915
    {
916
        try {
917 158
            return $returnType->serialize($result);
918 3
        } catch (Throwable $error) {
919 3
            throw new InvariantViolation(
920 3
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
921 3
                0,
922 3
                $error
923
            );
924
        }
925
    }
926
927
    /**
928
     * Complete a value of an abstract type by determining the runtime object type
929
     * of that value, then complete the value for that type.
930
     *
931
     * @param FieldNode[] $fieldNodes
932
     * @param mixed[]     $path
933
     * @param mixed[]     $result
934
     *
935
     * @return mixed
936
     *
937
     * @throws Error
938
     */
939 33
    private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
940
    {
941 33
        $exeContext  = $this->exeContext;
942 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

942
        $runtimeType = $returnType->resolveType(/** @scrutinizer ignore-type */ $result, $exeContext->contextValue, $info);
Loading history...
943 33
        if ($runtimeType === null) {
944 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

944
            /** @scrutinizer ignore-call */ 
945
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
945
        }
946 33
        $promise = $this->getPromise($runtimeType);
947 33
        if ($promise !== null) {
948
            return $promise->then(function ($resolvedRuntimeType) use (
949 5
                $returnType,
950 5
                $fieldNodes,
951 5
                $info,
952 5
                $path,
953 5
                &$result
954
            ) {
955 5
                return $this->completeObjectValue(
956 5
                    $this->ensureValidRuntimeType(
957 5
                        $resolvedRuntimeType,
958 5
                        $returnType,
959 5
                        $info,
960 5
                        $result
961
                    ),
962 5
                    $fieldNodes,
963 5
                    $info,
964 5
                    $path,
965 5
                    $result
966
                );
967 7
            });
968
        }
969
970 26
        return $this->completeObjectValue(
971 26
            $this->ensureValidRuntimeType(
972 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

972
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
973 26
                $returnType,
974 26
                $info,
975 26
                $result
976
            ),
977 24
            $fieldNodes,
978 24
            $info,
979 24
            $path,
980 24
            $result
981
        );
982
    }
983
984
    /**
985
     * If a resolveType function is not given, then a default resolve behavior is
986
     * used which attempts two strategies:
987
     *
988
     * First, See if the provided value has a `__typename` field defined, if so, use
989
     * that value as name of the resolved type.
990
     *
991
     * Otherwise, test each possible type for the abstract type by calling
992
     * isTypeOf for the object being coerced, returning the first type that matches.
993
     *
994
     * @param mixed|null              $value
995
     * @param mixed|null              $contextValue
996
     * @param InterfaceType|UnionType $abstractType
997
     *
998
     * @return ObjectType|Promise|null
999
     */
1000 11
    private function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
1001
    {
1002
        // First, look for `__typename`.
1003 11
        if ($value !== null &&
1004 11
            (is_array($value) || $value instanceof ArrayAccess) &&
1005 11
            isset($value['__typename']) &&
1006 11
            is_string($value['__typename'])
1007
        ) {
1008 2
            return $value['__typename'];
1009
        }
1010
1011 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader !== null) {
1012 1
            Warning::warnOnce(
1013 1
                sprintf(
1014
                    'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
1015
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1016
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1017 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1018 1
                    $abstractType->name,
1019 1
                    Utils::printSafe($value)
1020
                ),
1021 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1022
            );
1023
        }
1024
        // Otherwise, test each possible type.
1025 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1026 9
        $promisedIsTypeOfResults = [];
1027 9
        foreach ($possibleTypes as $index => $type) {
1028 9
            $isTypeOfResult = $type->isTypeOf($value, $contextValue, $info);
1029 9
            if ($isTypeOfResult === null) {
1030
                continue;
1031
            }
1032 9
            $promise = $this->getPromise($isTypeOfResult);
1033 9
            if ($promise !== null) {
1034 3
                $promisedIsTypeOfResults[$index] = $promise;
1035 6
            } elseif ($isTypeOfResult) {
1036 9
                return $type;
1037
            }
1038
        }
1039 3
        if (! empty($promisedIsTypeOfResults)) {
1040 3
            return $this->exeContext->promiseAdapter->all($promisedIsTypeOfResults)
1041
                ->then(static function ($isTypeOfResults) use ($possibleTypes) {
1042 2
                    foreach ($isTypeOfResults as $index => $result) {
1043 2
                        if ($result) {
1044 2
                            return $possibleTypes[$index];
1045
                        }
1046
                    }
1047
1048
                    return null;
1049 3
                });
1050
        }
1051
1052
        return null;
1053
    }
1054
1055
    /**
1056
     * Complete an Object value by executing all sub-selections.
1057
     *
1058
     * @param FieldNode[] $fieldNodes
1059
     * @param mixed[]     $path
1060
     * @param mixed       $result
1061
     *
1062
     * @return mixed[]|Promise|stdClass
1063
     *
1064
     * @throws Error
1065
     */
1066 92
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1067
    {
1068
        // If there is an isTypeOf predicate function, call it with the
1069
        // current result. If isTypeOf returns false, then raise an error rather
1070
        // than continuing execution.
1071 92
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1072 92
        if ($isTypeOf !== null) {
1073 11
            $promise = $this->getPromise($isTypeOf);
1074 11
            if ($promise !== null) {
1075
                return $promise->then(function ($isTypeOfResult) use (
1076 2
                    $returnType,
1077 2
                    $fieldNodes,
1078 2
                    $path,
1079 2
                    &$result
1080
                ) {
1081 2
                    if (! $isTypeOfResult) {
1082
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1083
                    }
1084
1085 2
                    return $this->collectAndExecuteSubfields(
1086 2
                        $returnType,
1087 2
                        $fieldNodes,
1088 2
                        $path,
1089 2
                        $result
1090
                    );
1091 2
                });
1092
            }
1093 9
            if (! $isTypeOf) {
1094 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1095
            }
1096
        }
1097
1098 90
        return $this->collectAndExecuteSubfields(
1099 90
            $returnType,
1100 90
            $fieldNodes,
1101 90
            $path,
1102 90
            $result
1103
        );
1104
    }
1105
1106
    /**
1107
     * @param mixed[]     $result
1108
     * @param FieldNode[] $fieldNodes
1109
     *
1110
     * @return Error
1111
     */
1112 1
    private function invalidReturnTypeError(
1113
        ObjectType $returnType,
1114
        $result,
1115
        $fieldNodes
1116
    ) {
1117 1
        return new Error(
1118 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1119 1
            $fieldNodes
1120
        );
1121
    }
1122
1123
    /**
1124
     * @param FieldNode[] $fieldNodes
1125
     * @param mixed[]     $path
1126
     * @param mixed       $result
1127
     *
1128
     * @return mixed[]|Promise|stdClass
1129
     *
1130
     * @throws Error
1131
     */
1132 92
    private function collectAndExecuteSubfields(
1133
        ObjectType $returnType,
1134
        $fieldNodes,
1135
        $path,
1136
        &$result
1137
    ) {
1138 92
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
0 ignored issues
show
Bug introduced by
$fieldNodes of type GraphQL\Language\AST\FieldNode[] is incompatible with the type object expected by parameter $fieldNodes of GraphQL\Executor\Referen...tor::collectSubFields(). ( Ignorable by Annotation )

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

1138
        $subFieldNodes = $this->collectSubFields($returnType, /** @scrutinizer ignore-type */ $fieldNodes);
Loading history...
1139
1140 92
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1141
    }
1142
1143
    /**
1144
     * A memoized collection of relevant subfields with regard to the return
1145
     * type. Memoizing ensures the subfields are not repeatedly calculated, which
1146
     * saves overhead when resolving lists of values.
1147
     *
1148
     * @param object $fieldNodes
1149
     */
1150 92
    private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayObject
1151
    {
1152 92
        if (! isset($this->subFieldCache[$returnType])) {
1153 92
            $this->subFieldCache[$returnType] = new SplObjectStorage();
1154
        }
1155 92
        if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
1156
            // Collect sub-fields to execute to complete this value.
1157 92
            $subFieldNodes        = new ArrayObject();
1158 92
            $visitedFragmentNames = new ArrayObject();
1159 92
            foreach ($fieldNodes as $fieldNode) {
1160 92
                if (! isset($fieldNode->selectionSet)) {
1161
                    continue;
1162
                }
1163 92
                $subFieldNodes = $this->collectFields(
1164 92
                    $returnType,
1165 92
                    $fieldNode->selectionSet,
1166 92
                    $subFieldNodes,
1167 92
                    $visitedFragmentNames
1168
                );
1169
            }
1170 92
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
1171
        }
1172
1173 92
        return $this->subFieldCache[$returnType][$fieldNodes];
1174
    }
1175
1176
    /**
1177
     * Implements the "Evaluating selection sets" section of the spec
1178
     * for "read" mode.
1179
     *
1180
     * @param mixed       $rootValue
1181
     * @param mixed[]     $path
1182
     * @param ArrayObject $fields
1183
     *
1184
     * @return Promise|stdClass|mixed[]
1185
     */
1186 208
    private function executeFields(ObjectType $parentType, $rootValue, $path, $fields)
1187
    {
1188 208
        $containsPromise = false;
1189 208
        $results         = [];
1190 208
        foreach ($fields as $responseName => $fieldNodes) {
1191 208
            $fieldPath   = $path;
1192 208
            $fieldPath[] = $responseName;
1193 208
            $result      = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
1194 206
            if ($result === self::$UNDEFINED) {
1195 6
                continue;
1196
            }
1197 203
            if (! $containsPromise && $this->isPromise($result)) {
1198 38
                $containsPromise = true;
1199
            }
1200 203
            $results[$responseName] = $result;
1201
        }
1202
        // If there are no promises, we can just return the object
1203 206
        if (! $containsPromise) {
1204 177
            return self::fixResultsIfEmptyArray($results);
1205
        }
1206
1207
        // Otherwise, results is a map from field name to the result of resolving that
1208
        // field, which is possibly a promise. Return a promise that will return this
1209
        // same map, but with any promises replaced with the values they resolved to.
1210 38
        return $this->promiseForAssocArray($results);
1211
    }
1212
1213
    /**
1214
     * @see https://github.com/webonyx/graphql-php/issues/59
1215
     *
1216
     * @param mixed[] $results
1217
     *
1218
     * @return stdClass|mixed[]
1219
     */
1220 208
    private static function fixResultsIfEmptyArray($results)
1221
    {
1222 208
        if ($results === []) {
1223 5
            return new stdClass();
1224
        }
1225
1226 204
        return $results;
1227
    }
1228
1229
    /**
1230
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1231
     * a `Promise<array<key,scalar|array>>`
1232
     *
1233
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1234
     * any promises.
1235
     *
1236
     * @param (string|Promise)[] $assoc
1237
     *
1238
     * @return mixed
1239
     */
1240 38
    private function promiseForAssocArray(array $assoc)
1241
    {
1242 38
        $keys              = array_keys($assoc);
1243 38
        $valuesAndPromises = array_values($assoc);
1244 38
        $promise           = $this->exeContext->promiseAdapter->all($valuesAndPromises);
1245
1246
        return $promise->then(static function ($values) use ($keys) {
1247 36
            $resolvedResults = [];
1248 36
            foreach ($values as $i => $value) {
1249 36
                $resolvedResults[$keys[$i]] = $value;
1250
            }
1251
1252 36
            return self::fixResultsIfEmptyArray($resolvedResults);
1253 38
        });
1254
    }
1255
1256
    /**
1257
     * @param string|ObjectType|null  $runtimeTypeOrName
1258
     * @param InterfaceType|UnionType $returnType
1259
     * @param mixed                   $result
1260
     *
1261
     * @return ObjectType
1262
     */
1263 31
    private function ensureValidRuntimeType(
1264
        $runtimeTypeOrName,
1265
        AbstractType $returnType,
1266
        ResolveInfo $info,
1267
        &$result
1268
    ) {
1269 31
        $runtimeType = is_string($runtimeTypeOrName)
1270 4
            ? $this->exeContext->schema->getType($runtimeTypeOrName)
1271 31
            : $runtimeTypeOrName;
1272 31
        if (! $runtimeType instanceof ObjectType) {
1273 1
            throw new InvariantViolation(
1274 1
                sprintf(
1275
                    'Abstract type %s must resolve to an Object type at ' .
1276
                    'runtime for field %s.%s with value "%s", received "%s". ' .
1277
                    'Either the %s type should provide a "resolveType" ' .
1278 1
                    'function or each possible type should provide an "isTypeOf" function.',
1279 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

1279
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1280 1
                    $info->parentType,
1281 1
                    $info->fieldName,
1282 1
                    Utils::printSafe($result),
1283 1
                    Utils::printSafe($runtimeType),
1284 1
                    $returnType
1285
                )
1286
            );
1287
        }
1288 30
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1289 4
            throw new InvariantViolation(
1290 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1291
            );
1292
        }
1293 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...
1294 1
            throw new InvariantViolation(
1295 1
                sprintf(
1296
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1297
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1298
                    'type instance as referenced anywhere else within the schema ' .
1299 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1300 1
                    $runtimeType,
1301 1
                    $returnType
1302
                )
1303
            );
1304
        }
1305
1306 29
        return $runtimeType;
1307
    }
1308
}
1309