Passed
Pull Request — master (#199)
by Quang
02:42
created

Executor::completeValue()   C

Complexity

Conditions 11
Paths 10

Size

Total Lines 67
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 67
rs 5.8904
c 0
b 0
f 0
cc 11
eloc 29
nc 10
nop 5

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\ExecutionException;
6
use Digia\GraphQL\Error\GraphQLException;
7
use Digia\GraphQL\Error\InvalidTypeException;
8
use Digia\GraphQL\Error\UndefinedException;
9
use Digia\GraphQL\Language\Node\FieldNode;
10
use Digia\GraphQL\Language\Node\FragmentDefinitionNode;
11
use Digia\GraphQL\Language\Node\FragmentSpreadNode;
12
use Digia\GraphQL\Language\Node\InlineFragmentNode;
13
use Digia\GraphQL\Language\Node\NodeInterface;
14
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
15
use Digia\GraphQL\Language\Node\SelectionSetNode;
16
use Digia\GraphQL\Schema\Schema;
17
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
18
use Digia\GraphQL\Type\Definition\Field;
19
use Digia\GraphQL\Type\Definition\InterfaceType;
20
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
21
use Digia\GraphQL\Type\Definition\ListType;
22
use Digia\GraphQL\Type\Definition\NonNullType;
23
use Digia\GraphQL\Type\Definition\ObjectType;
24
use Digia\GraphQL\Type\Definition\TypeInterface;
25
use Digia\GraphQL\Type\Definition\UnionType;
26
use React\Promise\ExtendedPromiseInterface;
27
use React\Promise\PromiseInterface;
28
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
29
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
30
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
31
use function Digia\GraphQL\Util\toString;
32
use function Digia\GraphQL\Util\typeFromAST;
33
34
/**
35
 * Class AbstractStrategy
36
 * @package Digia\GraphQL\Execution\Strategies
37
 */
38
class Executor
39
{
40
    /**
41
     * @var ExecutionContext
42
     */
43
    protected $context;
44
45
    /**
46
     * @var OperationDefinitionNode
47
     */
48
    protected $operation;
49
50
    /**
51
     * @var mixed
52
     */
53
    protected $rootValue;
54
55
    /**
56
     * @var array
57
     */
58
    protected $finalResult;
59
60
    /**
61
     * @var array
62
     */
63
    protected static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
64
65
    /**
66
     * AbstractStrategy constructor.
67
     * @param ExecutionContext        $context
68
     *
69
     * @param OperationDefinitionNode $operation
70
     */
71
    public function __construct(
72
        ExecutionContext $context,
73
        OperationDefinitionNode $operation,
74
        $rootValue
75
    ) {
76
        $this->context   = $context;
77
        $this->operation = $operation;
78
        $this->rootValue = $rootValue;
79
    }
80
81
82
    /**
83
     * @return ?array
84
     * @throws \Throwable
85
     */
86
    function execute(): ?array
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
87
    {
88
        $operation = $this->context->getOperation();
89
        $schema    = $this->context->getSchema();
90
91
        $path = [];
92
93
        $objectType = $this->getOperationType($schema, $operation);
94
95
        $fields               = [];
96
        $visitedFragmentNames = [];
97
        try {
98
            $fields = $this->collectFields(
99
                $objectType,
100
                $this->operation->getSelectionSet(),
101
                $fields,
102
                $visitedFragmentNames
103
            );
104
105
            $result = ($operation->getOperation() === 'mutation')
106
                ? $this->executeFieldsSerially($objectType, $this->rootValue, $path, $fields)
107
                : $this->executeFields($objectType, $this->rootValue, $path, $fields);
108
109
        } catch (\Exception $ex) {
110
            $this->context->addError(
111
                new ExecutionException($ex->getMessage())
112
            );
113
114
            //@TODO return [null]
115
            return [$ex->getMessage()];
116
        }
117
118
        return $result;
119
    }
120
121
    /**
122
     * @param Schema                  $schema
123
     * @param OperationDefinitionNode $operation
124
     * @throws ExecutionException
125
     */
126
    public function getOperationType(Schema $schema, OperationDefinitionNode $operation)
127
    {
128
        switch ($operation->getOperation()) {
129
            case 'query':
130
                return $schema->getQueryType();
131
            case 'mutation':
132
                $mutationType = $schema->getMutationType();
133
                if (!$mutationType) {
134
                    throw new ExecutionException(
135
                        'Schema is not configured for mutations',
136
                        [$operation]
137
                    );
138
                }
139
                return $mutationType;
140
            case 'subscription':
141
                $subscriptionType = $schema->getSubscriptionType();
142
                if (!$subscriptionType) {
143
                    throw new ExecutionException(
144
                        'Schema is not configured for subscriptions',
145
                        [$operation]
146
                    );
147
                }
148
                return $subscriptionType;
149
            default:
150
                throw new ExecutionException(
151
                    'Can only execute queries, mutations and subscriptions',
152
                    [$operation]
153
                );
154
        }
155
    }
156
157
    /**
158
     * @param ObjectType       $runtimeType
159
     * @param SelectionSetNode $selectionSet
160
     * @param                  $fields
161
     * @param                  $visitedFragmentNames
162
     * @return mixed
163
     * @throws InvalidTypeException
164
     * @throws \Digia\GraphQL\Error\ExecutionException
165
     * @throws \Digia\GraphQL\Error\InvariantException
166
     */
167
    protected function collectFields(
168
        ObjectType $runtimeType,
169
        SelectionSetNode $selectionSet,
170
        &$fields,
171
        &$visitedFragmentNames
172
    ) {
173
        foreach ($selectionSet->getSelections() as $selection) {
174
            // Check if this Node should be included first
175
            if (!$this->shouldIncludeNode($selection)) {
176
                continue;
177
            }
178
            // Collect fields
179
            if ($selection instanceof FieldNode) {
180
                $fieldName = $this->getFieldNameKey($selection);
181
182
                if (!isset($fields[$fieldName])) {
183
                    $fields[$fieldName] = [];
184
                }
185
186
                $fields[$fieldName][] = $selection;
187
            } elseif ($selection instanceof InlineFragmentNode) {
188
                if (!$this->doesFragmentConditionMatch($selection, $runtimeType)) {
189
                    continue;
190
                }
191
192
                $this->collectFields($runtimeType, $selection->getSelectionSet(), $fields, $visitedFragmentNames);
193
            } elseif ($selection instanceof FragmentSpreadNode) {
194
                $fragmentName = $selection->getNameValue();
195
196
                if (!empty($visitedFragmentNames[$fragmentName])) {
197
                    continue;
198
                }
199
200
                $visitedFragmentNames[$fragmentName] = true;
201
                /** @var FragmentDefinitionNode $fragment */
202
                $fragment = $this->context->getFragments()[$fragmentName];
203
                $this->collectFields($runtimeType, $fragment->getSelectionSet(), $fields, $visitedFragmentNames);
204
            }
205
        }
206
207
        return $fields;
208
    }
209
210
211
    /**
212
     * @param $node
213
     * @return bool
214
     * @throws InvalidTypeException
215
     * @throws \Digia\GraphQL\Error\ExecutionException
216
     * @throws \Digia\GraphQL\Error\InvariantException
217
     */
218
    private function shouldIncludeNode(NodeInterface $node): bool
219
    {
220
221
        $contextVariables = $this->context->getVariableValues();
222
223
        $skip = coerceDirectiveValues(SkipDirective(), $node, $contextVariables);
224
225
        if ($skip && $skip['if'] === true) {
226
            return false;
227
        }
228
229
        $include = coerceDirectiveValues(IncludeDirective(), $node, $contextVariables);
230
231
        if ($include && $include['if'] === false) {
232
            return false;
233
        }
234
235
        return true;
236
    }
237
238
    /**
239
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
240
     * @param ObjectType                                $type
241
     * @return bool
242
     * @throws InvalidTypeException
243
     */
244
    private function doesFragmentConditionMatch(
245
        NodeInterface $fragment,
246
        ObjectType $type
247
    ): bool {
248
        $typeConditionNode = $fragment->getTypeCondition();
0 ignored issues
show
Bug introduced by
The method getTypeCondition() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of Digia\GraphQL\Language\Node\NodeInterface such as Digia\GraphQL\Language\Node\InlineFragmentNode or Digia\GraphQL\Language\Node\FragmentDefinitionNode. ( Ignorable by Annotation )

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

248
        /** @scrutinizer ignore-call */ 
249
        $typeConditionNode = $fragment->getTypeCondition();
Loading history...
249
250
        if (!$typeConditionNode) {
251
            return true;
252
        }
253
254
        $conditionalType = typeFromAST($this->context->getSchema(), $typeConditionNode);
255
256
        if ($conditionalType === $type) {
257
            return true;
258
        }
259
260
        if ($conditionalType instanceof AbstractTypeInterface) {
261
            return $this->context->getSchema()->isPossibleType($conditionalType, $type);
262
        }
263
264
        return false;
265
    }
266
267
    /**
268
     * @TODO: consider to move this to FieldNode
269
     * @param FieldNode $node
270
     * @return string
271
     */
272
    private function getFieldNameKey(FieldNode $node): string
273
    {
274
        return $node->getAlias() ? $node->getAlias()->getValue() : $node->getNameValue();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $node->getAlias()...: $node->getNameValue() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
275
    }
276
277
    /**
278
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
279
     * @param ObjectType $objectType
280
     * @param            $rootValue
281
     * @param            $path
282
     * @param            $fields
283
     * @return array
284
     * @throws InvalidTypeException
285
     * @throws \Digia\GraphQL\Error\ExecutionException
286
     * @throws \Digia\GraphQL\Error\InvariantException
287
     * @throws \Throwable
288
     */
289
    protected function executeFields(
290
        ObjectType $objectType,
291
        $rootValue,
292
        $path,
293
        $fields
294
    ): array {
295
        $finalResults      = [];
296
        $isContainsPromise = false;
297
298
        foreach ($fields as $fieldName => $fieldNodes) {
299
            $fieldPath   = $path;
300
            $fieldPath[] = $fieldName;
301
302
            try {
303
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->resolveField($obj...fieldNodes, $fieldPath) targeting Digia\GraphQL\Execution\Executor::resolveField() 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...
304
            } catch (UndefinedException $ex) {
305
                continue;
306
            }
307
308
            $isContainsPromise = $isContainsPromise || $this->isPromise($result);
309
310
            $finalResults[$fieldName] = $result;
311
        }
312
313
        if ($isContainsPromise) {
314
            $keys    = array_keys($finalResults);
315
            $promise = \React\Promise\all(array_values($finalResults));
316
            $promise->then(function ($values) use ($keys, &$finalResults) {
317
                foreach ($values as $i => $value) {
318
                    $finalResults[$keys[$i]] = $value;
319
                }
320
            });
321
        }
322
323
        return $finalResults;
324
    }
325
326
    /**
327
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
328
     *
329
     * @param ObjectType $objectType
330
     * @param            $rootValue
331
     * @param            $path
332
     * @param            $fields
333
     * @return array
334
     * @throws InvalidTypeException
335
     * @throws \Digia\GraphQL\Error\ExecutionException
336
     * @throws \Digia\GraphQL\Error\InvariantException
337
     * @throws \Throwable
338
     */
339
    public function executeFieldsSerially(
340
        ObjectType $objectType,
341
        $rootValue,
342
        $path,
343
        $fields
344
    ) {
345
346
        $finalResults = [];
347
348
        $promise = new \React\Promise\FulfilledPromise([]);
349
350
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
351
            $fieldPath   = $path;
352
            $fieldPath[] = $fieldName;
353
            try {
354
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->resolveField($obj...fieldNodes, $fieldPath) targeting Digia\GraphQL\Execution\Executor::resolveField() 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...
355
            } catch (UndefinedException $ex) {
356
                return null;
357
            }
358
359
            if ($this->isPromise($result)) {
360
                /** @var ExtendedPromiseInterface $result */
361
                return $result->then(function ($resolvedResult) use ($fieldName, $results) {
362
                    $results[$fieldName] = $resolvedResult;
363
                    return $results;
364
                });
365
            }
366
367
            $results[$fieldName] = $result;
368
369
            return $results;
370
        };
371
372
        foreach ($fields as $fieldName => $fieldNodes) {
373
            $promise = $promise->then(function ($resolvedResults) use (
374
                $resolve,
375
                $fieldName,
376
                $path,
377
                $objectType,
378
                $rootValue,
379
                $fieldNodes
380
            ) {
381
                return $resolve($resolvedResults, $fieldName, $path, $objectType, $rootValue, $fieldNodes);
382
            });
383
        }
384
385
        $promise->then(function ($resolvedResults) use (&$finalResults) {
386
            $finalResults = $resolvedResults ?? [];
387
        });
388
389
        return $finalResults;
390
    }
391
392
    /**
393
     * @param Schema     $schema
394
     * @param ObjectType $parentType
395
     * @param string     $fieldName
396
     * @return \Digia\GraphQL\Type\Definition\Field|null
397
     * @throws InvalidTypeException
398
     * @throws \Digia\GraphQL\Error\InvariantException
399
     */
400
    public function getFieldDefinition(
401
        Schema $schema,
402
        ObjectType $parentType,
403
        string $fieldName
404
    ) {
405
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
406
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
407
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
408
409
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
410
            return $schemaMetaFieldDefinition;
411
        }
412
413
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
414
            return $typeMetaFieldDefinition;
415
        }
