Passed
Pull Request — master (#190)
by Sebastian
03:29
created

ExecutionStrategy::doesFragmentConditionMatch()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 2
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
 *
37
 * @package Digia\GraphQL\Execution\Strategies
38
 */
39
abstract class ExecutionStrategy
40
{
41
42
    /**
43
     * @var array
44
     */
45
    protected static $defaultFieldResolver = [
46
        __CLASS__,
47
        'defaultFieldResolver',
48
    ];
49
50
    /**
51
     * @var ExecutionContext
52
     */
53
    protected $context;
54
55
    /**
56
     * @var OperationDefinitionNode
57
     */
58
    protected $operation;
59
60
    /**
61
     * @var mixed
62
     */
63
    protected $rootValue;
64
65
    /**
66
     * @var array
67
     */
68
    protected $finalResult;
69
70
    /**
71
     * AbstractStrategy constructor.
72
     *
73
     * @param ExecutionContext $context
74
     *
75
     * @param OperationDefinitionNode $operation
76
     */
77
    public function __construct(
78
        ExecutionContext $context,
79
        OperationDefinitionNode $operation,
80
        $rootValue
81
    ) {
82
        $this->context = $context;
83
        $this->operation = $operation;
84
        $this->rootValue = $rootValue;
85
    }
86
87
    /**
88
     * Try to resolve a field without any field resolver function.
89
     *
90
     * @param array|object $rootValue
91
     * @param              $args
92
     * @param              $context
93
     * @param ResolveInfo $info
94
     *
95
     * @return mixed|null
96
     */
97
    public static function defaultFieldResolver(
98
        $rootValue,
99
        $args,
100
        $context,
101
        ResolveInfo $info
102
    ) {
103
        $fieldName = $info->getFieldName();
104
        $property = null;
105
106
        if (is_array($rootValue) && isset($rootValue[$fieldName])) {
107
            $property = $rootValue[$fieldName];
108
        }
109
110
        if (is_object($rootValue)) {
111
            $getter = 'get'.ucfirst($fieldName);
112
            if (method_exists($rootValue, $getter)) {
113
                $property = $rootValue->{$getter}();
114
            } elseif (property_exists($rootValue, $fieldName)) {
115
                $property = $rootValue->{$fieldName};
116
            }
117
        }
118
119
120
        return $property instanceof \Closure ? $property($rootValue, $args,
121
            $context, $info) : $property;
122
    }
123
124
    /**
125
     * @return array|null
126
     */
127
    abstract 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...
128
129
    /**
130
     * Implements the "Evaluating selection sets" section of the spec for
131
     * "write" mode.
132
     *
133
     * @param ObjectType $objectType
134
     * @param            $rootValue
135
     * @param            $path
136
     * @param            $fields
137
     *
138
     * @return array
139
     * @throws InvalidTypeException
140
     * @throws \Digia\GraphQL\Error\ExecutionException
141
     * @throws \Digia\GraphQL\Error\InvariantException
142
     * @throws \Throwable
143
     */
144
    public function executeFieldsSerially(
145
        ObjectType $objectType,
146
        $rootValue,
147
        $path,
148
        $fields
149
    ) {
150
151
        $finalResults = [];
152
153
        $promise = new \React\Promise\FulfilledPromise([]);
154
155
        $resolve = function (
156
            $results,
157
            $fieldName,
158
            $path,
159
            $objectType,
160
            $rootValue,
161
            $fieldNodes
162
        ) {
163
            $fieldPath = $path;
164
            $fieldPath[] = $fieldName;
165
            try {
166
                $result = $this->resolveField($objectType, $rootValue,
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\...trategy::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...
167
                    $fieldNodes, $fieldPath);
168
            } catch (UndefinedException $ex) {
169
                return null;
170
            }
171
172
            if ($this->isPromise($result)) {
173
                /** @var ExtendedPromiseInterface $result */
174
                return $result->then(function ($resolvedResult) use (
175
                    $fieldName,
176
                    $results
177
                ) {
178
                    $results[$fieldName] = $resolvedResult;
179
180
                    return $results;
181
                });
182
            }
183
184
            $results[$fieldName] = $result;
185
186
            return $results;
187
        };
188
189
        foreach ($fields as $fieldName => $fieldNodes) {
190
            $promise = $promise->then(function ($resolvedResults) use (
191
                $resolve,
192
                $fieldName,
193
                $path,
194
                $objectType,
195
                $rootValue,
196
                $fieldNodes
197
            ) {
198
                return $resolve($resolvedResults, $fieldName, $path,
199
                    $objectType, $rootValue, $fieldNodes);
200
            });
201
        }
202
203
        $promise->then(function ($resolvedResults) use (&$finalResults) {
204
            $finalResults = $resolvedResults ?? [];
205
        });
206
207
        return $finalResults;
208
    }
209
210
    /**
211
     * @param ObjectType $parentType
212
     * @param            $rootValue
213
     * @param            $fieldNodes
214
     * @param            $path
215
     *
216
     * @return array|null|\Throwable
217
     * @throws InvalidTypeException
218
     * @throws \Digia\GraphQL\Error\ExecutionException
219
     * @throws \Digia\GraphQL\Error\InvariantException
220
     * @throws \Throwable
221
     */
222
    protected function resolveField(
223
        ObjectType $parentType,
224
        $rootValue,
225
        $fieldNodes,
226
        $path
227
    ) {
228
        /** @var FieldNode $fieldNode */
229
        $fieldNode = $fieldNodes[0];
230
231
        $field = $this->getFieldDefinition($this->context->getSchema(),
232
            $parentType, $fieldNode->getNameValue());
233
234
        if (null === $field) {
235
            throw new UndefinedException('Undefined field definition.');
236
        }
237
238
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field,
239
            $parentType, $path, $this->context);
240
241
        $resolveFunction = $this->determineResolveFunction($field, $parentType,
242
            $this->context);
243
244
        $result = $this->resolveFieldValueOrError(
245
            $field,
246
            $fieldNode,
247
            $resolveFunction,
248
            $rootValue,
249
            $this->context,
250
            $info
251
        );
252
253
        $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...
254
            $field->getType(),
255
            $fieldNodes,
256
            $info,
257
            $path,
258
            $result// $result is passed as $source
259
        );
260
261
        return $result;
262
    }
