Passed
Pull Request — master (#142)
by Quang
02:43
created

ExecutionStrategy   F

Complexity

Total Complexity 102

Size/Duplication

Total Lines 935
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 102
dl 0
loc 935
rs 1.263
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getFieldNameKey() 0 3 2
A doesFragmentConditionMatch() 0 21 4
B shouldIncludeNode() 0 18 5
D collectFields() 0 41 9
B executeFields() 0 35 6
A __construct() 0 10 1
B collectAndExecuteSubFields() 0 27 4
C defaultTypeResolver() 0 39 11
A completeLeafValue() 0 11 2
A completeObjectValue() 0 13 1
A isPromise() 0 3 1
B resolveField() 0 37 2
A completeListValue() 0 20 4
C completeValue() 0 67 11
B executeFieldsSerially() 0 44 4
B completeValueCatchingError() 0 39 4
B ensureValidRuntimeType() 0 40 6
B getFieldDefinition() 0 24 6
A completeValueWithLocatedError() 0 23 3
A resolveFieldValueOrError() 0 14 2
B defaultFieldResolver() 0 20 7
A determineResolveFunction() 0 15 3
A buildResolveInfo() 0 19 1
A completeAbstractValue() 0 51 3

How to fix   Complexity   

Complex Class

Complex classes like ExecutionStrategy often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExecutionStrategy, and based on these observations, apply Extract Interface, too.

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 Digia\GraphQL\Util\ValueNodeCoercer;
27
use React\Promise\ExtendedPromiseInterface;
28
use React\Promise\PromiseInterface;
29
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
30
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
31
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
32
use function Digia\GraphQL\Util\toString;
33
use function Digia\GraphQL\Util\typeFromAST;
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
284
        $finalResults = [];
285
286
        $promise = new \React\Promise\FulfilledPromise([]);
287
288
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
289
            $fieldPath   = $path;
290
            $fieldPath[] = $fieldName;
291
            try {
292
                $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...
293
            } catch (UndefinedException $ex) {
294
                return null;
295
            }
296
297
            if ($this->isPromise($result)) {
298
                /** @var ExtendedPromiseInterface $result */
299
                return $result->then(function ($resolvedResult) use ($fieldName, $results) {
300
                    $results[$fieldName] = $resolvedResult;
301
                    return $results;
302
                });
303
            }
304
305
            $results[$fieldName] = $result;
306
307
            return $results;
308
        };
309
310
        foreach ($fields as $fieldName => $fieldNodes) {
311
            $promise = $promise->then(function ($resolvedResults) use ($resolve, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
312
                return $resolve($resolvedResults, $fieldName, $path, $objectType, $rootValue, $fieldNodes);
313
            });
314
        }
315
316
        $promise->then(function ($resolvedResults) use (&$finalResults) {
317
            $finalResults = $resolvedResults ?? [];
318
        });
319
320
        return $finalResults;
321
    }
322
323
    /**
324
     * @param Schema     $schema
325
     * @param ObjectType $parentType
326
     * @param string     $fieldName
327
     * @return \Digia\GraphQL\Type\Definition\Field|null
328
     * @throws InvalidTypeException
329
     */
330
    public function getFieldDefinition(
331
        Schema $schema,
332
        ObjectType $parentType,
333
        string $fieldName
334
    ) {
335
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
336
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
337
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
338
339
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
340
            return $schemaMetaFieldDefinition;
341
        }
342
343
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
344
            return $typeMetaFieldDefinition;
345
        }
346
347
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
348
            return $typeNameMetaFieldDefinition;
349
        }
350
351
        $fields = $parentType->getFields();
352
353
        return $fields[$fieldName] ?? null;
354
    }
355
356
357
    /**
358
     * @param ObjectType $parentType
359
     * @param            $rootValue
360
     * @param            $fieldNodes
361
     * @param            $path
362
     * @return array|null|\Throwable
363
     * @throws InvalidTypeException
364
     * @throws \Digia\GraphQL\Error\ExecutionException
365
     * @throws \Digia\GraphQL\Error\InvariantException
366
     * @throws \Throwable
367
     */