416
417
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
418
            return $typeNameMetaFieldDefinition;
419
        }
420
421
        $fields = $parentType->getFields();
422
423
        return $fields[$fieldName] ?? null;
424
    }
425
426
427
    /**
428
     * @param ObjectType $parentType
429
     * @param            $rootValue
430
     * @param            $fieldNodes
431
     * @param            $path
432
     * @return array|null|\Throwable
433
     * @throws InvalidTypeException
434
     * @throws \Digia\GraphQL\Error\ExecutionException
435
     * @throws \Digia\GraphQL\Error\InvariantException
436
     * @throws \Throwable
437
     */
438
    protected function resolveField(
439
        ObjectType $parentType,
440
        $rootValue,
441
        $fieldNodes,
442
        $path
443
    ) {
444
        /** @var FieldNode $fieldNode */
445
        $fieldNode = $fieldNodes[0];
446
447
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
448
449
        if (null === $field) {
450
            throw new UndefinedException('Undefined field definition.');
451
        }
452
453
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
454
455
        $resolveFunction = $this->determineResolveFunction($field, $parentType, $this->context);
456
457
        $result = $this->resolveFieldValueOrError(
458
            $field,
459
            $fieldNode,
460
            $resolveFunction,
461
            $rootValue,
462
            $this->context,
463
            $info
464
        );
465
466
        $result = $this->completeValueCatchingError(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->completeValueCatc... $info, $path, $result) targeting Digia\GraphQL\Execution\...eteValueCatchingError() 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...
467
            $field->getType(),
468
            $fieldNodes,
469
            $info,
470
            $path,
471
            $result// $result is passed as $source
472
        );
