Completed
Push — master ( a01b08...b72ba3 )
by Vladimir
16s queued 14s
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 17
cts 17
cp 1
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 216
    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 216
        $exeContext = self::buildExecutionContext(
83 216
            $schema,
84 216
            $documentNode,
85 216
            $rootValue,
86 216
            $contextValue,
87 216
            $variableValues,
88 216
            $operationName,
89 216
            $fieldResolver,
90 216
            $promiseAdapter
91
        );
92
93 216
        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 16
                public function __construct(Promise $result)
100
                {
101 16
                    $this->result = $result;
102 16
                }
103
104 16
                public function doExecute() : Promise
105
                {
106 16
                    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 216
    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 216
        $errors    = [];
136 216
        $fragments = [];
137
        /** @var OperationDefinitionNode|null $operation */
138 216
        $operation                    = null;
139 216
        $hasMultipleAssumedOperations = false;
140 216
        foreach ($documentNode->definitions as $definition) {
141
            switch (true) {
142 216
                case $definition instanceof OperationDefinitionNode:
143 214
                    if ($operationName === null && $operation !== null) {
144 1
                        $hasMultipleAssumedOperations = true;
145
                    }
146 214
                    if ($operationName === null ||
147 214
                        (isset($definition->name) && $definition->name->value === $operationName)) {
148 213
                        $operation = $definition;
149
                    }
150 214
                    break;
151 17
                case $definition instanceof FragmentDefinitionNode:
152 16
                    $fragments[$definition->name->value] = $definition;
153 216
                    break;
154
            }
155
        }
156 216
        if ($operation === null) {
157 3
            if ($operationName === null) {
158 2
                $errors[] = new Error('Must provide an operation.');
159
            } else {
160 3
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
161
            }
162 213
        } elseif ($hasMultipleAssumedOperations) {
163 1
            $errors[] = new Error(
164 1
                'Must provide operation name if query contains multiple operations.'
165
            );
166
        }
167 216
        $variableValues = null;
168 216
        if ($operation !== null) {
169 213
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
170 213
                $schema,
171 213
                $operation->variableDefinitions ?: [],
172 213
                $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 213
            if (empty($coercionErrors)) {
175 202
                $variableValues = $coercedVariableValues;
176
            } else {
177 12
                $errors = array_merge($errors, $coercionErrors);
178
            }
179
        }
180 216
        if (! empty($errors)) {
181 16
            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->promiseAdapter->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->promiseAdapter->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 === null) {
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 === null) {
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 === null) {
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 === null || ! $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 201
    private function shouldIncludeNode($node) : bool
403
    {
404 201
        $variableValues = $this->exeContext->variableValues;
405 201
        $skipDirective  = Directive::skipDirective();
406 201
        $skip           = Values::getDirectiveValues(
407 201
            $skipDirective,
408 201
            $node,
409 201
            $variableValues
410
        );
411 201
        if (isset($skip['if']) && $skip['if'] === true) {
412 5
            return false;
413
        }
414 201
        $includeDirective = Directive::includeDirective();
415 201
        $include          = Values::getDirectiveValues(
416 201
            $includeDirective,
417 201
            $node,
418 201
            $variableValues
419
        );
420
421 201
        return ! isset($include['if']) || $include['if'] !== false;
422
    }
423
424
    /**
425
     * Implements the logic to compute the key of a given fields entry
426
     */
427 201
    private static function getFieldEntryKey(FieldNode $node) : string
428
    {
429 201
        return $node->alias === null ? $node->name->value : $node->alias->value;
430
    }
431
432
    /**
433
     * Determines if a fragment is applicable to the given type.
434
     *
435
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
436
     *
437
     * @return bool
438
     */
439 30
    private function doesFragmentConditionMatch(
440
        $fragment,
441
        ObjectType $type
442
    ) {
443 30
        $typeConditionNode = $fragment->typeCondition;
444 30
        if ($typeConditionNode === null) {
445 1
            return true;
446
        }
447 29
        $conditionalType = TypeInfo::typeFromAST($this->exeContext->schema, $typeConditionNode);
448 29
        if ($conditionalType === $type) {
0 ignored issues
show
introduced by
The condition $conditionalType === $type is always false.
Loading history...
449 29
            return true;
450
        }
451 18
        if ($conditionalType instanceof AbstractType) {
452 1
            return $this->exeContext->schema->isPossibleType($conditionalType, $type);
453
        }
454
455 18
        return false;
456
    }
457
458
    /**
459
     * Implements the "Evaluating selection sets" section of the spec
460
     * for "write" mode.
461
     *
462
     * @param mixed       $rootValue
463
     * @param mixed[]     $path
464
     * @param ArrayObject $fields
465
     *
466
     * @return Promise|stdClass|mixed[]
467
     */
468 6
    private function executeFieldsSerially(ObjectType $parentType, $rootValue, $path, $fields)
469
    {
470 6
        $result = $this->promiseReduce(
471 6
            array_keys($fields->getArrayCopy()),
472
            function ($results, $responseName) use ($path, $parentType, $rootValue, $fields) {
473 6
                $fieldNodes  = $fields[$responseName];
474 6
                $fieldPath   = $path;
475 6
                $fieldPath[] = $responseName;
476 6
                $result      = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
477 6
                if ($result === self::$UNDEFINED) {
478 1
                    return $results;
479
                }
480 5
                $promise = $this->getPromise($result);
481 5
                if ($promise !== null) {
482
                    return $promise->then(static function ($resolvedResult) use ($responseName, $results) {
483 2
                        $results[$responseName] = $resolvedResult;
484
485 2
                        return $results;
486 2
                    });
487
                }
488 5
                $results[$responseName] = $result;
489
490 5
                return $results;
491 6
            },
492 6
            []
493
        );
494 6
        if ($this->isPromise($result)) {
495
            return $result->then(static function ($resolvedResults) {
496 2
                return self::fixResultsIfEmptyArray($resolvedResults);
497 2
            });
498
        }
499
500 4
        return self::fixResultsIfEmptyArray($result);
501
    }
502
503
    /**
504
     * Resolves the field on the given root value.
505
     *
506
     * In particular, this figures out the value that the field returns
507
     * by calling its resolve function, then calls completeValue to complete promises,
508
     * serialize scalars, or execute the sub-selection-set for objects.
509
     *
510
     * @param mixed       $rootValue
511
     * @param FieldNode[] $fieldNodes
512
     * @param mixed[]     $path
513
     *
514
     * @return mixed[]|Exception|mixed|null
515
     */
516 201
    private function resolveField(ObjectType $parentType, $rootValue, $fieldNodes, $path)
517
    {
518 201
        $exeContext = $this->exeContext;
519 201
        $fieldNode  = $fieldNodes[0];
520 201
        $fieldName  = $fieldNode->name->value;
521 201
        $fieldDef   = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
522 201
        if ($fieldDef === null) {
523 7
            return self::$UNDEFINED;
524
        }
525 197
        $returnType = $fieldDef->getType();
526
        // The resolve function's optional 3rd argument is a context value that
527
        // is provided to every resolve function within an execution. It is commonly
528
        // used to represent an authenticated user, or request-specific caches.
529 197
        $context = $exeContext->contextValue;
530
        // The resolve function's optional 4th argument is a collection of
531
        // information about the current execution state.
532 197
        $info = new ResolveInfo(
533 197
            $fieldName,
534 197
            $fieldNodes,
535 197
            $returnType,
536 197
            $parentType,
537 197
            $path,
538 197
            $exeContext->schema,
539 197
            $exeContext->fragments,
540 197
            $exeContext->rootValue,
541 197
            $exeContext->operation,
542 197
            $exeContext->variableValues
543
        );
544 197
        if ($fieldDef->resolveFn !== null) {
545 145
            $resolveFn = $fieldDef->resolveFn;
546 119
        } elseif ($parentType->resolveFieldFn !== null) {
547
            $resolveFn = $parentType->resolveFieldFn;
548
        } else {
549 119
            $resolveFn = $this->exeContext->fieldResolver;
550
        }
551
        // Get the resolve function, regardless of if its result is normal
552
        // or abrupt (error).
553 197
        $result = $this->resolveOrError(
554 197
            $fieldDef,
555 197
            $fieldNode,
556 197
            $resolveFn,
557 197
            $rootValue,
558 197
            $context,
559 197
            $info
560
        );
561 197
        $result = $this->completeValueCatchingError(
562 197
            $returnType,
563 197
            $fieldNodes,
564 197
            $info,
565 197
            $path,
566 197
            $result
567
        );
568
569 195
        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 201
    private function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
583
    {
584 201
        static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
585 201
        $schemaMetaFieldDef   = $schemaMetaFieldDef ?: Introspection::schemaMetaFieldDef();
586 201
        $typeMetaFieldDef     = $typeMetaFieldDef ?: Introspection::typeMetaFieldDef();
587 201
        $typeNameMetaFieldDef = $typeNameMetaFieldDef ?: Introspection::typeNameMetaFieldDef();
588 201
        if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
589 5
            return $schemaMetaFieldDef;
590
        }
591
592 201
        if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
593 15
            return $typeMetaFieldDef;
594
        }
595
596 201
        if ($fieldName === $typeNameMetaFieldDef->name) {
597 7
            return $typeNameMetaFieldDef;
598
        }
599 201
        $tmp = $parentType->getFields();
600
601 201
        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 mixed           $context
613
     * @param ResolveInfo     $info
614
     *
615
     * @return Throwable|Promise|mixed
616
     */
617 197
    private function resolveOrError($fieldDef, $fieldNode, $resolveFn, $rootValue, $context, $info)
618
    {
619
        try {
620
            // Build a map of arguments from the field.arguments AST, using the
621
            // variables scope to fulfill any variable references.
622 197
            $args = Values::getArgumentValues(
623 197
                $fieldDef,
624 197
                $fieldNode,
625 197
                $this->exeContext->variableValues
626
            );
627
628 194
            return $resolveFn($rootValue, $args, $context, $info);
629 17
        } catch (Exception $error) {
630 17
            return $error;
631
        } catch (Throwable $error) {
632
            return $error;
633
        }
634
    }
635
636
    /**
637
     * This is a small wrapper around completeValue which detects and logs errors
638
     * in the execution context.
639
     *
640
     * @param FieldNode[] $fieldNodes
641
     * @param string[]    $path
642
     * @param mixed       $result
643
     *
644
     * @return mixed[]|Promise|null
645
     */
646 197
    private function completeValueCatchingError(
647
        Type $returnType,
648
        $fieldNodes,
649
        ResolveInfo $info,
650
        $path,
651
        $result
652
    ) {
653 197
        $exeContext = $this->exeContext;
654
        // If the field type is non-nullable, then it is resolved without any
655
        // protection from errors.
656 197
        if ($returnType instanceof NonNull) {
657 53
            return $this->completeValueWithLocatedError(
658 53
                $returnType,
659 53
                $fieldNodes,
660 53
                $info,
661 53
                $path,
662 53
                $result
663
            );
664
        }
665
        // Otherwise, error protection is applied, logging the error and resolving
666
        // a null value for this field if one is encountered.
667
        try {
668 192
            $completed = $this->completeValueWithLocatedError(
669 192
                $returnType,
670 192
                $fieldNodes,
671 192
                $info,
672 192
                $path,
673 192
                $result
674
            );
675 179
            $promise   = $this->getPromise($completed);
676 179
            if ($promise !== null) {
677 37
                return $promise->then(
678 37
                    null,
679
                    function ($error) use ($exeContext) {
680 24
                        $exeContext->addError($error);
681
682 24
                        return $this->exeContext->promiseAdapter->createFulfilled(null);
683 37
                    }
684
                );
685
            }
686
687 159
            return $completed;
688 29
        } catch (Error $err) {
689
            // If `completeValueWithLocatedError` returned abruptly (threw an error), log the error
690
            // and return null.
691 29
            $exeContext->addError($err);
692
693 29
            return null;
694
        }
695
    }
696
697
    /**
698
     * This is a small wrapper around completeValue which annotates errors with
699
     * location information.
700
     *
701
     * @param FieldNode[] $fieldNodes
702
     * @param string[]    $path
703
     * @param mixed       $result
704
     *
705
     * @return mixed[]|mixed|Promise|null
706
     *
707
     * @throws Error
708
     */
709 197
    public function completeValueWithLocatedError(
710
        Type $returnType,
711
        $fieldNodes,
712
        ResolveInfo $info,
713
        $path,
714
        $result
715
    ) {
716
        try {
717 197
            $completed = $this->completeValue(
718 197
                $returnType,
719 197
                $fieldNodes,
720 197
                $info,
721 197
                $path,
722 197
                $result
723
            );
724 182
            $promise   = $this->getPromise($completed);
725 182
            if ($promise !== null) {
726 40
                return $promise->then(
727 40
                    null,
728
                    function ($error) use ($fieldNodes, $path) {
729 26
                        return $this->exeContext->promiseAdapter->createRejected(Error::createLocatedError(
730 26
                            $error,
731 26
                            $fieldNodes,
732 26
                            $path
733
                        ));
734 40
                    }
735
                );
736
            }
737
738 161
            return $completed;
739 37
        } catch (Exception $error) {
740 37
            throw Error::createLocatedError($error, $fieldNodes, $path);
741
        } catch (Throwable $error) {
742
            throw Error::createLocatedError($error, $fieldNodes, $path);
743
        }
744
    }
745
746
    /**
747
     * Implements the instructions for completeValue as defined in the
748
     * "Field entries" section of the spec.
749
     *
750
     * If the field type is Non-Null, then this recursively completes the value
751
     * for the inner type. It throws a field error if that completion returns null,
752
     * as per the "Nullability" section of the spec.
753
     *
754
     * If the field type is a List, then this recursively completes the value
755
     * for the inner type on each item in the list.
756
     *
757
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
758
     * value of the type by calling the `serialize` method of GraphQL type
759
     * definition.
760
     *
761
     * If the field is an abstract type, determine the runtime type of the value
762
     * and then complete based on that type
763
     *
764
     * Otherwise, the field type expects a sub-selection set, and will complete the
765
     * value by evaluating all sub-selections.
766
     *
767
     * @param FieldNode[] $fieldNodes
768
     * @param string[]    $path
769
     * @param mixed       $result
770
     *
771
     * @return mixed[]|mixed|Promise|null
772
     *
773
     * @throws Error
774
     * @throws Throwable
775
     */
776 197
    private function completeValue(
777
        Type $returnType,
778
        $fieldNodes,
779
        ResolveInfo $info,
780
        $path,
781
        &$result
782
    ) {
783 197
        $promise = $this->getPromise($result);
784
        // If result is a Promise, apply-lift over completeValue.
785 197
        if ($promise !== null) {
786
            return $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
787 31
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
788 34
            });
789
        }
790 195
        if ($result instanceof Exception || $result instanceof Throwable) {
791 17
            throw $result;
792
        }
793
        // If field type is NonNull, complete for inner type, and throw field error
794
        // if result is null.
795 187
        if ($returnType instanceof NonNull) {
796 47
            $completed = $this->completeValue(
797 47
                $returnType->getWrappedType(),
798 47
                $fieldNodes,
799 47
                $info,
800 47
                $path,
801 47
                $result
802
            );
803 47
            if ($completed === null) {
804 15
                throw new InvariantViolation(
805 15
                    'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.'
806
                );
807
            }
808
809 41
            return $completed;
810
        }
811
        // If result is null-like, return null.
812 187
        if ($result === null) {
813 49
            return null;
814
        }
815
        // If field type is List, complete each item in the list with the inner type
816 167
        if ($returnType instanceof ListOfType) {
817 59
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
818
        }
819
        // Account for invalid schema definition when typeLoader returns different
820
        // instance than `resolveType` or $field->getType() or $arg->getType()
821 166
        if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
822 1
            $hint = '';
823 1
            if ($this->exeContext->schema->getConfig()->typeLoader !== null) {
824 1
                $hint = sprintf(
825 1
                    'Make sure that type loader returns the same instance as defined in %s.%s',
826 1
                    $info->parentType,
827 1
                    $info->fieldName
828
                );
829
            }
830 1
            throw new InvariantViolation(
831 1
                sprintf(
832
                    'Schema must contain unique named types but contains multiple types named "%s". %s ' .
833 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
834 1
                    $returnType,
835 1
                    $hint
836
                )
837
            );
838
        }
