Completed
Push — master ( 974258...005b1a )
by Vladimir
19:57 queued 16:19
created

ReferenceExecutor::buildExecutionContext()

Size

Total Lines 71
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 48
c 2
b 0
f 0
dl 0
loc 71
ccs 46
cts 46
cp 1
nc 168
nop 8

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

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

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

226
            return $data->/** @scrutinizer ignore-call */ then(function ($resolved) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

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

259
                return $result->/** @scrutinizer ignore-call */ then(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
260
                    null,
261
                    function ($error) {
262
                        if ($error instanceof Error) {
263
                            $this->exeContext->addError($error);
264
265
                            return $this->exeContext->promiseAdapter->createFulfilled(null);
266
                        }
267
                    }
268
                );
269
            }
270
271
            return $result;
272
        } catch (Error $error) {
273
            $this->exeContext->addError($error);
274
275
            return null;
276
        }
277
    }
278
279
    /**
280
     * Extracts the root type of the operation from the schema.
281
     *
282
     * @return ObjectType
283
     *
284
     * @throws Error
285
     */
286 7
    private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation)
287
    {
288 7
        switch ($operation->operation) {
289 7
            case 'query':
290 7
                $queryType = $schema->getQueryType();
291 7
                if ($queryType === null) {
292
                    throw new Error(
293
                        'Schema does not define the required query root type.',
294
                        [$operation]
295
                    );
296
                }
297
298 7
                return $queryType;
299
            case 'mutation':
300
                $mutationType = $schema->getMutationType();
301
                if ($mutationType === null) {
302
                    throw new Error(
303
                        'Schema is not configured for mutations.',
304
                        [$operation]
305
                    );
306
                }
307
308
                return $mutationType;
309
            case 'subscription':
310
                $subscriptionType = $schema->getSubscriptionType();
311
                if ($subscriptionType === null) {
312
                    throw new Error(
313
                        'Schema is not configured for subscriptions.',
314
                        [$operation]
315
                    );
316
                }
317
318
                return $subscriptionType;
319
            default:
320
                throw new Error(
321
                    'Can only execute queries, mutations and subscriptions.',
322
                    [$operation]
323
                );
324
        }
325
    }
326
327
    /**
328
     * Given a selectionSet, adds all of the fields in that selection to
329
     * the passed in map of fields, and returns it at the end.
330
     *
331
     * CollectFields requires the "runtime type" of an object. For a field which
332
     * returns an Interface or Union type, the "runtime type" will be the actual
333
     * Object type returned by that field.
334
     *
335
     * @param ArrayObject $fields
336
     * @param ArrayObject $visitedFragmentNames
337
     *
338
     * @return ArrayObject
339
     */
