Failed Conditions
Push — master ( 15672a...e51596 )
by Vladimir
03:57
created

Executor::promiseToExecute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 28
c 0
b 0
f 0
rs 9.8333
ccs 14
cts 14
cp 1
cc 2
nc 2
nop 8
crap 2

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Executor;
6
7
use ArrayObject;
8
use GraphQL\Error\Error;
9
use GraphQL\Error\InvariantViolation;
10
use GraphQL\Error\Warning;
11
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
12
use GraphQL\Executor\Promise\Promise;
13
use GraphQL\Executor\Promise\PromiseAdapter;
14
use GraphQL\Language\AST\DocumentNode;
15
use GraphQL\Language\AST\FieldNode;
16
use GraphQL\Language\AST\FragmentDefinitionNode;
17
use GraphQL\Language\AST\FragmentSpreadNode;
18
use GraphQL\Language\AST\InlineFragmentNode;
19
use GraphQL\Language\AST\NodeKind;
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\Introspection;
33
use GraphQL\Type\Schema;
34
use GraphQL\Utils\TypeInfo;
35
use GraphQL\Utils\Utils;
36
use function array_keys;
37
use function array_merge;
38
use function array_values;
39
use function get_class;
40
use function is_array;
41
use function is_object;
42
use function is_string;
43
use function sprintf;
44
45
/**
46
 * Implements the "Evaluating requests" section of the GraphQL specification.
47
 */
48
class Executor
49
{
50
    /** @var object */
51
    private static $UNDEFINED;
52
53
    /** @var callable|string[] */
54
    private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
55
56
    /** @var PromiseAdapter */
57
    private static $promiseAdapter;
58
59
    /** @var ExecutionContext */
60
    private $exeContext;
61
62 187
    private function __construct(ExecutionContext $context)
63
    {
64 187
        if (! self::$UNDEFINED) {
65 1
            self::$UNDEFINED = Utils::undefined();
66
        }
67
68 187
        $this->exeContext = $context;
69 187
    }
70
71
    /**
72
     * Custom default resolve function
73
     *
74
     * @throws \Exception
75
     */
76
    public static function setDefaultFieldResolver(callable $fn)
77
    {
78
        self::$defaultFieldResolver = $fn;
79
    }
80
81
    /**
82
     * Executes DocumentNode against given $schema.
83
     *
84
     * Always returns ExecutionResult and never throws. All errors which occur during operation
85
     * execution are collected in `$result->errors`.
86
     *
87
     * @api
88
     * @param mixed|null                $rootValue
89
     * @param mixed[]|null              $contextValue
90
     * @param mixed[]|\ArrayAccess|null $variableValues
91
     * @param string|null               $operationName
92
     *
93
     * @return ExecutionResult|Promise
94
     */
95 116
    public static function execute(
96
        Schema $schema,
97
        DocumentNode $ast,
98
        $rootValue = null,
99
        $contextValue = null,
100
        $variableValues = null,
101
        $operationName = null,
102
        ?callable $fieldResolver = null
103
    ) {
104
        // TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
105 116
        $promiseAdapter = self::getPromiseAdapter();
106 116
        $result         = self::promiseToExecute(
107 116
            $promiseAdapter,
108 116
            $schema,
109 116
            $ast,
110 116
            $rootValue,
111 116
            $contextValue,
112 116
            $variableValues,
0 ignored issues
show
Bug introduced by
It seems like $variableValues can also be of type ArrayAccess; however, parameter $variableValues of GraphQL\Executor\Executor::promiseToExecute() does only seem to accept null|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

112
            /** @scrutinizer ignore-type */ $variableValues,
Loading history...
113 116
            $operationName,
114 116
            $fieldResolver
115
        );
116
117
        // Wait for promised results when using sync promises
118 116
        if ($promiseAdapter instanceof SyncPromiseAdapter) {
119 116
            $result = $promiseAdapter->wait($result);
120
        }
121
122 116
        return $result;
123
    }
124
125
    /**
126
     * @return PromiseAdapter
127
     */
128 143
    public static function getPromiseAdapter()
129
    {
130 143
        return self::$promiseAdapter ?: (self::$promiseAdapter = new SyncPromiseAdapter());
131
    }
132
133 24
    public static function setPromiseAdapter(?PromiseAdapter $promiseAdapter = null)
134
    {
135 24
        self::$promiseAdapter = $promiseAdapter;
136 24
    }
137
138
    /**
139
     * Same as execute(), but requires promise adapter and returns a promise which is always
140
     * fulfilled with an instance of ExecutionResult and never rejected.
141
     *
142
     * Useful for async PHP platforms.
143
     *
144
     * @api
145
     * @param mixed[]|null $rootValue
146
     * @param mixed[]|null $contextValue
147
     * @param mixed[]|null $variableValues
148
     * @param string|null  $operationName
149
     * @return Promise
150
     */
151 200
    public static function promiseToExecute(
152
        PromiseAdapter $promiseAdapter,
153
        Schema $schema,
154
        DocumentNode $ast,
155
        $rootValue = null,
156
        $contextValue = null,
157
        $variableValues = null,
158
        $operationName = null,
159
        ?callable $fieldResolver = null
160
    ) {
161 200
        $exeContext = self::buildExecutionContext(
162 200
            $schema,
163 200
            $ast,
164 200
            $rootValue,
165 200
            $contextValue,
166 200
            $variableValues,
167 200
            $operationName,
168 200
            $fieldResolver,
169 200
            $promiseAdapter
170
        );
171
172 200
        if (is_array($exeContext)) {
173 14
            return $promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext));
174
        }
175
176 187
        $executor = new self($exeContext);
177
178 187
        return $executor->doExecute();
179
    }
180
181
    /**
182
     * Constructs an ExecutionContext object from the arguments passed to
183
     * execute, which we will pass throughout the other execution methods.
184
     *
185
     * @param mixed[]              $rootValue
186
     * @param mixed[]              $contextValue
187
     * @param mixed[]|\Traversable $rawVariableValues
188
     * @param string|null          $operationName
189
     *
190
     * @return ExecutionContext|Error[]
191
     */