473
474
        return $result;
475
    }
476
477
    /**
478
     * @param array            $fieldNodes
479
     * @param FieldNode        $fieldNode
480
     * @param Field            $field
481
     * @param ObjectType       $parentType
482
     * @param array|null       $path
483
     * @param ExecutionContext $context
484
     * @return ResolveInfo
485
     */
486
    private function buildResolveInfo(
487
        array $fieldNodes,
488
        FieldNode $fieldNode,
489
        Field $field,
490
        ObjectType $parentType,
491
        ?array $path,
492
        ExecutionContext $context
493
    ) {
494
        return new ResolveInfo(
495
            $fieldNode->getNameValue(),
496
            $fieldNodes,
497
            $field->getType(),
498
            $parentType,
499
            $path,
500
            $context->getSchema(),
501
            $context->getFragments(),
502
            $context->getRootValue(),
503
            $context->getOperation(),
504
            $context->getVariableValues()
505
        );
506
    }
507
508
    /**
509
     * @param Field            $field
510
     * @param ObjectType       $objectType
511
     * @param ExecutionContext $context
512
     * @return callable|mixed|null
513
     */
514
    private function determineResolveFunction(
515
        Field $field,
516
        ObjectType $objectType,
517
        ExecutionContext $context
518
    ) {
519
520
        if ($field->hasResolve()) {
521
            return $field->getResolve();
522
        }
523
524
        if ($objectType->hasResolve()) {
525
            return $objectType->getResolve();
526
        }
527
528
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
529
    }
