Passed
Pull Request — master (#139)
by Christoffer
02:53
created

ExecutionStrategy::resolveFieldValueOrError()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 3
nop 6
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\Execution\Resolver\ResolveInfo;
10
use Digia\GraphQL\Language\Node\FieldNode;
11
use Digia\GraphQL\Language\Node\FragmentDefinitionNode;
12
use Digia\GraphQL\Language\Node\FragmentSpreadNode;
13
use Digia\GraphQL\Language\Node\InlineFragmentNode;
14
use Digia\GraphQL\Language\Node\NodeInterface;
15
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
16
use Digia\GraphQL\Language\Node\SelectionSetNode;
17
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
18
use Digia\GraphQL\Type\Definition\Field;
19
use Digia\GraphQL\Type\Definition\InterfaceType;
20
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
21
use Digia\GraphQL\Type\Definition\ListType;
22
use Digia\GraphQL\Type\Definition\NonNullType;
23
use Digia\GraphQL\Type\Definition\ObjectType;
24
use Digia\GraphQL\Type\Definition\TypeInterface;
25
use Digia\GraphQL\Type\Definition\UnionType;
26
use Digia\GraphQL\Type\Schema;
27
use Digia\GraphQL\Util\ValueNodeCoercer;
28
use React\Promise\ExtendedPromiseInterface;
29
use React\Promise\PromiseInterface;
30
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
31
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
32
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
33
use function Digia\GraphQL\Util\toString;
34
use function Digia\GraphQL\Util\typeFromAST;
35
36
/**
37
 * Class AbstractStrategy
38
 * @package Digia\GraphQL\Execution\Strategies
39
 */
40
abstract class ExecutionStrategy
41
{
42
    /**
43
     * @var ExecutionContext
44
     */
45
    protected $context;
46
47
    /**
48
     * @var OperationDefinitionNode
49
     */
50
    protected $operation;
51
52
    /**
53
     * @var mixed
54
     */
55
    protected $rootValue;
56
57
58
    /**
59
     * @var ValuesResolver
60
     */
61
    protected $valuesResolver;
62
63
    /**
64
     * @var array
65
     */
66
    protected $finalResult;
67
68
    /**
69
     * @var array
70
     */
71
    protected static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
72
73
    /**
74
     * AbstractStrategy constructor.
75
     * @param ExecutionContext        $context
76
     *
77
     * @param OperationDefinitionNode $operation
78
     */
79
    public function __construct(
80
        ExecutionContext $context,
81
        OperationDefinitionNode $operation,
82
        $rootValue
83
    ) {
84
        $this->context   = $context;
85
        $this->operation = $operation;
86
        $this->rootValue = $rootValue;
87
        // TODO: Inject the ValuesResolver instance by using a builder for this class.
88
        $this->valuesResolver = new ValuesResolver(new ValueNodeCoercer());
89
    }
90
91
    /**
92
     * @return array|null
93
     */
94
    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...
95
96
    /**
97
     * @param ObjectType       $runtimeType
98
     * @param SelectionSetNode $selectionSet
99
     * @param                  $fields
100
     * @param                  $visitedFragmentNames
101
     * @return mixed
102
     * @throws InvalidTypeException
103
     * @throws \Digia\GraphQL\Error\ExecutionException
104
     * @throws \Digia\GraphQL\Error\InvariantException
105
     */
106
    protected function collectFields(
107
        ObjectType $runtimeType,
108
        SelectionSetNode $selectionSet,
109
        &$fields,
110
        &$visitedFragmentNames
111
    ) {
112
        foreach ($selectionSet->getSelections() as $selection) {
113
            // Check if this Node should be included first
114
            if (!$this->shouldIncludeNode($selection)) {
115
                continue;
116
            }
117
            // Collect fields
118
            if ($selection instanceof FieldNode) {
119
                $fieldName = $this->getFieldNameKey($selection);
120
121
                if (!isset($fields[$fieldName])) {
122
                    $fields[$fieldName] = [];
123
                }
124
125
                $fields[$fieldName][] = $selection;
126
            } elseif ($selection instanceof InlineFragmentNode) {
127
                if (!$this->doesFragmentConditionMatch($selection, $runtimeType)) {
128
                    continue;
129
                }
130
131
                $this->collectFields($runtimeType, $selection->getSelectionSet(), $fields, $visitedFragmentNames);
132
            } elseif ($selection instanceof FragmentSpreadNode) {
133
                $fragmentName = $selection->getNameValue();
134
135
                if (!empty($visitedFragmentNames[$fragmentName])) {
136
                    continue;
137
                }
138
139
                $visitedFragmentNames[$fragmentName] = true;
140
                /** @var FragmentDefinitionNode $fragment */
141
                $fragment = $this->context->getFragments()[$fragmentName];
142
                $this->collectFields($runtimeType, $fragment->getSelectionSet(), $fields, $visitedFragmentNames);
143
            }
144
        }
145
146
        return $fields;
147
    }
148
149
150
    /**
151
     * @param $node
152
     * @return bool
153
     * @throws InvalidTypeException
154
     * @throws \Digia\GraphQL\Error\ExecutionException
155
     * @throws \Digia\GraphQL\Error\InvariantException
156
     */
157
    private function shouldIncludeNode(NodeInterface $node): bool
158
    {
159
160
        $contextVariables = $this->context->getVariableValues();
161
162
        $skip = $this->valuesResolver->getDirectiveValues(GraphQLSkipDirective(), $node, $contextVariables);
163
164
        if ($skip && $skip['if'] === true) {
165
            return false;
166
        }
167
168
        $include = $this->valuesResolver->getDirectiveValues(GraphQLIncludeDirective(), $node, $contextVariables);
169
170
        if ($include && $include['if'] === false) {
171
            return false;
172
        }
173
174
        return true;
175
    }
176
177
    /**
178
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
179
     * @param ObjectType                                $type
180
     * @return bool
181
     * @throws InvalidTypeException
182
     */
183
    private function doesFragmentConditionMatch(
184
        NodeInterface $fragment,
185
        ObjectType $type
186
    ): bool {
187
        $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

187
        /** @scrutinizer ignore-call */ 
188
        $typeConditionNode = $fragment->getTypeCondition();
Loading history...
188
189
        if (!$typeConditionNode) {
190
            return true;
191
        }
192
193
        $conditionalType = typeFromAST($this->context->getSchema(), $typeConditionNode);
194
195
        if ($conditionalType === $type) {
196
            return true;
197
        }
198
199
        if ($conditionalType instanceof AbstractTypeInterface) {
200
            return $this->context->getSchema()->isPossibleType($conditionalType, $type);
201
        }
202
203
        return false;
204
    }
205
206
    /**
207
     * @TODO: consider to move this to FieldNode
208
     * @param FieldNode $node
209
     * @return string
210
     */
211
    private function getFieldNameKey(FieldNode $node): string
212
    {
213
        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...
214
    }
215
216
    /**
217
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
218
     * @param ObjectType $objectType
219
     * @param            $rootValue
220
     * @param            $path
221
     * @param            $fields
222
     * @return array
223
     * @throws InvalidTypeException
224
     * @throws \Digia\GraphQL\Error\ExecutionException
225
     * @throws \Digia\GraphQL\Error\InvariantException
226
     * @throws \Throwable
227
     */
228
    protected function executeFields(
229
        ObjectType $objectType,
230
        $rootValue,
231
        $path,
232
        $fields
233
    ): array {
234
        $finalResults      = [];
235
        $isContainsPromise = false;
236
237
        foreach ($fields as $fieldName => $fieldNodes) {
238
            $fieldPath   = $path;
239
            $fieldPath[] = $fieldName;
240
241
            try {
242
                $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...
243
            } catch (UndefinedException $ex) {
244
                continue;
245
            }
246
247
            $isContainsPromise = $isContainsPromise || $this->isPromise($result);
248
249
            $finalResults[$fieldName] = $result;
250
        }
251
252
        if ($isContainsPromise) {
253
            $keys    = array_keys($finalResults);
254
            $promise = \React\Promise\all(array_values($finalResults));
255
            $promise->then(function ($values) use ($keys, &$finalResults) {
256
                foreach ($values as $i => $value) {
257
                    $finalResults[$keys[$i]] = $value;
258
                }
259
            });
260
        }
261
262
        return $finalResults;
263
    }
264
265
    /**
266
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
267
     *
268
     * @param ObjectType $objectType
269
     * @param            $rootValue
270
     * @param            $path
271
     * @param            $fields
272
     * @return array
273
     * @throws InvalidTypeException
274
     * @throws \Digia\GraphQL\Error\ExecutionException
275
     * @throws \Digia\GraphQL\Error\InvariantException
276
     * @throws \Throwable
277
     */
278
    public function executeFieldsSerially(
279
        ObjectType $objectType,
280
        $rootValue,
281
        $path,
282
        $fields
283
    ) {
284
        //@TODO execute fields serially
285
        $finalResults = [];
286
287
        foreach ($fields as $fieldName => $fieldNodes) {
288
            $fieldPath   = $path;
289
            $fieldPath[] = $fieldName;
290
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
                continue;
295
            }
296
297
            $finalResults[$fieldName] = $result;
298
        }
299
300
        return $finalResults;
301
    }
302
303
    /**
304
     * @param Schema     $schema
305
     * @param ObjectType $parentType
306
     * @param string     $fieldName
307
     * @return \Digia\GraphQL\Type\Definition\Field|null
308
     * @throws InvalidTypeException
309
     */
310
    public function getFieldDefinition(
311
        Schema $schema,
312
        ObjectType $parentType,
313
        string $fieldName
314
    ) {
315
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
316
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
317
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
318
319
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
320
            return $schemaMetaFieldDefinition;
321
        }
322
323
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
324
            return $typeMetaFieldDefinition;
325
        }