263
264
    /**
265
     * @param Schema $schema
266
     * @param ObjectType $parentType
267
     * @param string $fieldName
268
     *
269
     * @return \Digia\GraphQL\Type\Definition\Field|null
270
     * @throws InvalidTypeException
271
     * @throws \Digia\GraphQL\Error\InvariantException
272
     */
273
    public function getFieldDefinition(
274
        Schema $schema,
275
        ObjectType $parentType,
276
        string $fieldName
277
    ) {
278
        $schemaMetaFieldDefinition = SchemaMetaFieldDefinition();
279
        $typeMetaFieldDefinition = TypeMetaFieldDefinition();
280
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
281
282
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
283
            return $schemaMetaFieldDefinition;
284
        }
285
286
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
287
            return $typeMetaFieldDefinition;
288
        }
289
290
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
291
            return $typeNameMetaFieldDefinition;
292
        }
293
294
        $fields = $parentType->getFields();
295
296
        return $fields[$fieldName] ?? null;
297
    }
298
299
    /**
300
     * @param array $fieldNodes
301
     * @param FieldNode $fieldNode
302
     * @param Field $field
303
     * @param ObjectType $parentType
304
     * @param array|null $path
305
     * @param ExecutionContext $context
306
     *
307
     * @return ResolveInfo
308
     */
309
    private function buildResolveInfo(
310
        array $fieldNodes,
311
        FieldNode $fieldNode,
312
        Field $field,
313
        ObjectType $parentType,
314
        ?array $path,
315
        ExecutionContext $context
316
    ) {
317
        return new ResolveInfo(
318
            $fieldNode->getNameValue(),
319
            $fieldNodes,
320
            $field->getType(),
321
            $parentType,
322
            $path,
323
            $context->getSchema(),
324
            $context->getFragments(),
325
            $context->getRootValue(),
326
            $context->getOperation(),
327
            $context->getVariableValues()
328
        );
329
    }