530
531
    /**
532
     * @param TypeInterface $fieldType
533
     * @param               $fieldNodes
534
     * @param ResolveInfo   $info
535
     * @param               $path
536
     * @param               $result
537
     * @return null
538
     * @throws \Throwable
539
     */
540
    public function completeValueCatchingError(
541
        TypeInterface $fieldType,
542
        $fieldNodes,
543
        ResolveInfo $info,
544
        $path,
545
        &$result
546
    ) {
547
        if ($fieldType instanceof NonNullType) {
548
            return $this->completeValueWithLocatedError(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->completeVa... $info, $path, $result) also could return the type array which is incompatible with the documented return type null.
Loading history...
549
                $fieldType,
550
                $fieldNodes,
551
                $info,
552
                $path,
553
                $result
554
            );
555
        }
556
557
        try {
558
            $completed = $this->completeValueWithLocatedError(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $completed is correct as $this->completeValueWith... $info, $path, $result) targeting Digia\GraphQL\Execution\...ValueWithLocatedError() 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...
559
                $fieldType,
560
                $fieldNodes,
561
                $info,
562
                $path,
563
                $result
564
            );
565
566
            if ($this->isPromise($completed)) {
567
                $context = $this->context;
568
                /** @var ExtendedPromiseInterface $completed */
569
                return $completed->then(null, function ($error) use ($context, $fieldNodes, $path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression return $completed->then(...ion(...) { /* ... */ }) returns the type React\Promise\PromiseInterface which is incompatible with the documented return type null.
Loading history...
570
                    //@TODO Handle $error better
571
                    if ($error instanceof \Exception) {
572
                        $context->addError($this->buildLocatedError($error, $fieldNodes, $path));
573
                    } else {
574
                        $context->addError(
575
                            $this->buildLocatedError(
576
                                new ExecutionException($error ?? 'An unknown error occurred.'),
577
                                $fieldNodes,
578
                                $path
579
                            )
580
                        );
581
                    }
582
                    return new \React\Promise\FulfilledPromise(null);
583
                });
584
            }
