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

ExecutionStrategy::completeListValue()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.6737
c 0
b 0
f 0
cc 5
eloc 11
nc 6
nop 5
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 React\Promise\Promise|ar...romise\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
            return $completed;
476
        } catch (\Exception $ex) {
477
            $this->context->addError(new ExecutionException($ex->getMessage()));
478
            return null;
479
        }
480
    }
481
482
    /**
483
     * @param TypeInterface $fieldType
484
     * @param               $fieldNodes
485
     * @param ResolveInfo   $info
486
     * @param               $path
487
     * @param               $result
488
     * @throws \Throwable
489
     */
490
    public function completeValueWithLocatedError(
491
        TypeInterface $fieldType,
492
        $fieldNodes,
493
        ResolveInfo $info,
494
        $path,
495
        $result
496
    ) {
497
        try {
498
            $completed = $this->completeValue(
499
                $fieldType,
500
                $fieldNodes,
501
                $info,
502
                $path,
503
                $result
504
            );
505
            return $completed;
506
        } catch (\Exception $ex) {
507
            //@TODO throw located error
508
            throw $ex;
509
        } catch (\Throwable $ex) {
510
            //@TODO throw located error
511
            throw $ex;
512
        }
513
    }
514
515
    /**
516
     * @param TypeInterface $returnType
517
     * @param               $fieldNodes
518
     * @param ResolveInfo   $info
519
     * @param               $path
520
     * @param               $result
521
     * @return array|mixed
522
     * @throws ExecutionException
523
     * @throws \Throwable
524
     */
525
    private function completeValue(
526
        TypeInterface $returnType,
527
        $fieldNodes,
528
        ResolveInfo $info,
529
        $path,
530
        &$result
531
    ) {
532
        if ($this->isPromise($result)) {
533
            /** @var ExtendedPromiseInterface $result */
534
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
535
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
536
            });
537
        }
538
539
        if ($result instanceof \Throwable) {
540
            throw $result;
541
        }
542
543
        // If result is null-like, return null.
544
        if (null === $result) {
545
            return null;
546
        }
547
548
        if ($returnType instanceof NonNullType) {
549
            $completed = $this->completeValue(
550
                $returnType->getOfType(),
551
                $fieldNodes,
552
                $info,
553
                $path,
554
                $result
555
            );
556
557
            if ($completed === null) {
558
                throw new ExecutionException(
559
                    sprintf(
560
                        'Cannot return null for non-nullable field %s.%s.',
561
                        $info->getParentType(), $info->getFieldName()
562
                    )
563
                );
564
            }
565
566
            return $completed;
567
        }
568
569
        // If field type is List, complete each item in the list with the inner type
570
        if ($returnType instanceof ListType) {
571
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
572
        }
573
574
575
        // If field type is Scalar or Enum, serialize to a valid value, returning
576
        // null if serialization is not possible.
577
        if ($returnType instanceof LeafTypeInterface) {
578
            return $this->completeLeafValue($returnType, $result);
579
        }
580
581
        //@TODO Make a function for checking abstract type?
582
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
583
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
584
        }
585
586
        // Field type must be Object, Interface or Union and expect sub-selections.
587
        if ($returnType instanceof ObjectType) {
588
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
589
        }
590
591
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
592
    }
593
594
    /**
595
     * @param AbstractTypeInterface $returnType
596
     * @param                       $fieldNodes
597
     * @param ResolveInfo           $info
598
     * @param                       $path
599
     * @param                       $result
600
     * @return array|PromiseInterface
601
     * @throws ExecutionException
602
     * @throws InvalidTypeException
603
     * @throws \Digia\GraphQL\Error\InvariantException
604
     * @throws \Throwable
605
     */
606
    private function completeAbstractValue(
607
        AbstractTypeInterface $returnType,
608
        $fieldNodes,
609
        ResolveInfo $info,
610
        $path,
611
        &$result
612
    ) {
613
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
614
615
        if (null === $runtimeType) {
616
            //@TODO Show warning
617
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
618
        }
619
620
        if ($this->isPromise($runtimeType)) {
621
            /** @var PromiseInterface $runtimeType */
622
            return $runtimeType->then(function ($resolvedRuntimeType) use (
623
                $returnType,
624
                $fieldNodes,
625
                $info,
626
                $path,
627
                &
628
                $result
629
            ) {
630
                return $this->completeObjectValue(
631
                    $this->ensureValidRuntimeType(
632
                        $resolvedRuntimeType,
633
                        $returnType,
634
                        $fieldNodes,
635
                        $info,
636
                        $result
637
                    ),
638
                    $fieldNodes,
639
                    $info,
640
                    $path,
641
                    $result
642
                );
643
            });
644
        }
645
646
        return $this->completeObjectValue(
647
            $this->ensureValidRuntimeType(
648
                $runtimeType,
649
                $returnType,
650
                $fieldNodes,
651
                $info,
652
                $result
653
            ),
654
            $fieldNodes,
655
            $info,
656
            $path,
657
            $result
658
        );
659
    }
660
661
    /**
662
     * @param                       $runtimeTypeOrName
663
     * @param AbstractTypeInterface $returnType
664
     * @param                       $fieldNodes
665
     * @param ResolveInfo           $info
666
     * @param                       $result
667
     * @return TypeInterface|ObjectType|null
668
     * @throws ExecutionException
669
     */
670
    private function ensureValidRuntimeType(
671
        $runtimeTypeOrName,
672
        AbstractTypeInterface $returnType,
673
        $fieldNodes,
674
        ResolveInfo $info,
675
        &$result
676
    ) {
677
        $runtimeType = is_string($runtimeTypeOrName)
678
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
679
            : $runtimeTypeOrName;
680
681
        $runtimeTypeName = is_string($runtimeType) ?: $runtimeType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

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

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

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