330
331
    /**
332
     * @param Field $field
333
     * @param ObjectType $objectType
334
     * @param ExecutionContext $context
335
     *
336
     * @return callable|mixed|null
337
     */
338
    private function determineResolveFunction(
339
        Field $field,
340
        ObjectType $objectType,
341
        ExecutionContext $context
342
    ) {
343
344
        if ($field->hasResolve()) {
345
            return $field->getResolve();
346
        }
347
348
        if ($objectType->hasResolve()) {
349
            return $objectType->getResolve();
350
        }
351
352
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
353
    }
354
355
    /**
356
     * @param Field $field
357
     * @param FieldNode $fieldNode
358
     * @param callable $resolveFunction
359
     * @param                  $rootValue
360
     * @param ExecutionContext $context
361
     * @param ResolveInfo $info
362
     *
363
     * @return array|\Throwable
364
     */
365
    private function resolveFieldValueOrError(
366
        Field $field,
367
        FieldNode $fieldNode,
368
        ?callable $resolveFunction,
369
        $rootValue,
370
        ExecutionContext $context,
371
        ResolveInfo $info
372
    ) {
373
        try {
374
            $args = coerceArgumentValues($field, $fieldNode,
375
                $context->getVariableValues());
376
377
            return $resolveFunction($rootValue, $args,
378
                $context->getContextValue(), $info);
379
        } catch (\Throwable $error) {
380
            return $error;
381
        }
382
    }
383
384
    /**
385
     * @param TypeInterface $fieldType
386
     * @param               $fieldNodes
387
     * @param ResolveInfo $info
388
     * @param               $path
389
     * @param               $result
390
     *
391
     * @return null
392
     * @throws \Throwable
393
     */
394
    public function completeValueCatchingError(
395
        TypeInterface $fieldType,
396
        $fieldNodes,
397
        ResolveInfo $info,
398
        $path,
399
        &$result
400
    ) {
401
        if ($fieldType instanceof NonNullType) {
402
            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...
403
                $fieldType,
404
                $fieldNodes,
405
                $info,
406
                $path,
407
                $result
408
            );
409
        }
410
411
        try {
412
            $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...
413
                $fieldType,
414
                $fieldNodes,
415
                $info,
416
                $path,
417
                $result
418
            );
419
420
            if ($this->isPromise($completed)) {
421
                $context = $this->context;
422
423
                /** @var ExtendedPromiseInterface $completed */
424
                return $completed->then(null,
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...
425
                    function ($error) use ($context, $fieldNodes, $path) {
426
                        //@TODO Handle $error better
427
                        if ($error instanceof \Exception) {
428
                            $context->addError($this->buildLocatedError($error,
429
                                $fieldNodes, $path));
430
                        } else {
431
                            $context->addError(
432
                                $this->buildLocatedError(
433
                                    new ExecutionException($error ?? 'An unknown error occurred.'),
434
                                    $fieldNodes,
435
                                    $path
436
                                )
437
                            );
438
                        }
439
440
                        return new \React\Promise\FulfilledPromise(null);
441
                    });
442
            }
443
444
            return $completed;
445
        } catch (ExecutionException $ex) {
446
            $this->context->addError($ex);
447
448
            return null;
449
        } catch (\Exception $ex) {
450
            $this->context->addError($this->buildLocatedError($ex, $fieldNodes,
451
                $path));
452
453
            return null;
454
        }
455
    }
456
457
    /**
458
     * @param TypeInterface $fieldType
459
     * @param               $fieldNodes
460
     * @param ResolveInfo $info
461
     * @param               $path
462
     * @param               $result
463
     *
464
     * @throws \Throwable
465
     */
466
    public function completeValueWithLocatedError(
467
        TypeInterface $fieldType,
468
        $fieldNodes,
469
        ResolveInfo $info,
470
        $path,
471
        $result
472
    ) {
473
        try {
474
            $completed = $this->completeValue(
475
                $fieldType,
476
                $fieldNodes,
477
                $info,
478
                $path,
479
                $result
480
            );
481
482
            return $completed;
483
        } catch (\Throwable $ex) {
484
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
485
        }
486
    }