839
        // If field type is Scalar or Enum, serialize to a valid value, returning
840
        // null if serialization is not possible.
841 165
        if ($returnType instanceof LeafType) {
842 149
            return $this->completeLeafValue($returnType, $result);
843
        }
844 95
        if ($returnType instanceof AbstractType) {
845 33
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
846
        }
847
        // Field type must be Object, Interface or Union and expect sub-selections.
848 63
        if ($returnType instanceof ObjectType) {
849 63
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
850
        }
851
        throw new RuntimeException(sprintf('Cannot complete value of unexpected type "%s".', $returnType));
852
    }
853
854
    /**
855
     * @param mixed $value
856
     *
857
     * @return bool
858
     */
859 201
    private function isPromise($value)
860
    {
861 201
        return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
862
    }
863
864
    /**
865
     * Only returns the value if it acts like a Promise, i.e. has a "then" function,
866
     * otherwise returns null.
867
     *
868
     * @param mixed $value
869
     *
870
     * @return Promise|null
871
     */
872 198
    private function getPromise($value)
873
    {
874 198
        if ($value === null || $value instanceof Promise) {
875 94
            return $value;
876
        }
877 180
        if ($this->exeContext->promiseAdapter->isThenable($value)) {
878 39
            $promise = $this->exeContext->promiseAdapter->convertThenable($value);
879 39
            if (! $promise instanceof Promise) {
0 ignored issues
show
introduced by
$promise is always a sub-type of GraphQL\Executor\Promise\Promise.
Loading history...
880
                throw new InvariantViolation(sprintf(
881
                    '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
882
                    get_class($this->exeContext->promiseAdapter),
883
                    Utils::printSafe($promise)
884
                ));
885
            }
886
887 39
            return $promise;
888
        }
889
890 176
        return null;
891
    }