340 7
    private function collectFields(
341
        ObjectType $runtimeType,
342
        SelectionSetNode $selectionSet,
343
        $fields,
344
        $visitedFragmentNames
345
    ) {
346 7
        $exeContext = $this->exeContext;
347 7
        foreach ($selectionSet->selections as $selection) {
348
            switch (true) {
349 7
                case $selection instanceof FieldNode:
350 7
                    if (! $this->shouldIncludeNode($selection)) {
351
                        break;
352
                    }
353
                    $name = self::getFieldEntryKey($selection);
354
                    if (! isset($fields[$name])) {
355
                        $fields[$name] = new ArrayObject();
356
                    }
357
                    $fields[$name][] = $selection;
358
                    break;
359
                case $selection instanceof InlineFragmentNode:
360
                    if (! $this->shouldIncludeNode($selection) ||
361
                        ! $this->doesFragmentConditionMatch($selection, $runtimeType)
362
                    ) {
363
                        break;
364
                    }
365
                    $this->collectFields(
366
                        $runtimeType,
367
                        $selection->selectionSet,
368
                        $fields,
369
                        $visitedFragmentNames
370
                    );
371
                    break;
372
                case $selection instanceof FragmentSpreadNode:
373
                    $fragName = $selection->name->value;
374
                    if (! empty($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) {
375
                        break;
376
                    }
377
                    $visitedFragmentNames[$fragName] = true;
378
                    /** @var FragmentDefinitionNode|null $fragment */
379
                    $fragment = $exeContext->fragments[$fragName] ?? null;
380
                    if ($fragment === null || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
381
                        break;
382
                    }
383
                    $this->collectFields(
384
                        $runtimeType,
385
                        $fragment->selectionSet,
386
                        $fields,
387
                        $visitedFragmentNames
388
                    );
389
                    break;
390
            }
391
        }
392
393
        return $fields;
394
    }
395
396
    /**
397
     * Determines if a field should be included based on the @include and @skip
398
     * directives, where @skip has higher precedence than @include.
399
     *
400
     * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
401
     */
402 7
    private function shouldIncludeNode($node) : bool
403
    {
404 7
        $variableValues = $this->exeContext->variableValues;
405 7
        $skipDirective  = Directive::skipDirective();
406
        $skip           = Values::getDirectiveValues(
407
            $skipDirective,
408
            $node,
409
            $variableValues
410
        );
411
        if (isset($skip['if']) && $skip['if'] === true) {
412
            return false;
413
        }
414
        $includeDirective = Directive::includeDirective();
415
        $include          = Values::getDirectiveValues(
416
            $includeDirective,
417
            $node,
418
            $variableValues
419
        );
420
421
        return ! isset($include['if']) || $include['if'] !== false;
422
    }
423
424
    /**
425
     * Implements the logic to compute the key of a given fields entry
426
     */
427
    private static function getFieldEntryKey(FieldNode $node) : string
428
    {
429
        return $node->alias === null ? $node->name->value : $node->alias->value;
430
    }
431
432
    /**
433
     * Determines if a fragment is applicable to the given type.
434
     *
435
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
436
     *
437
     * @return bool
438
     */
439
    private function doesFragmentConditionMatch(
440
        $fragment,
441
        ObjectType $type
442
    ) {
443
        $typeConditionNode = $fragment->typeCondition;
444
        if ($typeConditionNode === null) {
445
            return true;
446
        }
447
        $conditionalType = TypeInfo::typeFromAST($this->exeContext->schema, $typeConditionNode);
448
        if ($conditionalType === $type) {
0 ignored issues
show
introduced by
The condition $conditionalType === $type is always false.
Loading history...
449
            return true;
450
        }
451
        if ($conditionalType instanceof AbstractType) {
452
            return $this->exeContext->schema->isPossibleType($conditionalType, $type);
453
        }
454
455
        return false;
456
    }
457
458
    /**
459
     * Implements the "Evaluating selection sets" section of the spec
460
     * for "write" mode.
461
     *
462
     * @param mixed       $rootValue
463
     * @param mixed[]     $path
464
     * @param ArrayObject $fields
465
     *
466
     * @return Promise|stdClass|mixed[]
467
     */
468
    private function executeFieldsSerially(ObjectType $parentType, $rootValue, $path, $fields)
469
    {
470
        $result = $this->promiseReduce(
471
            array_keys($fields->getArrayCopy()),
472
            function ($results, $responseName) use ($path, $parentType, $rootValue, $fields) {
473
                $fieldNodes  = $fields[$responseName];
474
                $fieldPath   = $path;
475
                $fieldPath[] = $responseName;
476
                $result      = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
477
                if ($result === self::$UNDEFINED) {
478
                    return $results;
479
                }
480
                $promise = $this->getPromise($result);
481
                if ($promise !== null) {
482
                    return $promise->then(static function ($resolvedResult) use ($responseName, $results) {
483
                        $results[$responseName] = $resolvedResult;
484
485
                        return $results;
486
                    });
487
                }
488
                $results[$responseName] = $result;
489
490
                return $results;
491
            },
492
            []
493
        );
494
        if ($this->isPromise($result)) {
495
            return $result->then(static function ($resolvedResults) {
496
                return self::fixResultsIfEmptyArray($resolvedResults);
497
            });
498
        }
499
500
        return self::fixResultsIfEmptyArray($result);
501
    }
502
503
    /**
504
     * Resolves the field on the given root value.
505
     *
506
     * In particular, this figures out the value that the field returns
507
     * by calling its resolve function, then calls completeValue to complete promises,
508
     * serialize scalars, or execute the sub-selection-set for objects.
509
     *
510
     * @param mixed       $rootValue
511
     * @param FieldNode[] $fieldNodes
512
     * @param mixed[]     $path
513
     *
514
     * @return mixed[]|Exception|mixed|null
515
     */
516
    private function resolveField(ObjectType $parentType, $rootValue, $fieldNodes, $path)
517
    {
518
        $exeContext = $this->exeContext;
519
        $fieldNode  = $fieldNodes[0];
520
        $fieldName  = $fieldNode->name->value;
521
        $fieldDef   = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
522
        if ($fieldDef === null) {
523
            return self::$UNDEFINED;
524
        }
525
        $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
        $context = $exeContext->contextValue;
530
        // The resolve function's optional 4th argument is a collection of
531
        // information about the current execution state.
532
        $info = new ResolveInfo(
533
            $fieldName,
534
            $fieldNodes,
535
            $returnType,
536
            $parentType,
537
            $path,
538
            $exeContext->schema,
539
            $exeContext->fragments,
540
            $exeContext->rootValue,
541
            $exeContext->operation,
542
            $exeContext->variableValues
543
        );
544
        if ($fieldDef->resolveFn !== null) {
545
            $resolveFn = $fieldDef->resolveFn;
546
        } elseif ($parentType->resolveFieldFn !== null) {
547
            $resolveFn = $parentType->resolveFieldFn;
548
        } else {
549
            $resolveFn = $this->exeContext->fieldResolver;
550
        }
551
        // Get the resolve function, regardless of if its result is normal
552
        // or abrupt (error).
553
        $result = $this->resolveOrError(
554
            $fieldDef,
555
            $fieldNode,
556
            $resolveFn,
557
            $rootValue,
558
            $context,
559
            $info
560
        );
561
        $result = $this->completeValueCatchingError(
562
            $returnType,
563
            $fieldNodes,
564
            $info,
565
            $path,
566
            $result
567
        );
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
     * definitions, which would cause issues.
581
     */
582
    private function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
583
    {
584
        static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
585
        $schemaMetaFieldDef   = $schemaMetaFieldDef ?: Introspection::schemaMetaFieldDef();
586
        $typeMetaFieldDef     = $typeMetaFieldDef ?: Introspection::typeMetaFieldDef();
587
        $typeNameMetaFieldDef = $typeNameMetaFieldDef ?: Introspection::typeNameMetaFieldDef();
588
        if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
589
            return $schemaMetaFieldDef;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $schemaMetaFieldDef could return the type GraphQL\Type\Definition\Type which is incompatible with the type-hinted return GraphQL\Type\Definition\FieldDefinition|null. Consider adding an additional type-check to rule them out.
Loading history...
590
        }
591
592
        if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
593
            return $typeMetaFieldDef;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $typeMetaFieldDef could return the type GraphQL\Type\Definition\Type which is incompatible with the type-hinted return GraphQL\Type\Definition\FieldDefinition|null. Consider adding an additional type-check to rule them out.
Loading history...
594
        }
595
596
        if ($fieldName === $typeNameMetaFieldDef->name) {
597
            return $typeNameMetaFieldDef;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $typeNameMetaFieldDef could return the type GraphQL\Type\Definition\Type which is incompatible with the type-hinted return GraphQL\Type\Definition\FieldDefinition|null. Consider adding an additional type-check to rule them out.
Loading history...
598
        }
599
        $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 mixed           $context
613
     * @param ResolveInfo     $info
614
     *
615
     * @return Throwable|Promise|mixed
616
     */
617
    private function resolveOrError($fieldDef, $fieldNode, $resolveFn, $rootValue, $context, $info)
618
    {
619
        try {
620
            // Build a map of arguments from the field.arguments AST, using the
621
            // variables scope to fulfill any variable references.
622
            $args = Values::getArgumentValues(
623
                $fieldDef,
624
                $fieldNode,
625
                $this->exeContext->variableValues
626
            );
627
628
            return $resolveFn($rootValue, $args, $context, $info);
629
        } catch (Exception $error) {
630
            return $error;
631
        } catch (Throwable $error) {
632
            return $error;
633
        }
634
    }
635
636
    /**
637
     * This is a small wrapper around completeValue which detects and logs errors
638
     * in the execution context.
639
     *
640
     * @param FieldNode[] $fieldNodes
641
     * @param string[]    $path
642
     * @param mixed       $result
643
     *
644
     * @return mixed[]|Promise|null
645
     */
646
    private function completeValueCatchingError(
647
        Type $returnType,
648
        $fieldNodes,
649
        ResolveInfo $info,
650
        $path,
651
        $result
652
    ) {
653
        $exeContext = $this->exeContext;
654
        // If the field type is non-nullable, then it is resolved without any
655
        // protection from errors.
656
        if ($returnType instanceof NonNull) {
657
            return $this->completeValueWithLocatedError(
658
                $returnType,
659
                $fieldNodes,
660
                $info,
661
                $path,
662
                $result
663
            );
664
        }
665
        // Otherwise, error protection is applied, logging the error and resolving
666
        // a null value for this field if one is encountered.
667
        try {
668
            $completed = $this->completeValueWithLocatedError(
669
                $returnType,
670
                $fieldNodes,
671
                $info,
672
                $path,
673
                $result
674
            );
675
            $promise   = $this->getPromise($completed);
676
            if ($promise !== null) {
677
                return $promise->then(
678
                    null,
679
                    function ($error) use ($exeContext) {
680
                        $exeContext->addError($error);
681
682
                        return $this->exeContext->promiseAdapter->createFulfilled(null);
683
                    }
684
                );
685
            }
686
687
            return $completed;
688
        } catch (Error $err) {
689
            // If `completeValueWithLocatedError` returned abruptly (threw an error), log the error
690
            // and return null.
691
            $exeContext->addError($err);
692
693
            return null;
694
        }
695
    }
696
697
    /**
698
     * This is a small wrapper around completeValue which annotates errors with
699
     * location information.
700
     *
701
     * @param FieldNode[] $fieldNodes
702
     * @param string[]    $path
703
     * @param mixed       $result
704
     *
705
     * @return mixed[]|mixed|Promise|null
706
     *
707
     * @throws Error
708
     */
709
    public function completeValueWithLocatedError(
710
        Type $returnType,
711
        $fieldNodes,
712
        ResolveInfo $info,
713
        $path,
714
        $result
715
    ) {
716
        try {
717
            $completed = $this->completeValue(
718
                $returnType,
719
                $fieldNodes,
720
                $info,
721
                $path,
722
                $result
723
            );
724
            $promise   = $this->getPromise($completed);
725
            if ($promise !== null) {
726
                return $promise->then(
727
                    null,
728
                    function ($error) use ($fieldNodes, $path) {
729
                        return $this->exeContext->promiseAdapter->createRejected(Error::createLocatedError(
730
                            $error,
731
                            $fieldNodes,
732
                            $path
733
                        ));
734
                    }
735
                );
736
            }
737
738
            return $completed;
739
        } catch (Exception $error) {
740
            throw Error::createLocatedError($error, $fieldNodes, $path);
741
        } catch (Throwable $error) {
742
            throw Error::createLocatedError($error, $fieldNodes, $path);
743
        }
744
    }
745
746
    /**
747
     * Implements the instructions for completeValue as defined in the
748
     * "Field entries" section of the spec.
749
     *
750
     * If the field type is Non-Null, then this recursively completes the value
751
     * for the inner type. It throws a field error if that completion returns null,
752
     * as per the "Nullability" section of the spec.
753
     *
754
     * If the field type is a List, then this recursively completes the value
755
     * for the inner type on each item in the list.
756
     *
757
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
758
     * value of the type by calling the `serialize` method of GraphQL type
759
     * definition.
760
     *
761
     * If the field is an abstract type, determine the runtime type of the value
762
     * and then complete based on that type
763
     *
764
     * Otherwise, the field type expects a sub-selection set, and will complete the
765
     * value by evaluating all sub-selections.
766
     *
767
     * @param FieldNode[] $fieldNodes
768
     * @param string[]    $path
769
     * @param mixed       $result
770
     *
771
     * @return mixed[]|mixed|Promise|null
772
     *
773
     * @throws Error
774
     * @throws Throwable
775
     */
776
    private function completeValue(
777
        Type $returnType,
778
        $fieldNodes,
779
        ResolveInfo $info,
780
        $path,
781
        &$result
782
    ) {
783
        $promise = $this->getPromise($result);
784
        // If result is a Promise, apply-lift over completeValue.
785
        if ($promise !== null) {
786
            return $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
787
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
788
            });
789
        }