368
    protected function resolveField(
369
        ObjectType $parentType,
370
        $rootValue,
371
        $fieldNodes,
372
        $path
373
    ) {
374
        /** @var FieldNode $fieldNode */
375
        $fieldNode = $fieldNodes[0];
376
377
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
378
379
        if (null === $field) {
380
            throw new UndefinedException('Undefined field definition.');
381
        }
382
383
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
384
385
        $resolveFunction = $this->determineResolveFunction($field, $parentType, $this->context);
386
387
        $result = $this->resolveFieldValueOrError(
388
            $field,
389
            $fieldNode,
390
            $resolveFunction,
391
            $rootValue,
392
            $this->context,
393
            $info
394
        );
395
396
        $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...
397
            $field->getType(),
398
            $fieldNodes,
399
            $info,
400
            $path,
401
            $result// $result is passed as $source
402
        );
403
404
        return $result;
405
    }
406
407
    /**
408
     * @param array            $fieldNodes
409
     * @param FieldNode        $fieldNode
410
     * @param Field            $field
411
     * @param ObjectType       $parentType
412
     * @param                  $path
413
     * @param ExecutionContext $context
414
     * @return ResolveInfo
415
     */
416
    private function buildResolveInfo(
417
        array $fieldNodes,
418
        FieldNode $fieldNode,
419
        Field $field,
420
        ObjectType $parentType,
421
        $path,
422
        ExecutionContext $context
423
    ) {
424
        return new ResolveInfo([
425
            'fieldName'      => $fieldNode->getNameValue(),
426
            'fieldNodes'     => $fieldNodes,
427
            'returnType'     => $field->getType(),
428
            'parentType'     => $parentType,
429
            'path'           => $path,
430
            'schema'         => $context->getSchema(),
431
            'fragments'      => $context->getFragments(),
432
            'rootValue'      => $context->getRootValue(),
433
            'operation'      => $context->getOperation(),
434
            'variableValues' => $context->getVariableValues(),
435
        ]);
436
    }
437
438
    /**
439
     * @param Field            $field
440
     * @param ObjectType       $objectType
441
     * @param ExecutionContext $context
442
     * @return callable|mixed|null
443
     */
444
    private function determineResolveFunction(
445
        Field $field,
446
        ObjectType $objectType,
447
        ExecutionContext $context
448
    ) {
449
450
        if ($field->hasResolve()) {
451
            return $field->getResolve();
452
        }
453
454
        if ($objectType->hasResolve()) {
455
            return $objectType->getResolve();
456
        }
457
458
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
459
    }
460
461
    /**
462
     * @param TypeInterface $fieldType
463
     * @param               $fieldNodes
464
     * @param ResolveInfo   $info
465
     * @param               $path
466
     * @param               $result
467
     * @return null
468
     * @throws \Throwable
469
     */
470
    public function completeValueCatchingError(
471
        TypeInterface $fieldType,
472
        $fieldNodes,
473
        ResolveInfo $info,
474
        $path,
475
        &$result
476
    ) {
477
        if ($fieldType instanceof NonNullType) {
478
            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...
479
                $fieldType,
480
                $fieldNodes,
481
                $info,
482
                $path,
483
                $result
484
            );
485
        }
486
487
        try {
488
            $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...
489
                $fieldType,
490
                $fieldNodes,
491
                $info,
492
                $path,
493
                $result
494
            );
495
496
            if ($this->isPromise($completed)) {
497
                $context = $this->context;
498
                /** @var ExtendedPromiseInterface $completed */
499
                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...
500
                    $context->addError($error);
501
                    return new \React\Promise\FulfilledPromise(null);
502
                });
503
            }
504
505
            return $completed;
506
        } catch (\Exception $ex) {
507
            $this->context->addError(new ExecutionException($ex->getMessage()));
508
            return null;
509
        }
510
    }
511
512
    /**
513
     * @param TypeInterface $fieldType
514
     * @param               $fieldNodes
515
     * @param ResolveInfo   $info
516
     * @param               $path
517
     * @param               $result
518
     * @throws \Throwable
519
     */
520
    public function completeValueWithLocatedError(
521
        TypeInterface $fieldType,
522
        $fieldNodes,
523
        ResolveInfo $info,
524
        $path,
525
        $result
526
    ) {
527
        try {
528
            $completed = $this->completeValue(
529
                $fieldType,
530
                $fieldNodes,
531
                $info,
532
                $path,
533
                $result
534
            );
535
536
            return $completed;
537
        } catch (\Exception $ex) {
538
            //@TODO throw located error
539
            throw $ex;
540
        } catch (\Throwable $ex) {
541
            //@TODO throw located error
542
            throw $ex;
543
        }
544
    }