192 200
    private static function buildExecutionContext(
193
        Schema $schema,
194
        DocumentNode $documentNode,
195
        $rootValue,
196
        $contextValue,
197
        $rawVariableValues,
198
        $operationName = null,
199
        ?callable $fieldResolver = null,
200
        ?PromiseAdapter $promiseAdapter = null
201
    ) {
202 200
        $errors    = [];
203 200
        $fragments = [];
204
        /** @var OperationDefinitionNode $operation */
205 200
        $operation                    = null;
206 200
        $hasMultipleAssumedOperations = false;
207
208 200
        foreach ($documentNode->definitions as $definition) {
209 200
            switch ($definition->kind) {
0 ignored issues
show
Bug introduced by
Accessing kind on the interface GraphQL\Language\AST\DefinitionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
210 200
                case NodeKind::OPERATION_DEFINITION:
211 199
                    if (! $operationName && $operation) {
212 1
                        $hasMultipleAssumedOperations = true;
213
                    }
214 199
                    if (! $operationName ||
215 199
                        (isset($definition->name) && $definition->name->value === $operationName)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface GraphQL\Language\AST\DefinitionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
216 198
                        $operation = $definition;
217
                    }
218 199
                    break;
219 14
                case NodeKind::FRAGMENT_DEFINITION:
220 13
                    $fragments[$definition->name->value] = $definition;
221 200
                    break;
222
            }
223
        }
224
225 200
        if (! $operation) {
226 2
            if ($operationName) {
227 1
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
228
            } else {
229 2
                $errors[] = new Error('Must provide an operation.');
230
            }
231 198
        } elseif ($hasMultipleAssumedOperations) {
232 1
            $errors[] = new Error(
233 1
                'Must provide operation name if query contains multiple operations.'
234
            );
235
        }
236
237 200
        $variableValues = null;
238 200
        if ($operation) {
239 198
            $coercedVariableValues = Values::getVariableValues(
240 198
                $schema,
241 198
                $operation->variableDefinitions ?: [],
242 198
                $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

242
                /** @scrutinizer ignore-type */ $rawVariableValues ?: []
Loading history...
243
            );
244
245 198
            if ($coercedVariableValues['errors']) {
246 11
                $errors = array_merge($errors, $coercedVariableValues['errors']);
247
            } else {
248 188
                $variableValues = $coercedVariableValues['coerced'];
249
            }
250
        }
251
252 200
        if ($errors) {
253 14
            return $errors;
254
        }
255
256 187
        Utils::invariant($operation, 'Has operation if no errors.');
257 187
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
258
259 187
        return new ExecutionContext(
260 187
            $schema,
261 187
            $fragments,
262 187
            $rootValue,
263 187
            $contextValue,
264 187
            $operation,
265 187
            $variableValues,
266 187
            $errors,
267 187
            $fieldResolver ?: self::$defaultFieldResolver,
268 187
            $promiseAdapter ?: self::getPromiseAdapter()
269
        );
270
    }
271
272
    /**
273
     * @return Promise
274
     */
275 187
    private function doExecute()
276
    {
277
        // Return a Promise that will eventually resolve to the data described by
278
        // The "Response" section of the GraphQL specification.
279
        //
280
        // If errors are encountered while executing a GraphQL field, only that
281
        // field and its descendants will be omitted, and sibling fields will still
282
        // be executed. An execution which encounters errors will still result in a
283
        // resolved Promise.
284
        $result = $this->exeContext->promises->create(function (callable $resolve) {
285 187
            return $resolve($this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue));
286 187
        });
287
288
        return $result
289 187
            ->then(
290 187
                null,
291
                function ($error) {
292
                    // Errors from sub-fields of a NonNull type may propagate to the top level,
293
                    // at which point we still log the error and null the parent field, which
294
                    // in this case is the entire response.
295
                    $this->exeContext->addError($error);
296
297
                    return null;
298 187
                }
299
            )
300
            ->then(function ($data) {
301 187
                if ($data !== null) {
302 183
                    $data = (array) $data;
303
                }
304
305 187
                return new ExecutionResult($data, $this->exeContext->errors);
306 187
            });
307
    }
308
309
    /**
310
     * Implements the "Evaluating operations" section of the spec.
311
     *
312
     * @param  mixed[] $rootValue
313
     * @return Promise|\stdClass|mixed[]
314
     */
315 187
    private function executeOperation(OperationDefinitionNode $operation, $rootValue)
316
    {
317 187
        $type   = $this->getOperationRootType($this->exeContext->schema, $operation);
318 187
        $fields = $this->collectFields($type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
319
320 187
        $path = [];
321
322
        // Errors from sub-fields of a NonNull type may propagate to the top level,
323
        // at which point we still log the error and null the parent field, which
324
        // in this case is the entire response.
325
        //
326
        // Similar to completeValueCatchingError.
327
        try {
328 187
            $result = $operation->operation === 'mutation' ?
329 5
                $this->executeFieldsSerially($type, $rootValue, $path, $fields) :
330 187
                $this->executeFields($type, $rootValue, $path, $fields);
331
332 185
            $promise = $this->getPromise($result);
333 185
            if ($promise) {
334 41
                return $promise->then(
335 41
                    null,
336
                    function ($error) {
337 2
                        $this->exeContext->addError($error);
338
339 2
                        return null;
340 41
                    }
341
                );
342
            }
343
344 145
            return $result;
345 2
        } catch (Error $error) {
346 2
            $this->exeContext->addError($error);
347
348 2
            return null;
349
        }
350
    }
351
352
    /**
353
     * Extracts the root type of the operation from the schema.
354
     *
355
     * @return ObjectType
356
     * @throws Error
357
     */
358 187
    private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation)