790
        if ($result instanceof Exception || $result instanceof Throwable) {
791
            throw $result;
792
        }
793
        // If field type is NonNull, complete for inner type, and throw field error
794
        // if result is null.
795
        if ($returnType instanceof NonNull) {
796
            $completed = $this->completeValue(
797
                $returnType->getWrappedType(),
798
                $fieldNodes,
799
                $info,
800
                $path,
801
                $result
802
            );
803
            if ($completed === null) {
804
                throw new InvariantViolation(
805
                    'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.'
806
                );
807
            }
808
809
            return $completed;
810
        }
811
        // If result is null-like, return null.
812
        if ($result === null) {
813
            return null;
814
        }
815
        // If field type is List, complete each item in the list with the inner type
816
        if ($returnType instanceof ListOfType) {
817
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
818
        }
819
        // Account for invalid schema definition when typeLoader returns different
820
        // instance than `resolveType` or $field->getType() or $arg->getType()
821
        if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
822
            $hint = '';
823
            if ($this->exeContext->schema->getConfig()->typeLoader !== null) {
824
                $hint = sprintf(
825
                    'Make sure that type loader returns the same instance as defined in %s.%s',
826
                    $info->parentType,
827
                    $info->fieldName
828
                );
829
            }
830
            throw new InvariantViolation(
831
                sprintf(
832
                    'Schema must contain unique named types but contains multiple types named "%s". %s ' .
833
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
834
                    $returnType,
835
                    $hint
836
                )
837
            );
