Passed
Pull Request — master (#144)
by Quang
03:07
created

ExecutionStrategy::completeListValue()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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

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

654
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
655
656
        if (null === $runtimeType) {
657
            //@TODO Show warning
658
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
659
        }
660
661
        if ($this->isPromise($runtimeType)) {
662
            /** @var ExtendedPromiseInterface $runtimeType */
663
            return $runtimeType->then(function ($resolvedRuntimeType) use (
664
                $returnType,
665
                $fieldNodes,
666
                $info,
667
                $path,
668
                &$result
669
            ) {
670
                return $this->completeObjectValue(
671
                    $this->ensureValidRuntimeType(
672
                        $resolvedRuntimeType,
673
                        $returnType,
674
                        $fieldNodes,
675
                        $info,
676
                        $result
677
                    ),
678
                    $fieldNodes,
679
                    $info,
680
                    $path,
681
                    $result
682
                );
683
            });
684
        }
685
686
        return $this->completeObjectValue(
687
            $this->ensureValidRuntimeType(
688
                $runtimeType,
689
                $returnType,
690
                $fieldNodes,
691
                $info,
692
                $result
693
            ),
694
            $fieldNodes,
695
            $info,
696
            $path,
697
            $result
698
        );
699
    }
700
701
    /**
702
     * @param                       $runtimeTypeOrName
703
     * @param AbstractTypeInterface $returnType
704
     * @param                       $fieldNodes
705
     * @param ResolveInfo           $info
706
     * @param                       $result
707
     * @return TypeInterface|ObjectType|null
708
     * @throws ExecutionException
709
     */
710
    private function ensureValidRuntimeType(
711
        $runtimeTypeOrName,
712
        AbstractTypeInterface $returnType,
713
        $fieldNodes,
714
        ResolveInfo $info,
715
        &$result
716
    ) {
717
        $runtimeType = is_string($runtimeTypeOrName)
718
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
719
            : $runtimeTypeOrName;
720
721
        $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

721
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->/** @scrutinizer ignore-call */ getName();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

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

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

851
        /** @scrutinizer ignore-call */ 
852
        $serializedResult = $returnType->serialize($result);
Loading history...
852
853
        if ($serializedResult === null) {
854
            throw new ExecutionException(
855
                sprintf('Expected a value of type "%s" but received: %s', (string)$returnType, toString($result))
856
            );
857
        }
858
859
        return $serializedResult;
860
    }
861
862
    /**
863
     * @param ObjectType  $returnType
864
     * @param             $fieldNodes
865
     * @param ResolveInfo $info
866
     * @param             $path
867
     * @param             $result
868
     * @return array
869
     * @throws ExecutionException
870
     * @throws InvalidTypeException
871
     * @throws \Digia\GraphQL\Error\InvariantException
872
     * @throws \Throwable
873
     */
874
    private function completeObjectValue(
875
        ObjectType $returnType,
876
        $fieldNodes,
877
        ResolveInfo $info,
878
        $path,
879
        &$result
880
    ) {
881
        return $this->collectAndExecuteSubFields(
882
            $returnType,
883
            $fieldNodes,
884
            $info,
885
            $path,
886
            $result
887
        );
888
    }
889
890
    /**
891
     * @param Field            $field
892
     * @param FieldNode        $fieldNode
893
     * @param callable         $resolveFunction
894
     * @param                  $rootValue
895
     * @param ExecutionContext $context
896
     * @param ResolveInfo      $info
897
     * @return array|\Throwable
898
     */
899
    private function resolveFieldValueOrError(
900
        Field $field,
901
        FieldNode $fieldNode,
902
        ?callable $resolveFunction,
903
        $rootValue,
904
        ExecutionContext $context,
905
        ResolveInfo $info
906
    ) {
907
        try {
908
            $args = $this->valuesResolver->coerceArgumentValues($field, $fieldNode, $context->getVariableValues());
909
910
            return $resolveFunction($rootValue, $args, $context->getContextValue(), $info);
911
        } catch (\Throwable $error) {
912
            return $error;
913
        }
914
    }
915
916
    /**
917
     * Try to resolve a field without any field resolver function.
918
     *
919
     * @param array|object $rootValue
920
     * @param              $args
921
     * @param              $context
922
     * @param ResolveInfo  $info
923
     * @return mixed|null
924
     */
925
    public static function defaultFieldResolver($rootValue, $args, $context, ResolveInfo $info)
926
    {
927
        $fieldName = $info->getFieldName();
928
        $property  = null;
929
930
        if (is_array($rootValue) && isset($rootValue[$fieldName])) {
931
            $property = $rootValue[$fieldName];
932
        }
933
934
        if (is_object($rootValue)) {
935
            $getter = 'get' . ucfirst($fieldName);
936
            if (method_exists($rootValue, $getter)) {
937
                $property = $rootValue->{$getter}();
938
            } elseif (property_exists($rootValue, $fieldName)) {
939
                $property = $rootValue->{$fieldName};
940
            }
941
        }
942
943
944
        return $property instanceof \Closure ? $property($rootValue, $args, $context, $info) : $property;
945
    }
946
947
    /**
948
     * @param ObjectType  $returnType
949
     * @param             $fieldNodes
950
     * @param ResolveInfo $info
951
     * @param             $path
952
     * @param             $result
953
     * @return array
954
     * @throws InvalidTypeException
955
     * @throws \Digia\GraphQL\Error\ExecutionException
956
     * @throws \Digia\GraphQL\Error\InvariantException
957
     * @throws \Throwable
958
     */
959
    private function collectAndExecuteSubFields(
960
        ObjectType $returnType,
961
        $fieldNodes,
962
        ResolveInfo $info,
963
        $path,
964
        &$result
965
    ) {
966
        $subFields            = [];
967
        $visitedFragmentNames = [];
968
969
        foreach ($fieldNodes as $fieldNode) {
970
            /** @var FieldNode $fieldNode */
971
            if ($fieldNode->getSelectionSet() !== null) {
972
                $subFields = $this->collectFields(
973
                    $returnType,
974
                    $fieldNode->getSelectionSet(),
975
                    $subFields,
976
                    $visitedFragmentNames
977
                );
978
            }
979
        }
980
981
        if (!empty($subFields)) {
982
            return $this->executeFields($returnType, $result, $path, $subFields);
983
        }
984
985
        return $result;
986
    }
987
988
    /**
989
     * @param $value
990
     * @return bool
991
     */
992
    protected function isPromise($value): bool
993
    {
994
        return $value instanceof ExtendedPromiseInterface;
995
    }
996
997
    /**
998
     * @param \Exception   $originalException
999
     * @param array        $nodes
1000
     * @param string|array $path
1001
     * @return GraphQLException
1002
     */
1003
    protected function buildLocatedError(\Exception $originalException, array $nodes, $path): ExecutionException
1004
    {
1005
        return new ExecutionException(
1006
            $originalException->getMessage(),
1007
            $originalException instanceof GraphQLException ? $originalException->getNodes() : $nodes,
1008
            $originalException instanceof GraphQLException ? $originalException->getSource() : null,
1009
            $originalException instanceof GraphQLException ? $originalException->getPositions() : null,
1010
            $path,
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type string; however, parameter $path of Digia\GraphQL\Error\Exec...xception::__construct() does only seem to accept null|array, maybe add an additional type check? ( Ignorable by Annotation )

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

1010
            /** @scrutinizer ignore-type */ $path,
Loading history...
1011
            $originalException
1012
        );
1013
    }
1014
}
1015