359
    {
360 187
        switch ($operation->operation) {
361 187
            case 'query':
362 180
                $queryType = $schema->getQueryType();
363 180
                if (! $queryType) {
0 ignored issues
show
introduced by
$queryType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
364
                    throw new Error(
365
                        'Schema does not define the required query root type.',
366
                        [$operation]
367
                    );
368
                }
369
370 180
                return $queryType;
371 7
            case 'mutation':
372 5
                $mutationType = $schema->getMutationType();
373 5
                if (! $mutationType) {
0 ignored issues
show
introduced by
$mutationType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
374
                    throw new Error(
375
                        'Schema is not configured for mutations.',
376
                        [$operation]
377
                    );
378
                }
379
380 5
                return $mutationType;
381 2
            case 'subscription':
382 2
                $subscriptionType = $schema->getSubscriptionType();
383 2
                if (! $subscriptionType) {
0 ignored issues
show
introduced by
$subscriptionType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
384
                    throw new Error(
385
                        'Schema is not configured for subscriptions.',
386
                        [$operation]
387
                    );
388
                }
389
390 2
                return $subscriptionType;
391
            default:
392
                throw new Error(
393
                    'Can only execute queries, mutations and subscriptions.',
394
                    [$operation]
395
                );
396
        }
397
    }
398
399
    /**
400
     * Given a selectionSet, adds all of the fields in that selection to
401
     * the passed in map of fields, and returns it at the end.
402
     *
403
     * CollectFields requires the "runtime type" of an object. For a field which
404
     * returns an Interface or Union type, the "runtime type" will be the actual
405
     * Object type returned by that field.
406
     *
407
     * @param ArrayObject $fields
408
     * @param ArrayObject $visitedFragmentNames
409
     *
410
     * @return \ArrayObject
411
     */
412 187
    private function collectFields(
413
        ObjectType $runtimeType,
414
        SelectionSetNode $selectionSet,
415
        $fields,
416
        $visitedFragmentNames
417
    ) {
418 187
        $exeContext = $this->exeContext;
419 187
        foreach ($selectionSet->selections as $selection) {
420 187
            switch ($selection->kind) {
0 ignored issues
show
Bug introduced by
Accessing kind on the interface GraphQL\Language\AST\SelectionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
421 187
                case NodeKind::FIELD:
422 187
                    if (! $this->shouldIncludeNode($selection)) {
423 2
                        break;
424
                    }
425 187
                    $name = self::getFieldEntryKey($selection);
426 187
                    if (! isset($fields[$name])) {
427 187
                        $fields[$name] = new \ArrayObject();
428
                    }
429 187
                    $fields[$name][] = $selection;
430 187
                    break;
431 29
                case NodeKind::INLINE_FRAGMENT:
432 21
                    if (! $this->shouldIncludeNode($selection) ||
433 21
                        ! $this->doesFragmentConditionMatch($selection, $runtimeType)
434
                    ) {
435 19
                        break;
436
                    }
437 21
                    $this->collectFields(
438 21
                        $runtimeType,
439 21
                        $selection->selectionSet,
0 ignored issues
show
Bug introduced by
Accessing selectionSet on the interface GraphQL\Language\AST\SelectionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
440 21
                        $fields,
441 21
                        $visitedFragmentNames
442
                    );
443 21
                    break;
444 10
                case NodeKind::FRAGMENT_SPREAD:
445 10
                    $fragName = $selection->name->value;
0 ignored issues
show
Bug introduced by
Accessing name on the interface GraphQL\Language\AST\SelectionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
446 10
                    if (! empty($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) {
447 2
                        break;
448
                    }
449 10
                    $visitedFragmentNames[$fragName] = true;
450
451
                    /** @var FragmentDefinitionNode|null $fragment */
452 10
                    $fragment = $exeContext->fragments[$fragName] ?? null;
453 10
                    if (! $fragment || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
454 1
                        break;
455
                    }
456 9
                    $this->collectFields(
457 9
                        $runtimeType,
458 9
                        $fragment->selectionSet,
459 9
                        $fields,
460 9
                        $visitedFragmentNames
461
                    );
462 187
                    break;
463
            }
464
        }
465
466 187
        return $fields;
467
    }
468
469
    /**
470
     * Determines if a field should be included based on the @include and @skip
471
     * directives, where @skip has higher precedence than @include.
472
     *
473
     * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
474
     * @return bool
475
     */
476 187
    private function shouldIncludeNode($node)
477
    {
478 187
        $variableValues = $this->exeContext->variableValues;
479 187
        $skipDirective  = Directive::skipDirective();
480
481 187
        $skip = Values::getDirectiveValues(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $skip is correct as GraphQL\Executor\Values:...$node, $variableValues) targeting GraphQL\Executor\Values::getDirectiveValues() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
482 187
            $skipDirective,
483 187
            $node,
484 187
            $variableValues
485
        );
486
487 187
        if (isset($skip['if']) && $skip['if'] === true) {
488 5
            return false;
489
        }
490
491 187
        $includeDirective = Directive::includeDirective();
492
493 187
        $include = Values::getDirectiveValues(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $include is correct as GraphQL\Executor\Values:...$node, $variableValues) targeting GraphQL\Executor\Values::getDirectiveValues() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
494 187
            $includeDirective,
495 187
            $node,
496 187
            $variableValues
497
        );
498
499 187
        if (isset($include['if']) && $include['if'] === false) {
500 5
            return false;
501
        }
502
503 187
        return true;
504
    }
505
506
    /**
507
     * Implements the logic to compute the key of a given fields entry
508
     *
509
     * @return string
510
     */
511 187
    private static function getFieldEntryKey(FieldNode $node)
512
    {
513 187
        return $node->alias ? $node->alias->value : $node->name->value;
514
    }
515
516
    /**
517
     * Determines if a fragment is applicable to the given type.
518
     *
519
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
520
     * @return bool
521
     */
522 29
    private function doesFragmentConditionMatch(
523
        $fragment,
524
        ObjectType $type
525
    ) {
526 29
        $typeConditionNode = $fragment->typeCondition;
527
528 29
        if ($typeConditionNode === null) {
529 1
            return true;
530
        }
531
532 28
        $conditionalType = TypeInfo::typeFromAST($this->exeContext->schema, $typeConditionNode);
533 28
        if ($conditionalType === $type) {
0 ignored issues
show
introduced by
The condition $conditionalType === $type is always false.
Loading history...
534 27
            return true;
535
        }
536 18
        if ($conditionalType instanceof AbstractType) {
537 1
            return $this->exeContext->schema->isPossibleType($conditionalType, $type);
538
        }
539
540 18
        return false;
541
    }