585
586
            return $completed;
587
        } catch (ExecutionException $ex) {
588
            $this->context->addError($ex);
589
            return null;
590
        } catch (\Exception $ex) {
591
            $this->context->addError($this->buildLocatedError($ex, $fieldNodes, $path));
592
            return null;
593
        }
594
    }
595
596
    /**
597
     * @param TypeInterface $fieldType
598
     * @param               $fieldNodes
599
     * @param ResolveInfo   $info
600
     * @param               $path
601
     * @param               $result
602
     * @throws \Throwable
603
     */
604
    public function completeValueWithLocatedError(
605
        TypeInterface $fieldType,
606
        $fieldNodes,
607
        ResolveInfo $info,
608
        $path,
609
        $result
610
    ) {
611
        try {
612
            $completed = $this->completeValue(
613
                $fieldType,
614
                $fieldNodes,
615
                $info,
616
                $path,
617
                $result
618
            );
619
620
            return $completed;
621
        } catch (\Throwable $ex) {
622
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
623
        }
624
    }
625
626
    /**
627
     * @param TypeInterface $returnType
628
     * @param               $fieldNodes
629
     * @param ResolveInfo   $info
630
     * @param               $path
631
     * @param               $result
632
     * @return array|mixed
633
     * @throws ExecutionException
634
     * @throws \Throwable
635
     */
636
    private function completeValue(
637
        TypeInterface $returnType,
638
        $fieldNodes,
639
        ResolveInfo $info,
640
        $path,
641
        &$result
642
    ) {
643
        if ($this->isPromise($result)) {
644
            /** @var ExtendedPromiseInterface $result */
645
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
646
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
647
            });
648
        }
649
650
        if ($result instanceof \Throwable) {
651
            throw $result;
652
        }
653
654
        // If result is null-like, return null.
655
        if (null === $result) {
656
            return null;
657
        }
658
659
        if ($returnType instanceof NonNullType) {
660
            $completed = $this->completeValue(
661
                $returnType->getOfType(),
662
                $fieldNodes,
663
                $info,
664
                $path,
665
                $result
666
            );
667
668
            if ($completed === null) {
669
                throw new ExecutionException(
670
                    sprintf(
671
                        'Cannot return null for non-nullable field %s.%s.',
672
                        $info->getParentType(), $info->getFieldName()
673
                    )
674
                );
675
            }
676
677
            return $completed;
678
        }
679
680
        // If field type is List, complete each item in the list with the inner type
681
        if ($returnType instanceof ListType) {
682
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
683
        }
684
685
686
        // If field type is Scalar or Enum, serialize to a valid value, returning
687
        // null if serialization is not possible.
688
        if ($returnType instanceof LeafTypeInterface) {
689
            return $this->completeLeafValue($returnType, $result);
690
        }
691
692
        //@TODO Make a function for checking abstract type?
693
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
694
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
695
        }
696
697
        // Field type must be Object, Interface or Union and expect sub-selections.
698
        if ($returnType instanceof ObjectType) {
699
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
700
        }
701
702
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
703
    }
704
705
    /**
706
     * @param AbstractTypeInterface $returnType
707
     * @param                       $fieldNodes
708
     * @param ResolveInfo           $info
709
     * @param                       $path
710
     * @param                       $result
711
     * @return array|PromiseInterface
712
     * @throws ExecutionException
713
     * @throws InvalidTypeException
714
     * @throws \Digia\GraphQL\Error\InvariantException
715
     * @throws \Throwable
716
     */