487
488
    /**
489
     * @param TypeInterface $returnType
490
     * @param               $fieldNodes
491
     * @param ResolveInfo $info
492
     * @param               $path
493
     * @param               $result
494
     *
495
     * @return array|mixed
496
     * @throws ExecutionException
497
     * @throws \Throwable
498
     */
499
    private function completeValue(
500
        TypeInterface $returnType,
501
        $fieldNodes,
502
        ResolveInfo $info,
503
        $path,
504
        &$result
505
    ) {
506
        if ($this->isPromise($result)) {
507
            /** @var ExtendedPromiseInterface $result */
508
            return $result->then(function (&$value) use (
509
                $returnType,
510
                $fieldNodes,
511
                $info,
512
                $path
513
            ) {
514
                return $this->completeValue($returnType, $fieldNodes, $info,
515
                    $path, $value);
516
            });
517
        }
518
519
        if ($result instanceof \Throwable) {
520
            throw $result;
521
        }
522
523
        // If result is null-like, return null.
524
        if (null === $result) {
525
            return null;
526
        }
527
528
        if ($returnType instanceof NonNullType) {
529
            $completed = $this->completeValue(
530
                $returnType->getOfType(),
531
                $fieldNodes,
532
                $info,
533
                $path,
534
                $result
535
            );
536
537
            if ($completed === null) {
538
                throw new ExecutionException(
539
                    sprintf(
540
                        'Cannot return null for non-nullable field %s.%s.',
541
                        $info->getParentType(), $info->getFieldName()
542
                    )
543
                );
544
            }
545
546
            return $completed;
547
        }
548
549
        // If field type is List, complete each item in the list with the inner type
550
        if ($returnType instanceof ListType) {
551
            return $this->completeListValue($returnType, $fieldNodes, $info,
552
                $path, $result);
553
        }
554
555
556
        // If field type is Scalar or Enum, serialize to a valid value, returning
557
        // null if serialization is not possible.
558
        if ($returnType instanceof LeafTypeInterface) {
559
            return $this->completeLeafValue($returnType, $result);
560
        }
561
562
        //@TODO Make a function for checking abstract type?
563
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
564
            return $this->completeAbstractValue($returnType, $fieldNodes, $info,
565
                $path, $result);
566
        }
567
568
        // Field type must be Object, Interface or Union and expect sub-selections.
569
        if ($returnType instanceof ObjectType) {
570
            return $this->completeObjectValue($returnType, $fieldNodes, $info,
571
                $path, $result);
572
        }
573
574
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
575
    }
576
577
    /**
578
     * @param $value
579
     *
580
     * @return bool
581
     */
582
    protected function isPromise($value): bool
583
    {
584
        return $value instanceof ExtendedPromiseInterface;
585
    }
586
587
    /**
588
     * @param ListType $returnType
589
     * @param             $fieldNodes
590
     * @param ResolveInfo $info
591
     * @param             $path
592
     * @param             $result
593
     *
594
     * @return array|\React\Promise\Promise
595
     * @throws \Throwable
596
     */
597
    private function completeListValue(
598
        ListType $returnType,
599
        $fieldNodes,
600
        ResolveInfo $info,
601
        $path,
602
        &$result
603
    ) {
604
        $itemType = $returnType->getOfType();
605
606
        $completedItems = [];
607
        $containsPromise = false;
608
609
        if (!is_array($result) && !($result instanceof \Traversable)) {
610
            throw new \Exception(
611
                sprintf(
612
                    'Expected Array or Traversable, but did not find one for field %s.%s.',
613
                    $info->getParentType()->getName(),
614
                    $info->getFieldName()
615
                )
616
            );
617
        }
618
619
        foreach ($result as $key => $item) {
620
            $fieldPath = $path;
621
            $fieldPath[] = $key;
622
            $completedItem = $this->completeValueCatchingError($itemType,
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...
623
                $fieldNodes, $info, $fieldPath, $item);
624
            $completedItems[] = $completedItem;
625
            $containsPromise = $containsPromise || $this->isPromise($completedItem);
626
        }
627
628
        return $containsPromise ? \React\Promise\all($completedItems) : $completedItems;
629
    }