542
543
    /**
544
     * Implements the "Evaluating selection sets" section of the spec
545
     * for "write" mode.
546
     *
547
     * @param mixed[]     $sourceValue
548
     * @param mixed[]     $path
549
     * @param ArrayObject $fields
550
     * @return Promise|\stdClass|mixed[]
551
     */
552 5
    private function executeFieldsSerially(ObjectType $parentType, $sourceValue, $path, $fields)
553
    {
554 5
        $prevPromise = $this->exeContext->promises->createFulfilled([]);
555
556
        $process = function ($results, $responseName, $path, $parentType, $sourceValue, $fieldNodes) {
557 5
            $fieldPath   = $path;
558 5
            $fieldPath[] = $responseName;
559 5
            $result      = $this->resolveField($parentType, $sourceValue, $fieldNodes, $fieldPath);
560 5
            if ($result === self::$UNDEFINED) {
561 1
                return $results;
562
            }
563 4
            $promise = $this->getPromise($result);
564 4
            if ($promise) {
565
                return $promise->then(function ($resolvedResult) use ($responseName, $results) {
566 2
                    $results[$responseName] = $resolvedResult;
567
568 2
                    return $results;
569 2
                });
570
            }
571 4
            $results[$responseName] = $result;
572
573 4
            return $results;
574 5
        };
575
576 5
        foreach ($fields as $responseName => $fieldNodes) {
577
            $prevPromise = $prevPromise->then(function ($resolvedResults) use (
578 5
                $process,
579 5
                $responseName,
580 5
                $path,
581 5
                $parentType,
582 5
                $sourceValue,
583 5
                $fieldNodes
584
            ) {
585 5
                return $process($resolvedResults, $responseName, $path, $parentType, $sourceValue, $fieldNodes);
586 5
            });
587
        }
588
589
        return $prevPromise->then(function ($resolvedResults) {
590 5
            return self::fixResultsIfEmptyArray($resolvedResults);
591 5
        });
592
    }
593
594
    /**
595
     * Resolves the field on the given source object. In particular, this
596
     * figures out the value that the field returns by calling its resolve function,
597
     * then calls completeValue to complete promises, serialize scalars, or execute
598
     * the sub-selection-set for objects.
599
     *
600
     * @param object|null $source
601
     * @param FieldNode[] $fieldNodes
602
     * @param mixed[]     $path
603
     *
604
     * @return mixed[]|\Exception|mixed|null
605
     */
606 187
    private function resolveField(ObjectType $parentType, $source, $fieldNodes, $path)
607
    {
608 187
        $exeContext = $this->exeContext;
609 187
        $fieldNode  = $fieldNodes[0];
610
611 187
        $fieldName = $fieldNode->name->value;
612 187
        $fieldDef  = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
613
614 187
        if (! $fieldDef) {
0 ignored issues
show
introduced by
$fieldDef is of type GraphQL\Type\Definition\FieldDefinition, thus it always evaluated to true.
Loading history...
615 7
            return self::$UNDEFINED;
616
        }
617
618 183
        $returnType = $fieldDef->getType();
619
620
        // The resolve function's optional third argument is a collection of
621
        // information about the current execution state.
622 183
        $info = new ResolveInfo([
623 183
            'fieldName'      => $fieldName,
624 183
            'fieldNodes'     => $fieldNodes,
625 183
            'returnType'     => $returnType,
626 183
            'parentType'     => $parentType,
627 183
            'path'           => $path,
628 183
            'schema'         => $exeContext->schema,
629 183
            'fragments'      => $exeContext->fragments,
630 183
            'rootValue'      => $exeContext->rootValue,
631 183
            'operation'      => $exeContext->operation,
632 183
            'variableValues' => $exeContext->variableValues,
633
        ]);
634
635 183
        if ($fieldDef->resolveFn !== null) {
636 132
            $resolveFn = $fieldDef->resolveFn;
637 116
        } elseif ($parentType->resolveFieldFn !== null) {
638
            $resolveFn = $parentType->resolveFieldFn;
639
        } else {
640 116
            $resolveFn = $this->exeContext->fieldResolver;
641
        }
642
643
        // The resolve function's optional third argument is a context value that
644
        // is provided to every resolve function within an execution. It is commonly
645
        // used to represent an authenticated user, or request-specific caches.
646 183
        $context = $exeContext->contextValue;
647
648
        // Get the resolve function, regardless of if its result is normal
649
        // or abrupt (error).
650 183
        $result = $this->resolveOrError(
651 183
            $fieldDef,
652 183
            $fieldNode,
653 183
            $resolveFn,
654 183
            $source,
655 183
            $context,
656 183
            $info
657
        );
658
659 183
        $result = $this->completeValueCatchingError(
660 183
            $returnType,
661 183
            $fieldNodes,
662 183
            $info,
663 183
            $path,
664 183
            $result
665
        );
666
667 181
        return $result;
668
    }
669
670
    /**
671
     * This method looks up the field on the given type definition.
672
     * It has special casing for the two introspection fields, __schema
673
     * and __typename. __typename is special because it can always be
674
     * queried as a field, even in situations where no other fields
675
     * are allowed, like on a Union. __schema could get automatically
676
     * added to the query type, but that would require mutating type
677
     * definitions, which would cause issues.
678
     *
679
     * @param string $fieldName
680
     *
681
     * @return FieldDefinition
682
     */
683 187
    private function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
684
    {
685 187
        static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
686
687 187
        $schemaMetaFieldDef   = $schemaMetaFieldDef ?: Introspection::schemaMetaFieldDef();
688 187
        $typeMetaFieldDef     = $typeMetaFieldDef ?: Introspection::typeMetaFieldDef();
689 187
        $typeNameMetaFieldDef = $typeNameMetaFieldDef ?: Introspection::typeNameMetaFieldDef();
690
691 187
        if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
692 6
            return $schemaMetaFieldDef;
693 187
        } elseif ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
694 14
            return $typeMetaFieldDef;
695 187
        } elseif ($fieldName === $typeNameMetaFieldDef->name) {
696 7
            return $typeNameMetaFieldDef;
697
        }
698
699 187
        $tmp = $parentType->getFields();
700
701 187
        return $tmp[$fieldName] ?? null;
702
    }
