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

Executor::collectFields()   D

Complexity

Conditions 9
Paths 9

Size

Total Lines 41
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

792
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->/** @scrutinizer ignore-call */ getName();
Loading history...
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

792
        $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...
793
        $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

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

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