838
        }
839
        // If field type is Scalar or Enum, serialize to a valid value, returning
840
        // null if serialization is not possible.
841
        if ($returnType instanceof LeafType) {
842
            return $this->completeLeafValue($returnType, $result);
843
        }
844
        if ($returnType instanceof AbstractType) {
845
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
846
        }
847
        // Field type must be Object, Interface or Union and expect sub-selections.
848
        if ($returnType instanceof ObjectType) {
849
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
850
        }
851
        throw new RuntimeException(sprintf('Cannot complete value of unexpected type "%s".', $returnType));
852
    }
853
854
    /**
855
     * @param mixed $value
856
     *
857
     * @return bool
858
     */
859
    private function isPromise($value)
860
    {
861
        return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
862
    }
863
864
    /**
865
     * Only returns the value if it acts like a Promise, i.e. has a "then" function,
866
     * otherwise returns null.
867
     *
868
     * @param mixed $value
869
     *
870
     * @return Promise|null
871
     */
872
    private function getPromise($value)
873
    {
874
        if ($value === null || $value instanceof Promise) {
875
            return $value;
876
        }
877
        if ($this->exeContext->promiseAdapter->isThenable($value)) {
878
            $promise = $this->exeContext->promiseAdapter->convertThenable($value);
879
            if (! $promise instanceof Promise) {
0 ignored issues
show
introduced by
$promise is always a sub-type of GraphQL\Executor\Promise\Promise.
Loading history...
880
                throw new InvariantViolation(sprintf(
881
                    '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
882
                    get_class($this->exeContext->promiseAdapter),
883
                    Utils::printSafe($promise)
884
                ));
885
            }
886
887
            return $promise;
888
        }
889
890
        return null;
891
    }