326
327
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
328
            return $typeNameMetaFieldDefinition;
329
        }
330
331
        $fields = $parentType->getFields();
332
333
        return $fields[$fieldName] ?? null;
334
    }
335
336
337
    /**
338
     * @param ObjectType $parentType
339
     * @param            $rootValue
340
     * @param            $fieldNodes
341
     * @param            $path
342
     * @return array|null|\Throwable
343
     * @throws InvalidTypeException
344
     * @throws \Digia\GraphQL\Error\ExecutionException
345
     * @throws \Digia\GraphQL\Error\InvariantException
346
     * @throws \Throwable
347
     */
348
    protected function resolveField(
349
        ObjectType $parentType,
350
        $rootValue,
351
        $fieldNodes,
352
        $path
353
    ) {
354
        /** @var FieldNode $fieldNode */
355
        $fieldNode = $fieldNodes[0];
356
357
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
358
359
        if (null === $field) {
360
            throw new UndefinedException('Undefined field definition.');
361
        }
362
363
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
364
365
        $resolveFunction = $this->determineResolveFunction($field, $parentType, $this->context);
366
367
        $result = $this->resolveFieldValueOrError(
368
            $field,
369
            $fieldNode,
370
            $resolveFunction,
371
            $rootValue,
372
            $this->context,
373
            $info
374
        );
375
376
        $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...
377
            $field->getType(),
378
            $fieldNodes,
379
            $info,
380
            $path,
381
            $result// $result is passed as $source
382
        );
