GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

ReferenceExecutor::buildResponse()
last analyzed

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

}

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

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

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

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

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

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

}

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

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

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

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

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

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

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

944
            /** @scrutinizer ignore-call */ 
945
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
945 33
        }
946
        $promise = $this->getPromise($runtimeType);
947 5
        if ($promise !== null) {
948 5
            return $promise->then(function ($resolvedRuntimeType) use (
949 5
                $returnType,
950 5
                $fieldNodes,
951 5
                $info,
952
                $path,
953 5
                &$result
954 5
            ) {
955 5
                return $this->completeObjectValue(
956 5
                    $this->ensureValidRuntimeType(
957 5
                        $resolvedRuntimeType,
958 5
                        $returnType,
959
                        $info,
960 5
                        $result
961 5
                    ),
962 5
                    $fieldNodes,
963 5
                    $info,
964
                    $path,
965 7
                    $result
966
                );
967
            });
968 26
        }
969 26
970 26
        return $this->completeObjectValue(
971 26
            $this->ensureValidRuntimeType(
972 26
                $runtimeType,
0 ignored issues
show
Bug introduced by
It seems like $runtimeType can also be of type GraphQL\Executor\Promise\Promise; however, parameter $runtimeTypeOrName of GraphQL\Executor\Referen...nsureValidRuntimeType() does only seem to accept GraphQL\Type\Definition\ObjectType|null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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