630
631
    /**
632
     * @param LeafTypeInterface $returnType
633
     * @param                   $result
634
     *
635
     * @return mixed
636
     * @throws ExecutionException
637
     */
638
    private function completeLeafValue(LeafTypeInterface $returnType, &$result)
639
    {
640
        $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

640
        /** @scrutinizer ignore-call */ 
641
        $serializedResult = $returnType->serialize($result);
Loading history...
641
642
        if ($serializedResult === null) {
643
            //@TODO Make a function for this type of exception
644
            throw new ExecutionException(
645
                sprintf('Expected value of type "%s" but received: %s.',
646
                    (string)$returnType, toString($result))
647
            );
648
        }
649
650
        return $serializedResult;
651
    }
652
653
    /**
654
     * @param AbstractTypeInterface $returnType
655
     * @param                       $fieldNodes
656
     * @param ResolveInfo $info
657
     * @param                       $path
658
     * @param                       $result
659
     *
660
     * @return array|PromiseInterface
661
     * @throws ExecutionException
662
     * @throws InvalidTypeException
663
     * @throws \Digia\GraphQL\Error\InvariantException
664
     * @throws \Throwable
665
     */
666
    private function completeAbstractValue(
667
        AbstractTypeInterface $returnType,
668
        $fieldNodes,
669
        ResolveInfo $info,
670
        $path,
671
        &$result
672
    ) {
673
        $runtimeType = $returnType->resolveType($result,
674
            $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

674
            $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
675
676
        if (null === $runtimeType) {
677
            //@TODO Show warning
678
            $runtimeType = $this->defaultTypeResolver($result,
679
                $this->context->getContextValue(), $info, $returnType);
680
        }
681
682
        if ($this->isPromise($runtimeType)) {
683
            /** @var ExtendedPromiseInterface $runtimeType */
684
            return $runtimeType->then(function ($resolvedRuntimeType) use (
685
                $returnType,
686
                $fieldNodes,
687
                $info,
688
                $path,
689
                &$result
690
            ) {
691
                return $this->completeObjectValue(
692
                    $this->ensureValidRuntimeType(
693
                        $resolvedRuntimeType,
694
                        $returnType,
695
                        $fieldNodes,
696
                        $info,
697
                        $result
698
                    ),
699
                    $fieldNodes,
700
                    $info,
701
                    $path,
702
                    $result
703
                );
704
            });
705
        }
706
707
        return $this->completeObjectValue(
708
            $this->ensureValidRuntimeType(
709
                $runtimeType,
710
                $returnType,
711
                $fieldNodes,
712
                $info,
713
                $result
714
            ),
715
            $fieldNodes,
716
            $info,
717
            $path,
718
            $result
719
        );
720
    }
721
722
    /**
723
     * @param                       $value
724
     * @param                       $context
725
     * @param ResolveInfo $info
726
     * @param AbstractTypeInterface $abstractType
727
     *
728
     * @return TypeInterface|null
729
     * @throws ExecutionException
730
     */
731
    private function defaultTypeResolver(
732
        $value,
733
        $context,
734
        ResolveInfo $info,
735
        AbstractTypeInterface $abstractType
736
    ) {
737
        $possibleTypes = $info->getSchema()->getPossibleTypes($abstractType);
738
        $promisedIsTypeOfResults = [];
739
740
        foreach ($possibleTypes as $index => $type) {
741
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
742
            if (null !== $isTypeOfResult) {
743
                if ($this->isPromise($isTypeOfResult)) {
744
                    $promisedIsTypeOfResults[$index] = $isTypeOfResult;
745
                    continue;
746
                }
747
748
                if ($isTypeOfResult === true) {
749
                    return $type;
750
                }
751
752
                if (\is_array($value)) {
753
                    //@TODO Make `type` configurable
754
                    if (isset($value['type']) && $value['type'] === $type->getName()) {
755
                        return $type;
756
                    }
757
                }
758
            }
759
        }
760
761
        if (!empty($promisedIsTypeOfResults)) {
762
            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...
763
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
764
                    foreach ($isTypeOfResults as $index => $result) {
765
                        if ($result) {
766
                            return $possibleTypes[$index];
767
                        }
768
                    }
769
770
                    return null;
771
                });
772
        }