892
893
    /**
894
     * Similar to array_reduce(), however the reducing callback may return
895
     * a Promise, in which case reduction will continue after each promise resolves.
896
     *
897
     * If the callback does not return a Promise, then this function will also not
898
     * return a Promise.
899
     *
900
     * @param mixed[]            $values
901
     * @param Promise|mixed|null $initialValue
902
     *
903
     * @return mixed
904
     */
905 6
    private function promiseReduce(array $values, callable $callback, $initialValue)
906
    {
907 6
        return array_reduce(
908 6
            $values,
909
            function ($previous, $value) use ($callback) {
910 6
                $promise = $this->getPromise($previous);
911 6
                if ($promise !== null) {
912
                    return $promise->then(static function ($resolved) use ($callback, $value) {
913 2
                        return $callback($resolved, $value);
914 2
                    });
915
                }
916
917 6
                return $callback($previous, $value);
918 6
            },
919 6
            $initialValue
920
        );
921
    }
922
923
    /**
924
     * Complete a list value by completing each item in the list with the inner type.
925
     *
926
     * @param FieldNode[]         $fieldNodes
927
     * @param mixed[]             $path
928
     * @param mixed[]|Traversable $results
929
     *
930
     * @return mixed[]|Promise
931
     *
932
     * @throws Exception
933
     */