717
    private function completeAbstractValue(
718
        AbstractTypeInterface $returnType,
719
        $fieldNodes,
720
        ResolveInfo $info,
721
        $path,
722
        &$result
723
    ) {
724
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
0 ignored issues
show
Bug introduced by
$info of type Digia\GraphQL\Execution\ResolveInfo is incompatible with the type array expected by parameter $args of Digia\GraphQL\Type\Defin...nterface::resolveType(). ( Ignorable by Annotation )

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

724
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
725
726
        if (null === $runtimeType) {
727
            //@TODO Show warning
728
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
729
        }
730
731
        if ($this->isPromise($runtimeType)) {
732
            /** @var ExtendedPromiseInterface $runtimeType */
733
            return $runtimeType->then(function ($resolvedRuntimeType) use (
734
                $returnType,
735
                $fieldNodes,
736
                $info,
737
                $path,
738
                &$result
739
            ) {
740
                return $this->completeObjectValue(
741
                    $this->ensureValidRuntimeType(
742
                        $resolvedRuntimeType,
743
                        $returnType,
744
                        $fieldNodes,
745
                        $info,
746
                        $result
747
                    ),
748
                    $fieldNodes,
749
                    $info,
750
                    $path,
751
                    $result
752
                );
753
            });
754
        }
755
756
        return $this->completeObjectValue(
757
            $this->ensureValidRuntimeType(
758
                $runtimeType,
759
                $returnType,
760
                $fieldNodes,
761
                $info,
762
                $result
763
            ),
764
            $fieldNodes,
765
            $info,
766
            $path,
767
            $result
768
        );
769
    }
770
771
    /**
772
     * @param                       $runtimeTypeOrName
773
     * @param AbstractTypeInterface $returnType
774
     * @param                       $fieldNodes
775
     * @param ResolveInfo           $info
776
     * @param                       $result
777
     * @return TypeInterface|ObjectType|null
778
     * @throws ExecutionException
779
     */
780
    private function ensureValidRuntimeType(
781
        $runtimeTypeOrName,
782
        AbstractTypeInterface $returnType,
783
        $fieldNodes,
784
        ResolveInfo $info,
785
        &$result
786
    ) {
787
        $runtimeType = is_string($runtimeTypeOrName)
788
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
789
            : $runtimeTypeOrName;
790
791
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on null. ( Ignorable by Annotation )

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

791
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->/** @scrutinizer ignore-call */ getName();

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

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

Loading history...
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

791
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->/** @scrutinizer ignore-call */ getName();
Loading history...
792
        $returnTypeName  = $returnType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. ( Ignorable by Annotation )

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

792
        /** @scrutinizer ignore-call */ 
793
        $returnTypeName  = $returnType->getName();
Loading history...
793
794
        if (!$runtimeType instanceof ObjectType) {
795
            $parentTypeName = $info->getParentType()->getName();
796
            $fieldName      = $info->getFieldName();
797
798
            throw new ExecutionException(
799
                "Abstract type {$returnTypeName} must resolve to an Object type at runtime " .
800
                "for field {$parentTypeName}.{$fieldName} with " .
801
                'value "' . $result . '", received "{$runtimeTypeName}".'
802
            );
803
        }
804
805
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
806
            throw new ExecutionException(
807
                "Runtime Object type \"{$runtimeTypeName}\" is not a possible type for \"{$returnTypeName}\"."
808
            );
809
        }
810
811
        if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
812
            throw new ExecutionException(
813
                "Schema must contain unique named types but contains multiple types named \"{$runtimeTypeName}\". " .
814
                "Make sure that `resolveType` function of abstract type \"{$returnTypeName}\" returns the same " .
815
                "type instance as referenced anywhere else within the schema."
816
            );
817
        }
818
819
        return $runtimeType;
820
    }
821
822
    /**
823
     * @param                       $value
824
     * @param                       $context
825
     * @param ResolveInfo           $info
826
     * @param AbstractTypeInterface $abstractType
827
     * @return TypeInterface|null
828
     * @throws ExecutionException
829
     */
