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

Executor::completeObjectValue()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 41
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 5.0012

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 41
ccs 26
cts 27
cp 0.963
rs 9.1928
c 0
b 0
f 0
cc 5
nc 4
nop 5
crap 5.0012
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