703
704
    /**
705
     * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
706
     * function. Returns the result of resolveFn or the abrupt-return Error object.
707
     *
708
     * @param FieldDefinition $fieldDef
709
     * @param FieldNode       $fieldNode
710
     * @param callable        $resolveFn
711
     * @param mixed           $source
712
     * @param mixed           $context
713
     * @param ResolveInfo     $info
714
     * @return \Throwable|Promise|mixed
715
     */
716 183
    private function resolveOrError($fieldDef, $fieldNode, $resolveFn, $source, $context, $info)
717
    {
718
        try {
719
            // Build hash of arguments from the field.arguments AST, using the
720
            // variables scope to fulfill any variable references.
721 183
            $args = Values::getArgumentValues(
722 183
                $fieldDef,
723 183
                $fieldNode,
724 183
                $this->exeContext->variableValues
725
            );
726
727 180
            return $resolveFn($source, $args, $context, $info);
728 16
        } catch (\Exception $error) {
729 16
            return $error;
730
        } catch (\Throwable $error) {
731
            return $error;
732
        }
733
    }
734
735
    /**
736
     * This is a small wrapper around completeValue which detects and logs errors
737
     * in the execution context.
738
     *
739
     * @param FieldNode[] $fieldNodes
740
     * @param string[]    $path
741
     * @param mixed       $result
742
     * @return mixed[]|Promise|null
743
     */
744 183
    private function completeValueCatchingError(
745
        Type $returnType,
746
        $fieldNodes,
747
        ResolveInfo $info,
748
        $path,
749
        $result
750
    ) {
751 183
        $exeContext = $this->exeContext;
752
753
        // If the field type is non-nullable, then it is resolved without any
754
        // protection from errors.
755 183
        if ($returnType instanceof NonNull) {
756 50
            return $this->completeValueWithLocatedError(
757 50
                $returnType,
758 50
                $fieldNodes,
759 50
                $info,
760 50
                $path,
761 50
                $result
762
            );
763
        }
764
765
        // Otherwise, error protection is applied, logging the error and resolving
766
        // a null value for this field if one is encountered.
767
        try {
768 179
            $completed = $this->completeValueWithLocatedError(
769 179
                $returnType,
770 179
                $fieldNodes,
771 179
                $info,
772 179
                $path,
773 179
                $result
774
            );
775
776 168
            $promise = $this->getPromise($completed);
777 168
            if ($promise) {
778 36
                return $promise->then(
779 36
                    null,
780
                    function ($error) use ($exeContext) {
781 24
                        $exeContext->addError($error);
782
783 24
                        return $this->exeContext->promises->createFulfilled(null);
784 36
                    }
785
                );
786
            }
787
788 148
            return $completed;
789 27
        } catch (Error $err) {
790
            // If `completeValueWithLocatedError` returned abruptly (threw an error), log the error
791
            // and return null.
792 27
            $exeContext->addError($err);
793
794 27
            return null;
795
        }
796
    }
797
798
    /**
799
     * This is a small wrapper around completeValue which annotates errors with
800
     * location information.
801
     *
802
     * @param FieldNode[] $fieldNodes
803
     * @param string[]    $path
804
     * @param mixed       $result
805
     * @return mixed[]|mixed|Promise|null
806
     * @throws Error
807
     */
808 183
    public function completeValueWithLocatedError(
809
        Type $returnType,
810
        $fieldNodes,
811
        ResolveInfo $info,
812
        $path,
813
        $result
814
    ) {
815
        try {
816 183
            $completed = $this->completeValue(
817 183
                $returnType,
818 183
                $fieldNodes,
819 183
                $info,
820 183
                $path,
821 183
                $result
822
            );
823 170
            $promise   = $this->getPromise($completed);
824 170
            if ($promise) {
825 38
                return $promise->then(
826 38
                    null,
827
                    function ($error) use ($fieldNodes, $path) {
828 26
                        return $this->exeContext->promises->createRejected(Error::createLocatedError(
829 26
                            $error,
830 26
                            $fieldNodes,
831 26
                            $path
832
                        ));
833 38
                    }
834
                );
835
            }
836
837 150
            return $completed;
838 35
        } catch (\Exception $error) {
839 35
            throw Error::createLocatedError($error, $fieldNodes, $path);
840
        } catch (\Throwable $error) {
841
            throw Error::createLocatedError($error, $fieldNodes, $path);
842
        }
843
    }
844
845
    /**
846
     * Implements the instructions for completeValue as defined in the
847
     * "Field entries" section of the spec.
848
     *
849
     * If the field type is Non-Null, then this recursively completes the value
850
     * for the inner type. It throws a field error if that completion returns null,
851
     * as per the "Nullability" section of the spec.
852
     *
853
     * If the field type is a List, then this recursively completes the value
854
     * for the inner type on each item in the list.
855
     *
856
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
857
     * value of the type by calling the `serialize` method of GraphQL type
858
     * definition.
859
     *
860
     * If the field is an abstract type, determine the runtime type of the value
861
     * and then complete based on that type
862
     *
863
     * Otherwise, the field type expects a sub-selection set, and will complete the
864
     * value by evaluating all sub-selections.
865
     *
866
     * @param FieldNode[] $fieldNodes
867
     * @param string[]    $path
868
     * @param mixed       $result
869
     * @return mixed[]|mixed|Promise|null
870
     * @throws Error
871
     * @throws \Throwable
872
     */
873 183
    private function completeValue(
874
        Type $returnType,
875
        $fieldNodes,
876
        ResolveInfo $info,
877
        $path,
878
        &$result
879
    ) {
880 183
        $promise = $this->getPromise($result);
881
882
        // If result is a Promise, apply-lift over completeValue.
883 183
        if ($promise) {
884
            return $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
885 29
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
886 32
            });
887
        }
888
889 181
        if ($result instanceof \Exception || $result instanceof \Throwable) {
890 16
            throw $result;
891
        }
892
893
        // If field type is NonNull, complete for inner type, and throw field error
894
        // if result is null.
895 174
        if ($returnType instanceof NonNull) {
896 44
            $completed = $this->completeValue(
897 44
                $returnType->getWrappedType(),
898 44
                $fieldNodes,
899 44
                $info,
900 44
                $path,
901 44
                $result
902
            );
903 44
            if ($completed === null) {
904 15
                throw new InvariantViolation(
905 15
                    'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.'
906
                );
907
            }
908
909 38
            return $completed;
910
        }