383
384
        return $result;
385
    }
386
387
    /**
388
     * @param array            $fieldNodes
389
     * @param FieldNode        $fieldNode
390
     * @param Field            $field
391
     * @param ObjectType       $parentType
392
     * @param                  $path
393
     * @param ExecutionContext $context
394
     * @return ResolveInfo
395
     */
396
    private function buildResolveInfo(
397
        array $fieldNodes,
398
        FieldNode $fieldNode,
399
        Field $field,
400
        ObjectType $parentType,
401
        $path,
402
        ExecutionContext $context
403
    ) {
404
        return new ResolveInfo([
405
            'fieldName'      => $fieldNode->getNameValue(),
406
            'fieldNodes'     => $fieldNodes,
407
            'returnType'     => $field->getType(),
408
            'parentType'     => $parentType,
409
            'path'           => $path,
410
            'schema'         => $context->getSchema(),
411
            'fragments'      => $context->getFragments(),
412
            'rootValue'      => $context->getRootValue(),
413
            'operation'      => $context->getOperation(),
414
            'variableValues' => $context->getVariableValues(),
415
        ]);
416
    }
417
418
    /**
419
     * @param Field            $field
420
     * @param ObjectType       $objectType
421
     * @param ExecutionContext $context
422
     * @return callable|mixed|null
423
     */