545
546
    /**
547
     * @param TypeInterface $returnType
548
     * @param               $fieldNodes
549
     * @param ResolveInfo   $info
550
     * @param               $path
551
     * @param               $result
552
     * @return array|mixed
553
     * @throws ExecutionException
554
     * @throws \Throwable
555
     */
556
    private function completeValue(
557
        TypeInterface $returnType,
558
        $fieldNodes,
559
        ResolveInfo $info,
560
        $path,
561
        &$result
562
    ) {
563
        if ($this->isPromise($result)) {
564
            /** @var ExtendedPromiseInterface $result */
565
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
566
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
567
            });
568
        }
569
570
        if ($result instanceof \Throwable) {
571
            throw $result;
572
        }
573
574
        // If result is null-like, return null.
575
        if (null === $result) {
576
            return null;
577
        }
578
579
        if ($returnType instanceof NonNullType) {
580
            $completed = $this->completeValue(
581
                $returnType->getOfType(),
582
                $fieldNodes,
583
                $info,
584
                $path,
585
                $result
586
            );
587
588
            if ($completed === null) {
589
                throw new ExecutionException(
590
                    sprintf(
591
                        'Cannot return null for non-nullable field %s.%s.',
592
                        $info->getParentType(), $info->getFieldName()
593
                    )
594
                );
595
            }
596
597
            return $completed;
598
        }
599
600
        // If field type is List, complete each item in the list with the inner type
601
        if ($returnType instanceof ListType) {
602
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
603
        }
604
605
606
        // If field type is Scalar or Enum, serialize to a valid value, returning
607
        // null if serialization is not possible.
608
        if ($returnType instanceof LeafTypeInterface) {
609
            return $this->completeLeafValue($returnType, $result);
610
        }
611
612
        //@TODO Make a function for checking abstract type?
613
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
614
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
615
        }
616
617
        // Field type must be Object, Interface or Union and expect sub-selections.
618
        if ($returnType instanceof ObjectType) {
619
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
620
        }
621
622
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
623
    }
624
625
    /**
626
     * @param AbstractTypeInterface $returnType
627
     * @param                       $fieldNodes
628
     * @param ResolveInfo           $info
629
     * @param                       $path
630
     * @param                       $result
631
     * @return array|PromiseInterface
632
     * @throws ExecutionException
633
     * @throws InvalidTypeException
634
     * @throws \Digia\GraphQL\Error\InvariantException
635
     * @throws \Throwable
636
     */
637
    private function completeAbstractValue(
638
        AbstractTypeInterface $returnType,
639
        $fieldNodes,
640
        ResolveInfo $info,
641
        $path,
642
        &$result
643
    ) {
644
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
0 ignored issues
show
Bug introduced by
$info of type Digia\GraphQL\Execution\Resolver\ResolveInfo is incompatible with the type array expected by parameter $args of Digia\GraphQL\Type\Defin...nterface::resolveType(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

644
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
645
646
        if (null === $runtimeType) {
647
            //@TODO Show warning
648
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
649
        }
650
651
        if ($this->isPromise($runtimeType)) {
652
            /** @var ExtendedPromiseInterface $runtimeType */
653
            return $runtimeType->then(function ($resolvedRuntimeType) use (
654
                $returnType,
655
                $fieldNodes,
656
                $info,
657
                $path,
658
                &$result
659
            ) {
660
                return $this->completeObjectValue(
661
                    $this->ensureValidRuntimeType(
662
                        $resolvedRuntimeType,
663
                        $returnType,
664
                        $fieldNodes,
665
                        $info,
666
                        $result
667
                    ),
668
                    $fieldNodes,
669
                    $info,
670
                    $path,
671
                    $result
672
                );
673
            });
674
        }
675
676
        return $this->completeObjectValue(
677
            $this->ensureValidRuntimeType(
678
                $runtimeType,
679
                $returnType,
680
                $fieldNodes,
681
                $info,
682
                $result
683
            ),
684
            $fieldNodes,
685
            $info,
686
            $path,
687
            $result
688
        );
689
    }
690
691
    /**
692
     * @param                       $runtimeTypeOrName
693
     * @param AbstractTypeInterface $returnType
694
     * @param                       $fieldNodes
695
     * @param ResolveInfo           $info
696
     * @param                       $result
697
     * @return TypeInterface|ObjectType|null
698
     * @throws ExecutionException
699
     */
700
    private function ensureValidRuntimeType(
701
        $runtimeTypeOrName,
702
        AbstractTypeInterface $returnType,
703
        $fieldNodes,
704
        ResolveInfo $info,
705
        &$result
706
    ) {
707
        $runtimeType = is_string($runtimeTypeOrName)
708
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
709
            : $runtimeTypeOrName;
710
711
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

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

711
        $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...
712
        $returnTypeName  = $returnType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

712
        /** @scrutinizer ignore-call */ 
713
        $returnTypeName  = $returnType->getName();
Loading history...
713
714
        if (!$runtimeType instanceof ObjectType) {
715
            $parentTypeName = $info->getParentType()->getName();
716
            $fieldName      = $info->getFieldName();
717
718
            throw new ExecutionException(
719
                "Abstract type {$returnTypeName} must resolve to an Object type at runtime " .
720
                "for field {$parentTypeName}.{$fieldName} with " .
721
                'value "' . $result . '", received "{$runtimeTypeName}".'
722
            );
723
        }
724
725
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
726
            throw new ExecutionException(
727
                "Runtime Object type \"{$runtimeTypeName}\" is not a possible type for \"{$returnTypeName}\"."
728
            );
729
        }
730
731
        if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
732
            throw new ExecutionException(
733
                "Schema must contain unique named types but contains multiple types named \"{$runtimeTypeName}\". " .
734
                "Make sure that `resolveType` function of abstract type \"{$returnTypeName}\" returns the same " .
735
                "type instance as referenced anywhere else within the schema."
736
            );
737
        }
