Passed
Push — master ( 2e6153...5ee55a )
by Quang
02:19
created

ExecutionStrategy::defaultTypeResolver()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 17
nc 9
nop 4
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\ExecutionException;
6
use Digia\GraphQL\Error\InvalidTypeException;
7
use Digia\GraphQL\Error\UndefinedException;
8
use Digia\GraphQL\Execution\Resolver\ResolveInfo;
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\Type\Definition\AbstractTypeInterface;
17
use Digia\GraphQL\Type\Definition\Field;
18
use Digia\GraphQL\Type\Definition\InterfaceType;
19
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
20
use Digia\GraphQL\Type\Definition\ListType;
21
use Digia\GraphQL\Type\Definition\NonNullType;
22
use Digia\GraphQL\Type\Definition\ObjectType;
23
use Digia\GraphQL\Type\Definition\TypeInterface;
24
use Digia\GraphQL\Type\Definition\UnionType;
25
use Digia\GraphQL\Type\Schema;
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
use Digia\GraphQL\Util\ValueNodeCoercer;
34
35
/**
36
 * Class AbstractStrategy
37
 * @package Digia\GraphQL\Execution\Strategies
38
 */
39
abstract class ExecutionStrategy
40
{
41
    /**
42
     * @var ExecutionContext
43
     */
44
    protected $context;
45
46
    /**
47
     * @var OperationDefinitionNode
48
     */
49
    protected $operation;
50
51
    /**
52
     * @var mixed
53
     */
54
    protected $rootValue;
55
56
57
    /**
58
     * @var ValuesResolver
59
     */
60
    protected $valuesResolver;
61
62
    /**
63
     * @var array
64
     */
65
    protected $finalResult;
66
67
    /**
68
     * @var array
69
     */
70
    protected static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
71
72
    /**
73
     * AbstractStrategy constructor.
74
     * @param ExecutionContext        $context
75
     *
76
     * @param OperationDefinitionNode $operation
77
     */
78
    public function __construct(
79
        ExecutionContext $context,
80
        OperationDefinitionNode $operation,
81
        $rootValue
82
    ) {
83
        $this->context        = $context;
84
        $this->operation      = $operation;
85
        $this->rootValue      = $rootValue;
86
        // TODO: Inject the ValuesResolver instance by using a builder for this class.
87
        $this->valuesResolver = new ValuesResolver(new ValueNodeCoercer());
88
    }
89
90
    /**
91
     * @return array|null
92
     */
93
    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...
94
95
    /**
96
     * @param ObjectType       $runtimeType
97
     * @param SelectionSetNode $selectionSet
98
     * @param                  $fields
99
     * @param                  $visitedFragmentNames
100
     * @return mixed
101
     * @throws InvalidTypeException
102
     * @throws \Digia\GraphQL\Error\ExecutionException
103
     * @throws \Digia\GraphQL\Error\InvariantException
104
     */
105
    protected function collectFields(
106
        ObjectType $runtimeType,
107
        SelectionSetNode $selectionSet,
108
        &$fields,
109
        &$visitedFragmentNames
110
    ) {
111
        foreach ($selectionSet->getSelections() as $selection) {
112
            // Check if this Node should be included first
113
            if (!$this->shouldIncludeNode($selection)) {
114
                continue;
115
            }
116
            // Collect fields
117
            if ($selection instanceof FieldNode) {
118
                $fieldName = $this->getFieldNameKey($selection);
119
120
                if (!isset($fields[$fieldName])) {
121
                    $fields[$fieldName] = [];
122
                }
123
124
                $fields[$fieldName][] = $selection;
125
            } elseif ($selection instanceof InlineFragmentNode) {
126
                if (!$this->doesFragmentConditionMatch($selection, $runtimeType)) {
127
                    continue;
128
                }
129
130
                $this->collectFields($runtimeType, $selection->getSelectionSet(), $fields, $visitedFragmentNames);
131
            } elseif ($selection instanceof FragmentSpreadNode) {
132
                $fragmentName = $selection->getNameValue();
133
134
                if (!empty($visitedFragmentNames[$fragmentName])) {
135
                    continue;
136
                }
137
138
                $visitedFragmentNames[$fragmentName] = true;
139
                /** @var FragmentDefinitionNode $fragment */
140
                $fragment = $this->context->getFragments()[$fragmentName];
141
                $this->collectFields($runtimeType, $fragment->getSelectionSet(), $fields, $visitedFragmentNames);
142
            }
143
        }
144
145
        return $fields;
146
    }
147
148
149
    /**
150
     * @param $node
151
     * @return bool
152
     * @throws InvalidTypeException
153
     * @throws \Digia\GraphQL\Error\ExecutionException
154
     * @throws \Digia\GraphQL\Error\InvariantException
155
     */
156
    private function shouldIncludeNode(NodeInterface $node): bool
157
    {
158
159
        $contextVariables = $this->context->getVariableValues();
160
161
        $skip = $this->valuesResolver->getDirectiveValues(GraphQLSkipDirective(), $node, $contextVariables);
162
163
        if ($skip && $skip['if'] === true) {
164
            return false;
165
        }
166
167
        $include = $this->valuesResolver->getDirectiveValues(GraphQLIncludeDirective(), $node, $contextVariables);
168
169
        if ($include && $include['if'] === false) {
170
            return false;
171
        }
172
173
        return true;
174
    }
175
176
    /**
177
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
178
     * @param ObjectType                                $type
179
     * @return bool
180
     * @throws InvalidTypeException
181
     */
182
    private function doesFragmentConditionMatch(
183
        NodeInterface $fragment,
184
        ObjectType $type
185
    ): bool {
186
        $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

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

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

690
        $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...
691
        $returnTypeName  = $returnType->getName();
692
693
        if (!$runtimeType instanceof ObjectType) {
694
            $parentTypeName = $info->getParentType()->getName();
695
            $fieldName      = $info->getFieldName();
696
697
            throw new ExecutionException(
698
                "Abstract type {$returnTypeName} must resolve to an Object type at runtime " .
699
                "for field {$parentTypeName}.{$fieldName} with " .
700
                'value "' . $result . '", received "{$runtimeTypeName}".'
701
            );
702
        }
703
704
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
705
            throw new ExecutionException(
706
                "Runtime Object type \"{$runtimeTypeName}\" is not a possible type for \"{$returnTypeName}\"."
707
            );
708
        }
709
710
        if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
711
            throw new ExecutionException(
712
                "Schema must contain unique named types but contains multiple types named \"{$runtimeTypeName}\". " .
713
                "Make sure that `resolveType` function of abstract type \"{$returnTypeName}\" returns the same " .
714
                "type instance as referenced anywhere else within the schema."
715
            );
716
        }