773
774
        return null;
775
    }
776
777
    /**
778
     * @param ObjectType $returnType
779
     * @param             $fieldNodes
780
     * @param ResolveInfo $info
781
     * @param             $path
782
     * @param             $result
783
     *
784
     * @return array
785
     * @throws ExecutionException
786
     * @throws InvalidTypeException
787
     * @throws \Digia\GraphQL\Error\InvariantException
788
     * @throws \Throwable
789
     */
790
    private function completeObjectValue(
791
        ObjectType $returnType,
792
        $fieldNodes,
793
        ResolveInfo $info,
794
        $path,
795
        &$result
796
    ) {
797
798
        if (null !== $returnType->getIsTypeOf()) {
799
            $isTypeOf = $returnType->isTypeOf($result,
800
                $this->context->getContextValue(), $info);
801
            //@TODO check for promise?
802
            if (!$isTypeOf) {
803
                throw new ExecutionException(
804
                    sprintf('Expected value of type "%s" but received: %s.',
805
                        (string)$returnType, toString($result))
806
                );
807
            }
808
        }
809
810
        return $this->collectAndExecuteSubFields(
811
            $returnType,
812
            $fieldNodes,
813
            $info,
814
            $path,
815
            $result
816
        );
817
    }
818
819
    /**
820
     * @param ObjectType $returnType
821
     * @param             $fieldNodes
822
     * @param ResolveInfo $info
823
     * @param             $path
824
     * @param             $result
825
     *
826
     * @return array
827
     * @throws InvalidTypeException
828
     * @throws \Digia\GraphQL\Error\ExecutionException
829
     * @throws \Digia\GraphQL\Error\InvariantException
830
     * @throws \Throwable
831
     */
832
    private function collectAndExecuteSubFields(
833
        ObjectType $returnType,
834
        $fieldNodes,
835
        ResolveInfo $info,
836
        $path,
837
        &$result
838
    ) {
839
        $subFields = [];
840
        $visitedFragmentNames = [];
841
842
        foreach ($fieldNodes as $fieldNode) {
843
            /** @var FieldNode $fieldNode */
844
            if ($fieldNode->getSelectionSet() !== null) {
845
                $subFields = $this->collectFields(
846
                    $returnType,
847
                    $fieldNode->getSelectionSet(),
848
                    $subFields,
849
                    $visitedFragmentNames
850
                );
851
            }
852
        }
853
854
        if (!empty($subFields)) {
855
            return $this->executeFields($returnType, $result, $path,
856
                $subFields);
857
        }
858
859
        return $result;
860
    }
861
862
    /**
863
     * @param ObjectType $runtimeType
864
     * @param SelectionSetNode $selectionSet
865
     * @param                  $fields
866
     * @param                  $visitedFragmentNames
867
     *
868
     * @return mixed
869
     * @throws InvalidTypeException
870
     * @throws \Digia\GraphQL\Error\ExecutionException
871
     * @throws \Digia\GraphQL\Error\InvariantException
872
     */
