ReferenceExecutor::create()
last analyzed

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

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

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

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

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

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

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

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

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

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