Passed
Pull Request — master (#131)
by Quang
02:17
created

ExecutionStrategy::completeAbstractValue()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 51
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 51
rs 9.4109
c 0
b 0
f 0
cc 3
eloc 32
nc 4
nop 5

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\ExecutionException;
6
use Digia\GraphQL\Error\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
34
/**
35
 * Class AbstractStrategy
36
 * @package Digia\GraphQL\Execution\Strategies
37
 */
38
abstract class ExecutionStrategy
39
{
40
    /**
41
     * @var ExecutionContext
42
     */
43
    protected $context;
44
45
    /**
46
     * @var OperationDefinitionNode
47
     */
48
    protected $operation;
49
50
    /**
51
     * @var mixed
52
     */
53
    protected $rootValue;
54
55
56
    /**
57
     * @var ValuesResolver
58
     */
59
    protected $valuesResolver;
60
61
    /**
62
     * @var array
63
     */
64
    protected $finalResult;
65
66
    /**
67
     * @var array
68
     */
69
    protected static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
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
        $isContainsPromise = false;
233
234
        foreach ($fields as $fieldName => $fieldNodes) {
235
            $fieldPath   = $path;
236
            $fieldPath[] = $fieldName;
237
238
            try {
239
                $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...
240
            } catch (UndefinedException $ex) {
241
                continue;
242
            }
243
244
            if (!$isContainsPromise && $this->isPromise($result)) {
245
                $isContainsPromise = true;
246
            }
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
                return $completed->then(null, function ($error) use ($context) {
478
                    $context->addError($error);
479
                    return new \React\Promise\FulfilledPromise(null);
480
                });
481
            }
482
483
            return $completed;
484
        } catch (\Exception $ex) {
485
            $this->context->addError(new ExecutionException($ex->getMessage()));
486
            return null;
487
        }
488
    }
489
490
    /**
491
     * @param TypeInterface $fieldType
492
     * @param               $fieldNodes
493
     * @param ResolveInfo   $info
494
     * @param               $path
495
     * @param               $result
496
     * @throws \Throwable
497
     */
498
    public function completeValueWithLocatedError(
499
        TypeInterface $fieldType,
500
        $fieldNodes,
501
        ResolveInfo $info,
502
        $path,
503
        $result
504
    ) {
505
        try {
506
            $completed = $this->completeValue(
507
                $fieldType,
508
                $fieldNodes,
509
                $info,
510
                $path,
511
                $result
512
            );
513
514
            return $completed;
515
        } catch (\Exception $ex) {
516
            //@TODO throw located error
517
            throw $ex;
518
        } catch (\Throwable $ex) {
519
            //@TODO throw located error
520
            throw $ex;
521
        }
522
    }
523
524
    /**
525
     * @param TypeInterface $returnType
526
     * @param               $fieldNodes
527
     * @param ResolveInfo   $info
528
     * @param               $path
529
     * @param               $result
530
     * @return array|mixed
531
     * @throws ExecutionException
532
     * @throws \Throwable
533
     */
534
    private function completeValue(
535
        TypeInterface $returnType,
536
        $fieldNodes,
537
        ResolveInfo $info,
538
        $path,
539
        &$result
540
    ) {
541
        if ($this->isPromise($result)) {
542
            /** @var ExtendedPromiseInterface $result */
543
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
544
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
545
            });
546
        }
547
548
        if ($result instanceof \Throwable) {
549
            throw $result;
550
        }
551
552
        // If result is null-like, return null.
553
        if (null === $result) {
554
            return null;
555
        }
556
557
        if ($returnType instanceof NonNullType) {
558
            $completed = $this->completeValue(
559
                $returnType->getOfType(),
560
                $fieldNodes,
561
                $info,
562
                $path,
563
                $result
564
            );
565
566
            if ($completed === null) {
567
                throw new ExecutionException(
568
                    sprintf(
569
                        'Cannot return null for non-nullable field %s.%s.',
570
                        $info->getParentType(), $info->getFieldName()
571
                    )
572
                );
573
            }
574
575
            return $completed;
576
        }
577
578
        // If field type is List, complete each item in the list with the inner type
579
        if ($returnType instanceof ListType) {
580
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
581
        }
582
583
584
        // If field type is Scalar or Enum, serialize to a valid value, returning
585
        // null if serialization is not possible.
586
        if ($returnType instanceof LeafTypeInterface) {
587
            return $this->completeLeafValue($returnType, $result);
588
        }
589
590
        //@TODO Make a function for checking abstract type?
591
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
592
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
593
        }
594
595
        // Field type must be Object, Interface or Union and expect sub-selections.
596
        if ($returnType instanceof ObjectType) {
597
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
598
        }
599
600
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
601
    }
602
603
    /**
604
     * @param AbstractTypeInterface $returnType
605
     * @param                       $fieldNodes
606
     * @param ResolveInfo           $info
607
     * @param                       $path
608
     * @param                       $result
609
     * @return array|PromiseInterface
610
     * @throws ExecutionException
611
     * @throws InvalidTypeException
612
     * @throws \Digia\GraphQL\Error\InvariantException
613
     * @throws \Throwable
614
     */
615
    private function completeAbstractValue(
616
        AbstractTypeInterface $returnType,
617
        $fieldNodes,
618
        ResolveInfo $info,
619
        $path,
620
        &$result
621
    ) {
622
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
623
624
        if (null === $runtimeType) {
625
            //@TODO Show warning
626
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
627
        }
628
629
        if ($this->isPromise($runtimeType)) {
630
            /** @var PromiseInterface $runtimeType */
631
            return $runtimeType->then(function ($resolvedRuntimeType) use (
632
                $returnType,
633
                $fieldNodes,
634
                $info,
635
                $path,
636
                &$result
637
            ) {
638
                return $this->completeObjectValue(
639
                    $this->ensureValidRuntimeType(
640
                        $resolvedRuntimeType,
641
                        $returnType,
642
                        $fieldNodes,
643
                        $info,
644
                        $result
645
                    ),
646
                    $fieldNodes,
647
                    $info,
648
                    $path,
649
                    $result
650
                );
651
            });
652
        }
653
654
        return $this->completeObjectValue(
655
            $this->ensureValidRuntimeType(
656
                $runtimeType,
657
                $returnType,
658
                $fieldNodes,
659
                $info,
660
                $result
661
            ),
662
            $fieldNodes,
663
            $info,
664
            $path,
665
            $result
666
        );
667
    }
668
669
    /**
670
     * @param                       $runtimeTypeOrName
671
     * @param AbstractTypeInterface $returnType
672
     * @param                       $fieldNodes
673
     * @param ResolveInfo           $info
674
     * @param                       $result
675
     * @return TypeInterface|ObjectType|null
676
     * @throws ExecutionException
677
     */
678
    private function ensureValidRuntimeType(
679
        $runtimeTypeOrName,
680
        AbstractTypeInterface $returnType,
681
        $fieldNodes,
682
        ResolveInfo $info,
683
        &$result
684
    ) {
685
        $runtimeType = is_string($runtimeTypeOrName)
686
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
687
            : $runtimeTypeOrName;
688
689
        $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

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

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

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