Completed
Pull Request — master (#130)
by Quang
02:22
created

ExecutionStrategy::executeFields()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

648
        $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\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

648
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->/** @scrutinizer ignore-call */ getName();
Loading history...
649
        $returnTypeName  = $returnType->getName();
650
651
        if (!$runtimeType instanceof ObjectType) {
652
            $parentTypeName = $info->getParentType()->getName();
653
            $fieldName      = $info->getFieldName();
654
655
            throw new ExecutionException(
656
                "Abstract type {$returnTypeName} must resolve to an Object type at runtime " .
657
                "for field {$parentTypeName}.{$fieldName} with " .
658
                'value "' . $result . '", received "{$runtimeTypeName}".'
659
            );
660
        }
661
662
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
663
            throw new ExecutionException(
664
                "Runtime Object type \"{$runtimeTypeName}\" is not a possible type for \"{$returnTypeName}\"."
665
            );
666
        }
667
668
        if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
669
            throw new ExecutionException(
670
                "Schema must contain unique named types but contains multiple types named \"{$runtimeTypeName}\". " .
671
                "Make sure that `resolveType` function of abstract type \"{$returnTypeName}\" returns the same " .
672
                "type instance as referenced anywhere else within the schema."
673
            );
674
        }
675
676
        return $runtimeType;
677
    }
678
679
    /**
680
     * @param                       $value
681
     * @param                       $context
682
     * @param ResolveInfo           $info
683
     * @param AbstractTypeInterface $abstractType
684
     * @return TypeInterface|null
685
     */
686
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractTypeInterface $abstractType)
687
    {
688
        $possibleTypes = $info->getSchema()->getPossibleTypes($abstractType);
689
690
        foreach ($possibleTypes as $type) {
691
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
692
693
            if (null !== $isTypeOfResult) {
694
                if ($isTypeOfResult) {
695
                    return $type;
696
                }
697
            }
698
        }
699
700
        return null;
701
    }
702
703
    /**
704
     * @param ListType    $returnType
705
     * @param             $fieldNodes
706
     * @param ResolveInfo $info
707
     * @param             $path
708
     * @param             $result
709
     * @return array
710
     * @throws \Throwable
711
     */
712
    private function completeListValue(
713
        ListType $returnType,
714
        $fieldNodes,
715
        ResolveInfo $info,
716
        $path,
717
        &$result
718
    ) {
719
        $itemType = $returnType->getOfType();
720
721
        $completedItems = [];
722
723
        foreach ($result as $key => $item) {
724
            $fieldPath        = $path;
725
            $fieldPath[]      = $key;
726
            $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...
727
            $completedItems[] = $completedItem;
728
        }
729
730
        return $completedItems;
731
    }
732
733
    /**
734
     * @param LeafTypeInterface $returnType
735
     * @param                   $result
736
     * @return mixed
737
     * @throws ExecutionException
738
     */
739
    private function completeLeafValue(LeafTypeInterface $returnType, &$result)
740
    {
741
        $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

741
        /** @scrutinizer ignore-call */ 
742
        $serializedResult = $returnType->serialize($result);
Loading history...
742
743
        if ($serializedResult === null) {
744
            throw new ExecutionException(
745
                sprintf('Expected a value of type "%s" but received: %s', toString($returnType), toString($result))
746
            );
747
        }
748
749
        return $serializedResult;
750
    }
751
752
    /**
753
     * @param ObjectType  $returnType
754
     * @param             $fieldNodes
755
     * @param ResolveInfo $info
756
     * @param             $path
757
     * @param             $result
758
     * @return array
759
     * @throws ExecutionException
760
     * @throws InvalidTypeException
761
     * @throws \Digia\GraphQL\Error\InvariantException
762
     * @throws \Throwable
763
     */
764
    private function completeObjectValue(
765
        ObjectType $returnType,
766
        $fieldNodes,
767
        ResolveInfo $info,
768
        $path,
769
        &$result
770
    ) {
771
        return $this->collectAndExecuteSubFields(
772
            $returnType,
773
            $fieldNodes,
774
            $info,
775
            $path,
776
            $result
777
        );
778
    }
779
780
    /**
781
     * @param Field            $field
782
     * @param FieldNode        $fieldNode
783
     * @param callable         $resolveFunction
784
     * @param                  $rootValue
785
     * @param ExecutionContext $context
786
     * @param ResolveInfo      $info
787
     * @return array|\Throwable
788
     */
789
    private function resolveFieldValueOrError(
790
        Field $field,
791
        FieldNode $fieldNode,
792
        ?callable $resolveFunction,
793
        $rootValue,
794
        ExecutionContext $context,
795
        ResolveInfo $info
796
    ) {
797
        try {
798
            $args = $this->valuesResolver->coerceArgumentValues($field, $fieldNode, $context->getVariableValues());
799
800
            return $resolveFunction($rootValue, $args, $context->getContextValue(), $info);
801
        } catch (\Throwable $error) {
802
            return $error;
803
        }
804
    }
805
806
    /**
807
     * Try to resolve a field without any field resolver function.
808
     *
809
     * @param array|object $rootValue
810
     * @param              $args
811
     * @param              $context
812
     * @param ResolveInfo  $info
813
     * @return mixed|null
814
     */
815
    public static function defaultFieldResolver($rootValue, $args, $context, ResolveInfo $info)
816
    {
817
        $fieldName = $info->getFieldName();
818
        $property  = null;
819
820
        if (is_array($rootValue) && isset($rootValue[$fieldName])) {
821
            $property = $rootValue[$fieldName];
822
        }
823
824
        if (is_object($rootValue)) {
825
            $getter = 'get' . ucfirst($fieldName);
826
            if (method_exists($rootValue, $getter)) {
827
                $property = $rootValue->{$getter}();
828
            } else if (property_exists($rootValue, $fieldName)) {
829
                $property = $rootValue->{$fieldName};
830
            }
831
        }
832
833
834
        return $property instanceof \Closure ? $property($rootValue, $args, $context, $info) : $property;
835
    }
836
837
    /**
838
     * @param ObjectType  $returnType
839
     * @param             $fieldNodes
840
     * @param ResolveInfo $info
841
     * @param             $path
842
     * @param             $result
843
     * @return array
844
     * @throws InvalidTypeException
845
     * @throws \Digia\GraphQL\Error\ExecutionException
846
     * @throws \Digia\GraphQL\Error\InvariantException
847
     * @throws \Throwable
848
     */
849
    private function collectAndExecuteSubFields(
850
        ObjectType $returnType,
851
        $fieldNodes,
852
        ResolveInfo $info,
853
        $path,
854
        &$result
855
    ) {
856
        $subFields            = [];
857
        $visitedFragmentNames = [];
858
859
        foreach ($fieldNodes as $fieldNode) {
860
            /** @var FieldNode $fieldNode */
861
            if ($fieldNode->getSelectionSet() !== null) {
862
                $subFields = $this->collectFields(
863
                    $returnType,
864
                    $fieldNode->getSelectionSet(),
865
                    $subFields,
866
                    $visitedFragmentNames
867
                );
868
            }
869
        }
870
871
        if (!empty($subFields)) {
872
            return $this->executeFields($returnType, $result, $path, $subFields);
873
        }
874
875
        return $result;
876
    }
877
}
878