873
    protected function collectFields(
874
        ObjectType $runtimeType,
875
        SelectionSetNode $selectionSet,
876
        &$fields,
877
        &$visitedFragmentNames
878
    ) {
879
        foreach ($selectionSet->getSelections() as $selection) {
880
            // Check if this Node should be included first
881
            if (!$this->shouldIncludeNode($selection)) {
882
                continue;
883
            }
884
            // Collect fields
885
            if ($selection instanceof FieldNode) {
886
                $fieldName = $this->getFieldNameKey($selection);
887
888
                if (!isset($fields[$fieldName])) {
889
                    $fields[$fieldName] = [];
890
                }
891
892
                $fields[$fieldName][] = $selection;
893
            } elseif ($selection instanceof InlineFragmentNode) {
894
                if (!$this->doesFragmentConditionMatch($selection,
895
                    $runtimeType)) {
896
                    continue;
897
                }
898
899
                $this->collectFields($runtimeType,
900
                    $selection->getSelectionSet(), $fields,
901
                    $visitedFragmentNames);
902
            } elseif ($selection instanceof FragmentSpreadNode) {
903
                $fragmentName = $selection->getNameValue();
904
905
                if (!empty($visitedFragmentNames[$fragmentName])) {
906
                    continue;
907
                }
908
909
                $visitedFragmentNames[$fragmentName] = true;
910
                /** @var FragmentDefinitionNode $fragment */
911
                $fragment = $this->context->getFragments()[$fragmentName];
912
                $this->collectFields($runtimeType, $fragment->getSelectionSet(),
913
                    $fields, $visitedFragmentNames);
914
            }
915
        }
916
917
        return $fields;
918
    }
919
920
    /**
921
     * @param $node
922
     *
923
     * @return bool
924
     * @throws InvalidTypeException
925
     * @throws \Digia\GraphQL\Error\ExecutionException
926
     * @throws \Digia\GraphQL\Error\InvariantException
927
     */
928
    private function shouldIncludeNode(NodeInterface $node): bool
929
    {
930
931
        $contextVariables = $this->context->getVariableValues();
932
933
        $skip = coerceDirectiveValues(SkipDirective(), $node,
934
            $contextVariables);
935
936
        if ($skip && $skip['if'] === true) {
937
            return false;
938
        }
939
940
        $include = coerceDirectiveValues(IncludeDirective(), $node,
941
            $contextVariables);
942
943
        if ($include && $include['if'] === false) {
944
            return false;
945
        }
946
947
        return true;
948
    }
949
950
    /**
951
     * @TODO: consider to move this to FieldNode
952
     *
953
     * @param FieldNode $node
954
     *
955
     * @return string
956
     */
957
    private function getFieldNameKey(FieldNode $node): string
958
    {
959
        return $node->getAlias() ? $node->getAlias()
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...
960
            ->getValue() : $node->getNameValue();
961
    }
962
963
    /**
964
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
965
     * @param ObjectType $type
966
     *
967
     * @return bool
968
     * @throws InvalidTypeException
969
     */
970
    private function doesFragmentConditionMatch(
971
        NodeInterface $fragment,
972
        ObjectType $type
973
    ): bool {
974
        $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

974
        /** @scrutinizer ignore-call */ 
975
        $typeConditionNode = $fragment->getTypeCondition();
Loading history...
975
976
        if (!$typeConditionNode) {
977
            return true;
978
        }
979
980
        $conditionalType = typeFromAST($this->context->getSchema(),
981
            $typeConditionNode);
982
983
        if ($conditionalType === $type) {
984
            return true;
985
        }
986
987
        if ($conditionalType instanceof AbstractTypeInterface) {
988
            return $this->context->getSchema()
989
                ->isPossibleType($conditionalType, $type);
990
        }
991
992
        return false;
993
    }
994
995
    /**
996
     * Implements the "Evaluating selection sets" section of the spec for
997
     * "read" mode.
998
     *
999
     * @param ObjectType $objectType
1000
     * @param            $rootValue
1001
     * @param            $path
1002
     * @param            $fields
1003
     *
1004
     * @return array
1005
     * @throws InvalidTypeException
1006
     * @throws \Digia\GraphQL\Error\ExecutionException
1007
     * @throws \Digia\GraphQL\Error\InvariantException
1008
     * @throws \Throwable
1009
     */