717
718
        return $runtimeType;
719
    }
720
721
    /**
722
     * @param                       $value
723
     * @param                       $context
724
     * @param ResolveInfo           $info
725
     * @param AbstractTypeInterface $abstractType
726
     * @return TypeInterface|null
727
     */
728
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractTypeInterface $abstractType)
729
    {
730
        $possibleTypes           = $info->getSchema()->getPossibleTypes($abstractType);
731
        $promisedIsTypeOfResults = [];
732
733
        foreach ($possibleTypes as $index => $type) {
734
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
735
736
            if (null !== $isTypeOfResult) {
737
                if ($this->isPromise($isTypeOfResult)) {
738
                    $promisedIsTypeOfResults[$index] = $isTypeOfResult;
739
                } elseif ($isTypeOfResult) {
740
                    return $type;
741
                }
742
            }
743
        }
744
745
        if (!empty($promisedIsTypeOfResults)) {
746
            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...
747
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
748
                    foreach ($isTypeOfResults as $index => $result) {
749
                        if ($result) {
750
                            return $possibleTypes[$index];
751
                        }
752
                    }
753
                    return null;
754
                });
755
        }
756
757
        return null;
758
    }
759
760
    /**
761
     * @param ListType    $returnType
762
     * @param             $fieldNodes
763
     * @param ResolveInfo $info
764
     * @param             $path
765
     * @param             $result
766
     * @return array|\React\Promise\Promise
767
     * @throws \Throwable
768
     */
769
    private function completeListValue(
770
        ListType $returnType,
771
        $fieldNodes,
772
        ResolveInfo $info,
773
        $path,
774
        &$result
775
    ) {
776
        $itemType = $returnType->getOfType();
777
778
        $completedItems  = [];
779
        $containsPromise = false;
780
        foreach ($result as $key => $item) {
781
            $fieldPath        = $path;
782
            $fieldPath[]      = $key;
783
            $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...
784
            $completedItems[] = $completedItem;
785
            $containsPromise  = $containsPromise || $this->isPromise($completedItem);
786
        }
787
788
        return $containsPromise ? \React\Promise\all($completedItems) : $completedItems;
789
    }
790
791
    /**
792
     * @param LeafTypeInterface $returnType
793
     * @param                   $result
794
     * @return mixed
795
     * @throws ExecutionException
796
     */
797
    private function completeLeafValue(LeafTypeInterface $returnType, &$result)