911
912
        // If result is null-like, return null.
913 174
        if ($result === null) {
914 47
            return null;
915
        }
916
917
        // If field type is List, complete each item in the list with the inner type
918 156
        if ($returnType instanceof ListOfType) {
919 56
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
920
        }
921
922
        // Account for invalid schema definition when typeLoader returns different
923
        // instance than `resolveType` or $field->getType() or $arg->getType()
924 156
        if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
925 1
            $hint = '';
926 1
            if ($this->exeContext->schema->getConfig()->typeLoader) {
927 1
                $hint = sprintf(
928 1
                    'Make sure that type loader returns the same instance as defined in %s.%s',
929 1
                    $info->parentType,
930 1
                    $info->fieldName
931
                );
932
            }
933 1
            throw new InvariantViolation(
934 1
                sprintf(
935
                    'Schema must contain unique named types but contains multiple types named "%s". %s ' .
936 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
937 1
                    $returnType,
938 1
                    $hint
939
                )
940
            );
941
        }
942
943
        // If field type is Scalar or Enum, serialize to a valid value, returning
944
        // null if serialization is not possible.
945 155
        if ($returnType instanceof LeafType) {
946 140
            return $this->completeLeafValue($returnType, $result);
947
        }
948
949 92
        if ($returnType instanceof AbstractType) {
950 30
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
951
        }
952
953
        // Field type must be Object, Interface or Union and expect sub-selections.
954 63
        if ($returnType instanceof ObjectType) {
955 63
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
956
        }
957
958
        throw new \RuntimeException(sprintf('Cannot complete value of unexpected type "%s".', $returnType));
959
    }
960
961
    /**
962
     * Only returns the value if it acts like a Promise, i.e. has a "then" function,
963
     * otherwise returns null.
964
     *
965
     * @param mixed $value
966
     * @return Promise|null
967
     */
968 187
    private function getPromise($value)
969
    {
970 187
        if ($value === null || $value instanceof Promise) {
971 91
            return $value;
972
        }
973 185
        if ($this->exeContext->promises->isThenable($value)) {
974 38
            $promise = $this->exeContext->promises->convertThenable($value);
975 38
            if (! $promise instanceof Promise) {
0 ignored issues
show
introduced by
$promise is always a sub-type of GraphQL\Executor\Promise\Promise.
Loading history...
976
                throw new InvariantViolation(sprintf(
977
                    '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
978
                    get_class($this->exeContext->promises),
979
                    Utils::printSafe($promise)
980
                ));
981
            }
982
983 38
            return $promise;
984
        }
985
986 181
        return null;
987
    }
988
989
    /**
990
     * Complete a list value by completing each item in the list with the
991
     * inner type
992
     *
993
     * @param FieldNode[] $fieldNodes
994
     * @param mixed[]     $path
995
     * @param mixed       $result
996
     * @return mixed[]|Promise
997
     * @throws \Exception
998
     */
999 56
    private function completeListValue(ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1000
    {
1001 56
        $itemType = $returnType->getWrappedType();
1002 56
        Utils::invariant(
1003 56
            is_array($result) || $result instanceof \Traversable,
1004 56
            'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
1005
        );
1006 56
        $containsPromise = false;
1007
1008 56
        $i              = 0;
1009 56
        $completedItems = [];
1010 56
        foreach ($result as $item) {
1011 56
            $fieldPath     = $path;
1012 56
            $fieldPath[]   = $i++;
1013 56
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
1014 56
            if (! $containsPromise && $this->getPromise($completedItem)) {
1015 13
                $containsPromise = true;
1016
            }
1017 56
            $completedItems[] = $completedItem;
1018
        }
1019
1020 56
        return $containsPromise ? $this->exeContext->promises->all($completedItems) : $completedItems;
1021
    }
1022
1023
    /**
1024
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
1025
     *
1026
     * @param  mixed $result
1027
     * @return mixed
1028
     * @throws \Exception
1029
     */
1030 140
    private function completeLeafValue(LeafType $returnType, &$result)
1031
    {
1032
        try {
1033 140
            return $returnType->serialize($result);
1034 3
        } catch (\Exception $error) {
1035 3
            throw new InvariantViolation(
1036 3
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
1037 3
                0,
1038 3
                $error
1039
            );
1040
        } catch (\Throwable $error) {
1041
            throw new InvariantViolation(
1042
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
1043
                0,
1044
                $error
1045
            );
1046
        }
1047
    }
1048
1049
    /**
1050
     * Complete a value of an abstract type by determining the runtime object type
1051
     * of that value, then complete the value for that type.
1052
     *
1053
     * @param FieldNode[] $fieldNodes
1054
     * @param mixed[]     $path
1055
     * @param mixed[]     $result
1056
     * @return mixed
1057
     * @throws Error
1058
     */
1059 30
    private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1060
    {
1061 30
        $exeContext  = $this->exeContext;
1062 30
        $runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
1063
1064 30
        if ($runtimeType === null) {
1065 11
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
0 ignored issues
show
Bug Best Practice introduced by
The method GraphQL\Executor\Executor::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

1065
            /** @scrutinizer ignore-call */ 
1066
            $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
Loading history...
1066
        }
1067
1068 30
        $promise = $this->getPromise($runtimeType);
1069 30
        if ($promise) {
1070
            return $promise->then(function ($resolvedRuntimeType) use (
1071 5
                $returnType,
1072 5
                $fieldNodes,
1073 5
                $info,
1074 5
                $path,
1075 5
                &$result
1076
            ) {
1077 5
                return $this->completeObjectValue(
1078 5
                    $this->ensureValidRuntimeType(
1079 5
                        $resolvedRuntimeType,
1080 5
                        $returnType,
1081 5
                        $info,
1082 5
                        $result
1083
                    ),
1084 5
                    $fieldNodes,
1085 5
                    $info,
1086 5
                    $path,
1087 5
                    $result
1088
                );
1089 7
            });
1090
        }
1091
1092 23
        return $this->completeObjectValue(
1093 23
            $this->ensureValidRuntimeType(
1094 23
                $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\Executo...nsureValidRuntimeType() does only seem to accept null|string|GraphQL\Type\Definition\ObjectType, 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

1094
                /** @scrutinizer ignore-type */ $runtimeType,
Loading history...
1095 23
                $returnType,
1096 23
                $info,
1097 23
                $result
1098
            ),
1099 22
            $fieldNodes,
1100 22
            $info,
1101 22
            $path,
1102 22
            $result
1103
        );
1104
    }
1105
1106
    /**
1107
     * If a resolveType function is not given, then a default resolve behavior is
1108
     * used which attempts two strategies:
1109
     *
1110
     * First, See if the provided value has a `__typename` field defined, if so, use
1111
     * that value as name of the resolved type.
1112
     *
1113
     * Otherwise, test each possible type for the abstract type by calling
1114
     * isTypeOf for the object being coerced, returning the first type that matches.
1115
     *
1116
     * @param mixed|null $value
1117
     * @param mixed|null $context
1118
     * @return ObjectType|Promise|null
1119
     */
1120 11
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
1121
    {
1122
        // First, look for `__typename`.
1123 11
        if ($value !== null &&
1124 11
            is_array($value) &&
1125 11
            isset($value['__typename']) &&
1126 11
            is_string($value['__typename'])
1127
        ) {
1128 2
            return $value['__typename'];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value['__typename'] returns the type string which is incompatible with the documented return type null|GraphQL\Executor\Pr...e\Definition\ObjectType.
Loading history...
1129
        }
1130
1131 9
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
1132 1
            Warning::warnOnce(
1133 1
                sprintf(
1134
                    'GraphQL Interface Type `%s` returned `null` from it`s `resolveType` function ' .
1135
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
1136
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
1137 1
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
1138 1
                    $abstractType->name,
1139 1
                    Utils::printSafe($value)
1140
                ),
1141 1
                Warning::WARNING_FULL_SCHEMA_SCAN
1142
            );
1143
        }
1144
1145
        // Otherwise, test each possible type.
1146 9
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
1147 9
        $promisedIsTypeOfResults = [];
1148
1149 9
        foreach ($possibleTypes as $index => $type) {
1150 9
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
1151
1152 9
            if ($isTypeOfResult === null) {
1153
                continue;
1154
            }
1155
1156 9
            $promise = $this->getPromise($isTypeOfResult);
1157 9
            if ($promise) {
1158 3
                $promisedIsTypeOfResults[$index] = $promise;
1159 6
            } elseif ($isTypeOfResult) {
1160 9
                return $type;
1161
            }
1162
        }
1163
1164 3
        if (! empty($promisedIsTypeOfResults)) {
1165 3
            return $this->exeContext->promises->all($promisedIsTypeOfResults)
1166
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
1167 2
                    foreach ($isTypeOfResults as $index => $result) {
1168 2
                        if ($result) {
1169 2
                            return $possibleTypes[$index];
1170
                        }
1171
                    }
1172
1173
                    return null;
1174 3
                });