934 59
    private function completeListValue(ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$results)
935
    {
936 59
        $itemType = $returnType->getWrappedType();
937 59
        Utils::invariant(
938 59
            is_array($results) || $results instanceof Traversable,
939 59
            'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
940
        );
941 59
        $containsPromise = false;
942 59
        $i               = 0;
943 59
        $completedItems  = [];
944 59
        foreach ($results as $item) {
945 58
            $fieldPath     = $path;
946 58
            $fieldPath[]   = $i++;
947 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...
948 58
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
949 58
            if (! $containsPromise && $this->getPromise($completedItem) !== null) {
950 13
                $containsPromise = true;
951
            }
952 58
            $completedItems[] = $completedItem;
953
        }
954
955 59
        return $containsPromise
956 13
            ? $this->exeContext->promiseAdapter->all($completedItems)
957 59
            : $completedItems;
958
    }
959
960
    /**
961
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
962
     *
963
     * @param  mixed $result
964
     *
965
     * @return mixed
966
     *
967
     * @throws Exception
968
     */
969 149
    private function completeLeafValue(LeafType $returnType, &$result)
970
    {
971
        try {
972 149
            return $returnType->serialize($result);
973 3
        } catch (Exception $error) {
974 3
            throw new InvariantViolation(
975 3
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
976 3
                0,
977 3
                $error
978
            );
979
        } catch (Throwable $error) {
980
            throw new InvariantViolation(
981
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
982
                0,
983
                $error
984
            );
985
        }
986
    }