830
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractTypeInterface $abstractType)
831
    {
832
        $possibleTypes           = $info->getSchema()->getPossibleTypes($abstractType);
833
        $promisedIsTypeOfResults = [];
834
835
        foreach ($possibleTypes as $index => $type) {
836
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
837
            if (null !== $isTypeOfResult) {
838
                if ($this->isPromise($isTypeOfResult)) {
839
                    $promisedIsTypeOfResults[$index] = $isTypeOfResult;
840
                    continue;
841
                }
842
843
                if ($isTypeOfResult === true) {
844
                    return $type;
845
                }
846
847
                if (\is_array($value)) {
848
                    //@TODO Make `type` configurable
849
                    if (isset($value['type']) && $value['type'] === $type->getName()) {
850
                        return $type;
851
                    }
852
                }
853
            }
854
        }
855
856
        if (!empty($promisedIsTypeOfResults)) {
857
            return \React\Promise\all($promisedIsTypeOfResults)
0 ignored issues
show
Bug Best Practice introduced by
The expression return all($promisedIsTy...ion(...) { /* ... */ }) also could return the type React\Promise\Promise which is incompatible with the documented return type null|Digia\GraphQL\Type\Definition\TypeInterface.
Loading history...
858
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
859
                    foreach ($isTypeOfResults as $index => $result) {
860
                        if ($result) {
861
                            return $possibleTypes[$index];
862
                        }
863
                    }
864
                    return null;
865
                });
866
        }
867
868
        return null;
869
    }
870
871
    /**
872
     * @param ListType    $returnType
873
     * @param             $fieldNodes
874
     * @param ResolveInfo $info
875
     * @param             $path
876
     * @param             $result
877
     * @return array|\React\Promise\Promise
878
     * @throws \Throwable
879
     */
880
    private function completeListValue(
881
        ListType $returnType,
882
        $fieldNodes,
883
        ResolveInfo $info,
884
        $path,
885
        &$result
886
    ) {
887
        $itemType = $returnType->getOfType();
888
889
        $completedItems  = [];
890
        $containsPromise = false;
891
892
        if (!is_array($result) && !($result instanceof \Traversable)) {
893
            throw new \Exception(
894
                sprintf(
895
                    'Expected Array or Traversable, but did not find one for field %s.%s.',
896
                    $info->getParentType()->getName(),
897
                    $info->getFieldName()
898
                )
899
            );
900
        }
901
902
        foreach ($result as $key => $item) {
903
            $fieldPath        = $path;
904
            $fieldPath[]      = $key;
905
            $completedItem    = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $completedItem is correct as $this->completeValueCatc...nfo, $fieldPath, $item) targeting Digia\GraphQL\Execution\...eteValueCatchingError() 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...
906
            $completedItems[] = $completedItem;
907
            $containsPromise  = $containsPromise || $this->isPromise($completedItem);
908
        }
909
910
        return $containsPromise ? \React\Promise\all($completedItems) : $completedItems;
911
    }
912
913
    /**
914
     * @param LeafTypeInterface $returnType
915
     * @param                   $result
916
     * @return mixed
917
     * @throws ExecutionException
918
     */
919
    private function completeLeafValue(LeafTypeInterface $returnType, &$result)
920
    {
921
        $serializedResult = $returnType->serialize($result);
0 ignored issues
show
Bug introduced by
The method serialize() does not exist on Digia\GraphQL\Type\Definition\LeafTypeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Digia\GraphQL\Type\Definition\LeafTypeInterface. ( Ignorable by Annotation )

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

921
        /** @scrutinizer ignore-call */ 
922
        $serializedResult = $returnType->serialize($result);
Loading history...
922
923
        if ($serializedResult === null) {
924
            //@TODO Make a function for this type of exception
925
            throw new ExecutionException(
926
                sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
927
            );
928
        }
929
930
        return $serializedResult;
931
    }
932
933
    /**
934
     * @param ObjectType  $returnType
935
     * @param             $fieldNodes
936
     * @param ResolveInfo $info
937
     * @param             $path
938
     * @param             $result
939
     * @return array
940
     * @throws ExecutionException
941
     * @throws InvalidTypeException
942
     * @throws \Digia\GraphQL\Error\InvariantException
943
     * @throws \Throwable
944
     */
945
    private function completeObjectValue(
946
        ObjectType $returnType,
947
        $fieldNodes,
948
        ResolveInfo $info,
949
        $path,
950
        &$result
951
    ) {
952
953
        if (null !== $returnType->getIsTypeOf()) {
954
            $isTypeOf = $returnType->isTypeOf($result, $this->context->getContextValue(), $info);
955
            //@TODO check for promise?
956
            if (!$isTypeOf) {
957
                throw new ExecutionException(
958
                    sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
959
                );
960
            }
961
        }
962
963
        return $this->collectAndExecuteSubFields(
964
            $returnType,
965
            $fieldNodes,
966
            $info,
967
            $path,
968
            $result
969
        );
970
    }