1010
    protected function executeFields(
1011
        ObjectType $objectType,
1012
        $rootValue,
1013
        $path,
1014
        $fields
1015
    ): array {
1016
        $finalResults = [];
1017
        $isContainsPromise = false;
1018
1019
        foreach ($fields as $fieldName => $fieldNodes) {
1020
            $fieldPath = $path;
1021
            $fieldPath[] = $fieldName;
1022
1023
            try {
1024
                $result = $this->resolveField($objectType, $rootValue,
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\...trategy::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...
1025
                    $fieldNodes, $fieldPath);
1026
            } catch (UndefinedException $ex) {
1027
                continue;
1028
            }
1029
1030
            $isContainsPromise = $isContainsPromise || $this->isPromise($result);
1031
1032
            $finalResults[$fieldName] = $result;
1033
        }
1034
1035
        if ($isContainsPromise) {
1036
            $keys = array_keys($finalResults);
1037
            $promise = \React\Promise\all(array_values($finalResults));
1038
            $promise->then(function ($values) use ($keys, &$finalResults) {
1039
                foreach ($values as $i => $value) {
1040
                    $finalResults[$keys[$i]] = $value;
1041
                }
1042
            });
1043
        }
1044
1045
        return $finalResults;
1046
    }
1047
1048
    /**
1049
     * @param                       $runtimeTypeOrName
1050
     * @param AbstractTypeInterface $returnType
1051
     * @param                       $fieldNodes
1052
     * @param ResolveInfo $info
1053
     * @param                       $result
1054
     *
1055
     * @return TypeInterface|ObjectType|null
1056
     * @throws ExecutionException
1057
     */
1058
    private function ensureValidRuntimeType(
1059
        $runtimeTypeOrName,
1060
        AbstractTypeInterface $returnType,
1061
        $fieldNodes,
1062
        ResolveInfo $info,
1063
        &$result
1064
    ) {
1065
        $runtimeType = is_string($runtimeTypeOrName)
1066
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
1067
            : $runtimeTypeOrName;
1068
1069
        $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

1069
        $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

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

1070
        /** @scrutinizer ignore-call */ 
1071
        $returnTypeName = $returnType->getName();
Loading history...
1071
1072
        if (!$runtimeType instanceof ObjectType) {
1073
            $parentTypeName = $info->getParentType()->getName();
1074
            $fieldName = $info->getFieldName();
1075
1076
            throw new ExecutionException(
1077
                "Abstract type {$returnTypeName} must resolve to an Object type at runtime ".
1078
                "for field {$parentTypeName}.{$fieldName} with ".
1079
                'value "'.$result.'", received "{$runtimeTypeName}".'
1080
            );
1081
        }
1082
1083
        if (!$this->context->getSchema()
1084
            ->isPossibleType($returnType, $runtimeType)) {
1085
            throw new ExecutionException(
1086
                "Runtime Object type \"{$runtimeTypeName}\" is not a possible type for \"{$returnTypeName}\"."
1087
            );
1088
        }
1089
1090
        if ($runtimeType !== $this->context->getSchema()
1091
                ->getType($runtimeType->getName())) {
1092
            throw new ExecutionException(
1093
                "Schema must contain unique named types but contains multiple types named \"{$runtimeTypeName}\". ".
1094
                "Make sure that `resolveType` function of abstract type \"{$returnTypeName}\" returns the same ".
1095
                "type instance as referenced anywhere else within the schema."
1096
            );
1097
        }
1098
1099
        return $runtimeType;
1100
    }
1101
1102
    /**
1103
     * @param \Throwable $originalException
1104
     * @param array $nodes
1105
     * @param string|array $path
1106
     *
1107
     * @return GraphQLException
1108
     */
1109
    protected function buildLocatedError(
1110
        \Throwable $originalException,
1111
        array $nodes = [],
1112
        array $path = []
1113
    ): ExecutionException {
1114
        return new ExecutionException(
1115
            $originalException->getMessage(),
1116
            $originalException instanceof GraphQLException ? $originalException->getNodes() : $nodes,
1117
            $originalException instanceof GraphQLException ? $originalException->getSource() : null,
1118
            $originalException instanceof GraphQLException ? $originalException->getPositions() : null,
1119
            $originalException instanceof GraphQLException ? ($originalException->getPath() ?? $path) : $path,
1120
            $originalException
1121
        );
1122
    }
1123
}
1124