987
988
    /**
989
     * Complete a value of an abstract type by determining the runtime object type
990
     * of that value, then complete the value for that type.
991
     *
992
     * @param FieldNode[] $fieldNodes
993
     * @param mixed[]     $path
994
     * @param mixed[]     $result
995
     *
996
     * @return mixed
997
     *
998
     * @throws Error
999
     */
1000 33
    private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1001
    {
1002 33
        $exeContext  = $this->exeContext;
1003 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

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

1005
            /** @scrutinizer ignore-call */ 
1006
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
1006
        }
1007 33
        $promise = $this->getPromise($runtimeType);
1008 33
        if ($promise !== null) {
1009
            return $promise->then(function ($resolvedRuntimeType) use (
1010 5
                $returnType,
1011 5
                $fieldNodes,
1012 5
                $info,
1013 5
                $path,
1014 5
                &$result
1015
            ) {
1016 5
                return $this->completeObjectValue(
1017 5
                    $this->ensureValidRuntimeType(
1018 5
                        $resolvedRuntimeType,
1019 5
                        $returnType,
1020 5
                        $info,
1021 5
                        $result
1022
                    ),
1023 5
                    $fieldNodes,
1024 5
                    $info,
1025 5
                    $path,
1026 5
                    $result
1027
                );
1028 7
            });
1029
        }
1030
1031 26
        return $this->completeObjectValue(
1032 26
            $this->ensureValidRuntimeType(
1033 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

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

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