1175
        }
1176
1177
        return null;
1178
    }
1179
1180
    /**
1181
     * Complete an Object value by executing all sub-selections.
1182
     *
1183
     * @param FieldNode[] $fieldNodes
1184
     * @param mixed[]     $path
1185
     * @param mixed       $result
1186
     * @return mixed[]|Promise|\stdClass
1187
     * @throws Error
1188
     */
1189 89
    private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result)
1190
    {
1191
        // If there is an isTypeOf predicate function, call it with the
1192
        // current result. If isTypeOf returns false, then raise an error rather
1193
        // than continuing execution.
1194 89
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
1195
1196 89
        if ($isTypeOf !== null) {
1197 11
            $promise = $this->getPromise($isTypeOf);
1198 11
            if ($promise) {
1199
                return $promise->then(function ($isTypeOfResult) use (
1200 2
                    $returnType,
1201 2
                    $fieldNodes,
1202 2
                    $info,
1203 2
                    $path,
1204 2
                    &$result
1205
                ) {
1206 2
                    if (! $isTypeOfResult) {
1207
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1208
                    }
1209
1210 2
                    return $this->collectAndExecuteSubfields(
1211 2
                        $returnType,
1212 2
                        $fieldNodes,
1213 2
                        $info,
1214 2
                        $path,
1215 2
                        $result
1216
                    );
1217 2
                });
1218
            }
1219 9
            if (! $isTypeOf) {
1220 1
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1221
            }
1222
        }
1223
1224 87
        return $this->collectAndExecuteSubfields(
1225 87
            $returnType,
1226 87
            $fieldNodes,
1227 87
            $info,
1228 87
            $path,
1229 87
            $result
1230
        );
1231
    }
1232
1233
    /**
1234
     * @param mixed[]     $result
1235
     * @param FieldNode[] $fieldNodes
1236
     * @return Error
1237
     */
1238 1
    private function invalidReturnTypeError(
1239
        ObjectType $returnType,
1240
        $result,
1241
        $fieldNodes
1242
    ) {
1243 1
        return new Error(
1244 1
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1245 1
            $fieldNodes
1246
        );
1247
    }
1248
1249
    /**
1250
     * @param FieldNode[] $fieldNodes
1251
     * @param mixed[]     $path
1252
     * @param mixed[]     $result
1253
     * @return mixed[]|Promise|\stdClass
1254
     * @throws Error
1255
     */
1256 89
    private function collectAndExecuteSubfields(
1257
        ObjectType $returnType,
1258
        $fieldNodes,
1259
        ResolveInfo $info,
0 ignored issues
show
Unused Code introduced by
The parameter $info is not used and could be removed. ( Ignorable by Annotation )

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

1259
        /** @scrutinizer ignore-unused */ ResolveInfo $info,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1260
        $path,
1261
        &$result
1262
    ) {
1263
        // Collect sub-fields to execute to complete this value.
1264 89
        $subFieldNodes        = new \ArrayObject();
1265 89
        $visitedFragmentNames = new \ArrayObject();
1266
1267 89
        foreach ($fieldNodes as $fieldNode) {
1268 89
            if (! isset($fieldNode->selectionSet)) {
1269
                continue;
1270
            }
1271
1272 89
            $subFieldNodes = $this->collectFields(
1273 89
                $returnType,
1274 89
                $fieldNode->selectionSet,
1275 89
                $subFieldNodes,
1276 89
                $visitedFragmentNames
1277
            );
1278
        }
1279
1280 89
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
1281
    }