971
972
    /**
973
     * @param Field            $field
974
     * @param FieldNode        $fieldNode
975
     * @param callable         $resolveFunction
976
     * @param                  $rootValue
977
     * @param ExecutionContext $context
978
     * @param ResolveInfo      $info
979
     * @return array|\Throwable
980
     */
981
    private function resolveFieldValueOrError(
982
        Field $field,
983
        FieldNode $fieldNode,
984
        ?callable $resolveFunction,
985
        $rootValue,
986
        ExecutionContext $context,
987
        ResolveInfo $info
988
    ) {
989
        try {
990
            $args = coerceArgumentValues($field, $fieldNode, $context->getVariableValues());
991
992
            return $resolveFunction($rootValue, $args, $context->getContextValue(), $info);
993
        } catch (\Throwable $error) {
994
            return $error;
995
        }
996
    }
997
998
    /**
999
     * Try to resolve a field without any field resolver function.
1000
     *
1001
     * @param array|object $rootValue
1002
     * @param              $args
1003
     * @param              $context
1004
     * @param ResolveInfo  $info
1005
     * @return mixed|null
1006
     */
1007
    public static function defaultFieldResolver($rootValue, $args, $context, ResolveInfo $info)
1008
    {
1009
        $fieldName = $info->getFieldName();
1010
        $property  = null;
1011
1012
        if (is_array($rootValue) && isset($rootValue[$fieldName])) {
1013
            $property = $rootValue[$fieldName];
1014
        }
1015
1016
        if (is_object($rootValue)) {
1017
            $getter = 'get' . ucfirst($fieldName);
1018
            if (method_exists($rootValue, $getter)) {
1019
                $property = $rootValue->{$getter}();
1020
            } elseif (method_exists($rootValue, $fieldName)) {
1021
                $property = $rootValue->{$fieldName}($rootValue, $args, $context, $info);
1022
            } elseif (property_exists($rootValue, $fieldName)) {
1023
                $property = $rootValue->{$fieldName};
1024
            }
1025
        }
1026
1027
1028
        return $property instanceof \Closure ? $property($rootValue, $args, $context, $info) : $property;
1029
    }
1030
1031
    /**
1032
     * @param ObjectType  $returnType
1033
     * @param             $fieldNodes
1034
     * @param ResolveInfo $info
1035
     * @param             $path
1036
     * @param             $result
1037
     * @return array
1038
     * @throws InvalidTypeException
1039
     * @throws \Digia\GraphQL\Error\ExecutionException
1040
     * @throws \Digia\GraphQL\Error\InvariantException
1041
     * @throws \Throwable
1042
     */
1043
    private function collectAndExecuteSubFields(
1044
        ObjectType $returnType,
1045
        $fieldNodes,
1046
        ResolveInfo $info,
1047
        $path,
1048
        &$result
1049
    ) {
1050
        $subFields            = [];
1051
        $visitedFragmentNames = [];
1052
1053
        foreach ($fieldNodes as $fieldNode) {
1054
            /** @var FieldNode $fieldNode */
1055
            if ($fieldNode->getSelectionSet() !== null) {
1056
                $subFields = $this->collectFields(
1057
                    $returnType,
1058
                    $fieldNode->getSelectionSet(),
1059
                    $subFields,
1060
                    $visitedFragmentNames
1061
                );
1062
            }
1063
        }
1064
1065
        if (!empty($subFields)) {
1066
            return $this->executeFields($returnType, $result, $path, $subFields);
1067
        }
1068
1069
        return $result;
1070
    }
1071
1072
    /**
1073
     * @param $value
1074
     * @return bool
1075
     */
1076
    protected function isPromise($value): bool
1077
    {
1078
        return $value instanceof ExtendedPromiseInterface;
1079
    }
1080
1081
    /**
1082
     * @param \Throwable   $originalException
1083
     * @param array        $nodes
1084
     * @param string|array $path
1085
     * @return GraphQLException
1086
     */
1087
    protected function buildLocatedError(
1088
        \Throwable $originalException,
1089
        array $nodes = [],
1090
        array $path = []
1091
    ): ExecutionException {
1092
        return new ExecutionException(
1093
            $originalException->getMessage(),
1094
            $originalException instanceof GraphQLException ? $originalException->getNodes() : $nodes,
1095
            $originalException instanceof GraphQLException ? $originalException->getSource() : null,
1096
            $originalException instanceof GraphQLException ? $originalException->getPositions() : null,
1097
            $originalException instanceof GraphQLException ? ($originalException->getPath() ?? $path) : $path,
1098
            $originalException
1099
        );
1100
    }
1101
}
1102