892
893
    /**
894
     * Similar to array_reduce(), however the reducing callback may return
895
     * a Promise, in which case reduction will continue after each promise resolves.
896
     *
897
     * If the callback does not return a Promise, then this function will also not
898
     * return a Promise.
899
     *
900
     * @param mixed[]            $values
901
     * @param Promise|mixed|null $initialValue
902
     *
903
     * @return mixed
904
     */
905
    private function promiseReduce(array $values, callable $callback, $initialValue)
906
    {
907
        return array_reduce(
908
            $values,
909
            function ($previous, $value) use ($callback) {
910
                $promise = $this->getPromise($previous);
911
                if ($promise !== null) {
912
                    return $promise->then(static function ($resolved) use ($callback, $value) {
913
                        return $callback($resolved, $value);
914
                    });
915
                }
916
917
                return $callback($previous, $value);
918
            },
919
            $initialValue
920
        );
921
    }
922
923
    /**
924
     * Complete a list value by completing each item in the list with the inner type.
925
     *
926
     * @param FieldNode[]         $fieldNodes
927
     * @param mixed[]             $path
928
     * @param mixed[]|Traversable $results
929
     *
930
     * @return mixed[]|Promise
931
     *
932
     * @throws Exception
933
     */
934
    private function completeListValue(ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$results)