424
    private function determineResolveFunction(
425
        Field $field,
426
        ObjectType $objectType,
427
        ExecutionContext $context
428
    ) {
429
430
        if ($field->hasResolve()) {
431
            return $field->getResolve();
432
        }
433
434
        if ($objectType->hasResolve()) {
435
            return $objectType->getResolve();
436
        }
437
438
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
439
    }
440
441
    /**
442
     * @param TypeInterface $fieldType
443
     * @param               $fieldNodes
444
     * @param ResolveInfo   $info
445
     * @param               $path
446
     * @param               $result
447
     * @return null
448
     * @throws \Throwable
449
     */
450
    public function completeValueCatchingError(
451
        TypeInterface $fieldType,
452
        $fieldNodes,
453
        ResolveInfo $info,
454
        $path,
455
        &$result
456
    ) {
457
        if ($fieldType instanceof NonNullType) {
458
            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...
459
                $fieldType,
460
                $fieldNodes,
461
                $info,
462
                $path,
463
                $result
464
            );
465
        }
466
467
        try {
468
            $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...
469
                $fieldType,
470
                $fieldNodes,
471
                $info,
472
                $path,
473
                $result
474
            );
475
476
            if ($this->isPromise($completed)) {
477
                $context = $this->context;
478
                /** @var ExtendedPromiseInterface $completed */
479
                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...
480
                    $context->addError($error);
481
                    return new \React\Promise\FulfilledPromise(null);
482
                });
483
            }
484
485
            return $completed;
486
        } catch (ExecutionException $ex) {
487
            $this->context->addError($ex);
488
            return null;
489
        } catch (\Exception $ex) {
490
            $this->context->addError(new ExecutionException($ex->getMessage()));
491
            return null;
492
        }
493
    }
494
495
    /**
496
     * @param TypeInterface $fieldType
497
     * @param               $fieldNodes
498
     * @param ResolveInfo   $info
499
     * @param               $path
500
     * @param               $result
501
     * @throws \Throwable
502
     */
503
    public function completeValueWithLocatedError(
504
        TypeInterface $fieldType,
505
        $fieldNodes,
506
        ResolveInfo $info,
507
        $path,
508
        $result
509
    ) {
510
        try {
511
            $completed = $this->completeValue(
512
                $fieldType,
513
                $fieldNodes,
514
                $info,
515
                $path,
516
                $result
517
            );
518
519
            return $completed;
520
        } catch (\Exception $ex) {
521
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
522
        } catch (\Throwable $ex) {
523
            throw $ex;
524
        }
525
    }
526
527
    /**
528
     * @param TypeInterface $returnType
529
     * @param               $fieldNodes
530
     * @param ResolveInfo   $info
531
     * @param               $path
532
     * @param               $result
533
     * @return array|mixed
534
     * @throws ExecutionException
535
     * @throws \Throwable
536
     */
537
    private function completeValue(
538
        TypeInterface $returnType,
539
        $fieldNodes,
540
        ResolveInfo $info,
541
        $path,
542
        &$result
543
    ) {
544
        if ($this->isPromise($result)) {
545
            /** @var ExtendedPromiseInterface $result */
546
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
547
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
548
            });
549
        }