798
    {
799
        $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

799
        /** @scrutinizer ignore-call */ 
800
        $serializedResult = $returnType->serialize($result);
Loading history...
800
801
        if ($serializedResult === null) {
802
            throw new ExecutionException(
803
                sprintf('Expected a value of type "%s" but received: %s', toString($returnType), toString($result))
804
            );
805
        }
806
807
        return $serializedResult;
808
    }
809
810
    /**
811
     * @param ObjectType  $returnType
812
     * @param             $fieldNodes
813
     * @param ResolveInfo $info
814
     * @param             $path
815
     * @param             $result
816
     * @return array
817
     * @throws ExecutionException
818
     * @throws InvalidTypeException
819
     * @throws \Digia\GraphQL\Error\InvariantException
820
     * @throws \Throwable
821
     */
822
    private function completeObjectValue(
823
        ObjectType $returnType,
824
        $fieldNodes,
825
        ResolveInfo $info,
826
        $path,
827
        &$result
828
    ) {
829
        return $this->collectAndExecuteSubFields(
830
            $returnType,
831
            $fieldNodes,
832
            $info,
833
            $path,
834
            $result
835
        );
836
    }
837
838
    /**
839
     * @param Field            $field
840
     * @param FieldNode        $fieldNode
841
     * @param callable         $resolveFunction
842
     * @param                  $rootValue
843
     * @param ExecutionContext $context
844
     * @param ResolveInfo      $info
845
     * @return array|\Throwable
846
     */
847
    private function resolveFieldValueOrError(
848
        Field $field,
849
        FieldNode $fieldNode,
850
        ?callable $resolveFunction,
851
        $rootValue,
852
        ExecutionContext $context,
853
        ResolveInfo $info
854
    ) {
855
        try {
856
            $args = $this->valuesResolver->coerceArgumentValues($field, $fieldNode, $context->getVariableValues());
857
858
            return $resolveFunction($rootValue, $args, $context->getContextValue(), $info);
859
        } catch (\Throwable $error) {
860
            return $error;
861
        }
862
    }
863
864
    /**
865
     * Try to resolve a field without any field resolver function.
866
     *
867
     * @param array|object $rootValue
868
     * @param              $args
869
     * @param              $context
870
     * @param ResolveInfo  $info
871
     * @return mixed|null
872
     */
873
    public static function defaultFieldResolver($rootValue, $args, $context, ResolveInfo $info)
874
    {
875
        $fieldName = $info->getFieldName();
876
        $property  = null;
877
878
        if (is_array($rootValue) && isset($rootValue[$fieldName])) {
879
            $property = $rootValue[$fieldName];
880
        }
881
882
        if (is_object($rootValue)) {
883
            $getter = 'get' . ucfirst($fieldName);
884
            if (method_exists($rootValue, $getter)) {
885
                $property = $rootValue->{$getter}();
886
            } elseif (property_exists($rootValue, $fieldName)) {
887
                $property = $rootValue->{$fieldName};
888
            }
889
        }
890
891
892
        return $property instanceof \Closure ? $property($rootValue, $args, $context, $info) : $property;
893
    }
894
895
    /**
896
     * @param ObjectType  $returnType
897
     * @param             $fieldNodes
898
     * @param ResolveInfo $info
899
     * @param             $path
900
     * @param             $result
901
     * @return array
902
     * @throws InvalidTypeException
903
     * @throws \Digia\GraphQL\Error\ExecutionException
904
     * @throws \Digia\GraphQL\Error\InvariantException
905
     * @throws \Throwable
906
     */
907
    private function collectAndExecuteSubFields(
908
        ObjectType $returnType,
909
        $fieldNodes,
910
        ResolveInfo $info,
911
        $path,
912
        &$result
913
    ) {
914
        $subFields            = [];
915
        $visitedFragmentNames = [];
916
917
        foreach ($fieldNodes as $fieldNode) {
918
            /** @var FieldNode $fieldNode */
919
            if ($fieldNode->getSelectionSet() !== null) {
920
                $subFields = $this->collectFields(
921
                    $returnType,
922
                    $fieldNode->getSelectionSet(),
923
                    $subFields,
924
                    $visitedFragmentNames
925
                );
926
            }
927
        }
928
929
        if (!empty($subFields)) {
930
            return $this->executeFields($returnType, $result, $path, $subFields);
931
        }
932
933
        return $result;
934
    }
935
936
    /**
937
     * @param $value
938
     * @return bool
939
     */
940
    protected function isPromise($value): bool
941
    {
942
        return $value instanceof ExtendedPromiseInterface;
943
    }
944
}
945