Passed
Push — master ( c6bcad...379cff )
by Quang
02:40
created

ExecutionStrategy::ensureValidRuntimeType()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

712
        $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...
713
        $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

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

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