550
551
        if ($result instanceof \Throwable) {
552
            throw $result;
553
        }
554
555
        // If result is null-like, return null.
556
        if (null === $result) {
557
            return null;
558
        }
559
560
        if ($returnType instanceof NonNullType) {
561
            $completed = $this->completeValue(
562
                $returnType->getOfType(),
563
                $fieldNodes,
564
                $info,
565
                $path,
566
                $result
567
            );
568
569
            if ($completed === null) {
570
                throw new ExecutionException(
571
                    sprintf(
572
                        'Cannot return null for non-nullable field %s.%s.',
573
                        $info->getParentType(), $info->getFieldName()
574
                    )
575
                );
576
            }
577
578
            return $completed;
579
        }
580
581
        // If field type is List, complete each item in the list with the inner type
582
        if ($returnType instanceof ListType) {
583
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
584
        }
585
586
587
        // If field type is Scalar or Enum, serialize to a valid value, returning
588
        // null if serialization is not possible.
589
        if ($returnType instanceof LeafTypeInterface) {
590
            return $this->completeLeafValue($returnType, $result);
591
        }
592
593
        //@TODO Make a function for checking abstract type?
594
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
595
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
596
        }
597
598
        // Field type must be Object, Interface or Union and expect sub-selections.
599
        if ($returnType instanceof ObjectType) {
600
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
601
        }
602
603
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
604
    }
605
606
    /**
607
     * @param AbstractTypeInterface $returnType
608
     * @param                       $fieldNodes
609
     * @param ResolveInfo           $info
610
     * @param                       $path
611
     * @param                       $result
612
     * @return array|PromiseInterface
613
     * @throws ExecutionException
614
     * @throws InvalidTypeException
615
     * @throws \Digia\GraphQL\Error\InvariantException
616
     * @throws \Throwable
617
     */
618
    private function completeAbstractValue(
619
        AbstractTypeInterface $returnType,
620
        $fieldNodes,
621
        ResolveInfo $info,
622
        $path,
623
        &$result
624
    ) {
625
        $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

625
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
626
627
        if (null === $runtimeType) {
628
            //@TODO Show warning
629
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
630
        }
631
632
        if ($this->isPromise($runtimeType)) {
633
            /** @var ExtendedPromiseInterface $runtimeType */
634
            return $runtimeType->then(function ($resolvedRuntimeType) use (
635
                $returnType,
636
                $fieldNodes,
637
                $info,
638
                $path,
639
                &$result
640
            ) {
641
                return $this->completeObjectValue(
642
                    $this->ensureValidRuntimeType(
643
                        $resolvedRuntimeType,
644
                        $returnType,
645
                        $fieldNodes,
646
                        $info,
647
                        $result
648
                    ),
649
                    $fieldNodes,
650
                    $info,
651
                    $path,
652
                    $result
653
                );
654
            });
655
        }
656
657
        return $this->completeObjectValue(
658
            $this->ensureValidRuntimeType(
659
                $runtimeType,
660
                $returnType,
661
                $fieldNodes,
662
                $info,
663
                $result
664
            ),
665
            $fieldNodes,
666
            $info,
667
            $path,
668
            $result
669
        );
670
    }
671
672
    /**
673
     * @param                       $runtimeTypeOrName
674
     * @param AbstractTypeInterface $returnType
675
     * @param                       $fieldNodes
676
     * @param ResolveInfo           $info
677
     * @param                       $result
678
     * @return TypeInterface|ObjectType|null
679
     * @throws ExecutionException
680
     */
681
    private function ensureValidRuntimeType(
682
        $runtimeTypeOrName,
683
        AbstractTypeInterface $returnType,
684
        $fieldNodes,
685
        ResolveInfo $info,
686
        &$result
687
    ) {
688
        $runtimeType = is_string($runtimeTypeOrName)
689
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
690
            : $runtimeTypeOrName;
691
692
        $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

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

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

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

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

970
            /** @scrutinizer ignore-type */ $path,
Loading history...
971
            $originalException
972
        );
973
    }
974
}
975