935
    {
936
        $itemType = $returnType->getWrappedType();
937
        Utils::invariant(
938
            is_array($results) || $results instanceof Traversable,
939
            'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
940
        );
941
        $containsPromise = false;
942
        $i               = 0;
943
        $completedItems  = [];
944
        foreach ($results as $item) {
945
            $fieldPath     = $path;
946
            $fieldPath[]   = $i++;
947
            $info->path    = $fieldPath;
0 ignored issues
show
Documentation Bug introduced by
$fieldPath is of type array<mixed,integer|mixed>, but the property $path was declared to be of type array<mixed,string[]>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

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

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

class Alien {}

class Dalek extends Alien {}

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

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
948
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
949
            if (! $containsPromise && $this->getPromise($completedItem) !== null) {
950
                $containsPromise = true;
951
            }
952
            $completedItems[] = $completedItem;
953
        }
954
955
        return $containsPromise
956
            ? $this->exeContext->promiseAdapter->all($completedItems)
957
            : $completedItems;
958
    }
959
960
    /**
961
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
962
     *
963
     * @param  mixed $result
964
     *
965
     * @return mixed
966
     *
967
     * @throws Exception
968
     */
969
    private function completeLeafValue(LeafType $returnType, &$result)
970
    {
971
        try {
972
            return $returnType->serialize($result);
973
        } catch (Exception $error) {
974
            throw new InvariantViolation(
975
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
976
                0,
977
                $error
978
            );
979
        } catch (Throwable $error) {
980
            throw new InvariantViolation(
981
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
982
                0,
983
                $error
984
            );
985
        }
986
    }
987
988
    /**
989
     * Complete a value of an abstract type by determining the runtime object type
990
     * of that value, then complete the value for that type.
991
     *
992
     * @param FieldNode[] $fieldNodes
993
     * @param mixed[]     $path
994
     * @param mixed[]     $result
995
     *
996
     * @return mixed
997
     *
998
     * @throws Error
999
     */
1000
    private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1001
    {
1002
        $exeContext  = $this->exeContext;
1003
        $runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
0 ignored issues
show
Bug introduced by
$result of type array<mixed,mixed> is incompatible with the type object expected by parameter $objectValue of GraphQL\Type\Definition\...ractType::resolveType(). ( Ignorable by Annotation )

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

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

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

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

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

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

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

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