1282
1283
    /**
1284
     * Implements the "Evaluating selection sets" section of the spec
1285
     * for "read" mode.
1286
     *
1287
     * @param mixed|null  $source
1288
     * @param mixed[]     $path
1289
     * @param ArrayObject $fields
1290
     * @return Promise|\stdClass|mixed[]
1291
     */
1292 184
    private function executeFields(ObjectType $parentType, $source, $path, $fields)
1293
    {
1294 184
        $containsPromise = false;
1295 184
        $finalResults    = [];
1296
1297 184
        foreach ($fields as $responseName => $fieldNodes) {
1298 184
            $fieldPath   = $path;
1299 184
            $fieldPath[] = $responseName;
1300 184
            $result      = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath);
1301 182
            if ($result === self::$UNDEFINED) {
1302 6
                continue;
1303
            }
1304 179
            if (! $containsPromise && $this->getPromise($result)) {
1305 36
                $containsPromise = true;
1306
            }
1307 179
            $finalResults[$responseName] = $result;
1308
        }
1309
1310
        // If there are no promises, we can just return the object
1311 182
        if (! $containsPromise) {
1312 155
            return self::fixResultsIfEmptyArray($finalResults);
1313
        }
1314
1315
        // Otherwise, results is a map from field name to the result
1316
        // of resolving that field, which is possibly a promise. Return
1317
        // a promise that will return this same map, but with any
1318
        // promises replaced with the values they resolved to.
1319 36
        return $this->promiseForAssocArray($finalResults);
1320
    }
1321
1322
    /**
1323
     * @see https://github.com/webonyx/graphql-php/issues/59
1324
     *
1325
     * @param mixed[] $results
1326
     * @return \stdClass|mixed[]
1327
     */
1328 183
    private static function fixResultsIfEmptyArray($results)
1329
    {
1330 183
        if ($results === []) {
1331 5
            return new \stdClass();
1332
        }
1333
1334 179
        return $results;
1335
    }
1336
1337
    /**
1338
     * This function transforms a PHP `array<string, Promise|scalar|array>` into
1339
     * a `Promise<array<key,scalar|array>>`
1340
     *
1341
     * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain
1342
     * any promises.
1343
     *
1344
     * @param (string|Promise)[] $assoc
1345
     * @return mixed
1346
     */
1347 36
    private function promiseForAssocArray(array $assoc)
1348
    {
1349 36
        $keys              = array_keys($assoc);
1350 36
        $valuesAndPromises = array_values($assoc);
1351
1352 36
        $promise = $this->exeContext->promises->all($valuesAndPromises);
1353
1354 36
        return $promise->then(function ($values) use ($keys) {
1355 34
            $resolvedResults = [];
1356 34
            foreach ($values as $i => $value) {
1357 34
                $resolvedResults[$keys[$i]] = $value;
1358
            }
1359
1360 34
            return self::fixResultsIfEmptyArray($resolvedResults);
1361 36
        });
1362
    }
1363
1364
    /**
1365
     * @param string|ObjectType|null $runtimeTypeOrName
1366
     * @param FieldNode[]            $fieldNodes
1367
     * @param mixed                  $result
1368
     * @return ObjectType
1369
     */
1370 28
    private function ensureValidRuntimeType(
1371
        $runtimeTypeOrName,
1372
        AbstractType $returnType,
1373
        ResolveInfo $info,
1374
        &$result
1375
    ) {
1376 28
        $runtimeType = is_string($runtimeTypeOrName) ?
1377 4
            $this->exeContext->schema->getType($runtimeTypeOrName) :
1378 28
            $runtimeTypeOrName;
1379
1380 28
        if (! $runtimeType instanceof ObjectType) {
1381
            throw new InvariantViolation(
1382
                sprintf(
1383
                    'Abstract type %1$s must resolve to an Object type at ' .
1384
                    'runtime for field %s.%s with value "%s", received "%s".' .
1385
                    'Either the %1$s type should provide a "resolveType" ' .
1386
                    'function or each possible types should provide an "isTypeOf" function.',
1387
                    $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

1387
                    /** @scrutinizer ignore-type */ $returnType,
Loading history...
1388
                    $info->parentType,
1389
                    $info->fieldName,
1390
                    Utils::printSafe($result),
1391
                    Utils::printSafe($runtimeType)
1392
                )
1393
            );
1394
        }
1395
1396 28
        if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1397 4
            throw new InvariantViolation(
1398 4
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
1399
            );
1400
        }
1401
1402 28
        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...
1403 1
            throw new InvariantViolation(
1404 1
                sprintf(
1405
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
1406
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
1407
                    'type instance as referenced anywhere else within the schema ' .
1408 1
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
1409 1
                    $runtimeType,
1410 1
                    $returnType
1411
                )
1412
            );
1413
        }
1414
1415 27
        return $runtimeType;
1416
    }
1417
1418
    /**
1419
     * If a resolve function is not given, then a default resolve behavior is used
1420
     * which takes the property of the source object of the same name as the field
1421
     * and returns it as the result, or if it's a function, returns the result
1422
     * of calling that function while passing along args and context.
1423
     *
1424
     * @param mixed        $source
1425
     * @param mixed[]      $args
1426
     * @param mixed[]|null $context
1427
     *
1428
     * @return mixed|null
1429
     */
1430 115
    public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info)
1431
    {
1432 115
        $fieldName = $info->fieldName;
1433 115
        $property  = null;
1434
1435 115
        if (is_array($source) || $source instanceof \ArrayAccess) {
1436 73
            if (isset($source[$fieldName])) {
1437 73
                $property = $source[$fieldName];
1438
            }
1439 42
        } elseif (is_object($source)) {
1440 41
            if (isset($source->{$fieldName})) {
1441 41
                $property = $source->{$fieldName};
1442
            }
1443
        }
1444
1445 115
        return $property instanceof \Closure ? $property($source, $args, $context, $info) : $property;
1446
    }
1447
}
1448