738
739
        return $runtimeType;
740
    }
741
742
    /**
743
     * @param                       $value
744
     * @param                       $context
745
     * @param ResolveInfo           $info
746
     * @param AbstractTypeInterface $abstractType
747
     * @return TypeInterface|null
748
     * @throws ExecutionException
749
     */
750
    private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractTypeInterface $abstractType)
751
    {
752
        $possibleTypes           = $info->getSchema()->getPossibleTypes($abstractType);
753
        $promisedIsTypeOfResults = [];
754
755
        foreach ($possibleTypes as $index => $type) {
756
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
757
            if (null !== $isTypeOfResult) {
758
                if ($this->isPromise($isTypeOfResult)) {
759
                    $promisedIsTypeOfResults[$index] = $isTypeOfResult;
760
                    continue;
761
                }
762
763
                if ($isTypeOfResult === true) {
764
                    return $type;
765
                }
766
767
                if (\is_array($value)) {
768
                    //@TODO Make `type` configurable
769
                    if (isset($value['type']) && $value['type'] === $type->getName()) {
770
                        return $type;
771
                    }
772
                }
773
            }
774
        }
775
776
        if (!empty($promisedIsTypeOfResults)) {
777
            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...
778
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
779
                    foreach ($isTypeOfResults as $index => $result) {
780
                        if ($result) {
781
                            return $possibleTypes[$index];
782
                        }
783
                    }
784
                    return null;
785
                });
786
        }
787
788
        return null;
789
    }
790
791
    /**
792
     * @param ListType    $returnType
793
     * @param             $fieldNodes
794
     * @param ResolveInfo $info
795
     * @param             $path
796
     * @param             $result
797
     * @return array|\React\Promise\Promise
798
     * @throws \Throwable
799
     */
800
    private function completeListValue(
801
        ListType $returnType,
802
        $fieldNodes,
803
        ResolveInfo $info,
804
        $path,
805
        &$result
806
    ) {
807
        $itemType = $returnType->getOfType();
808
809
        $completedItems  = [];
810
        $containsPromise = false;
811
        foreach ($result as $key => $item) {
812
            $fieldPath        = $path;
813
            $fieldPath[]      = $key;
814
            $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...
815
            $completedItems[] = $completedItem;
816
            $containsPromise  = $containsPromise || $this->isPromise($completedItem);
817
        }
818
819
        return $containsPromise ? \React\Promise\all($completedItems) : $completedItems;
820
    }
821
822
    /**
823
     * @param LeafTypeInterface $returnType
824
     * @param                   $result
825
     * @return mixed
826
     * @throws ExecutionException
827
     */
828
    private function completeLeafValue(LeafTypeInterface $returnType, &$result)
829
    {
830
        $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

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