Completed
Pull Request — master (#139)
by Christoffer
04:59 queued 02:15
created

ExecutionStrategy::completeValueWithLocatedError()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
cc 3
eloc 12
nc 3
nop 5
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\ExecutionException;
6
use Digia\GraphQL\Error\GraphQLException;
7
use Digia\GraphQL\Error\InvalidTypeException;
8
use Digia\GraphQL\Error\UndefinedException;
9
use Digia\GraphQL\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
            //@TODO throw located error
524
            throw $ex;
525
        }
526
    }
527
528
    /**
529
     * @param TypeInterface $returnType
530
     * @param               $fieldNodes
531
     * @param ResolveInfo   $info
532
     * @param               $path
533
     * @param               $result
534
     * @return array|mixed
535
     * @throws ExecutionException
536
     * @throws \Throwable
537
     */
538
    private function completeValue(
539
        TypeInterface $returnType,
540
        $fieldNodes,
541
        ResolveInfo $info,
542
        $path,
543
        &$result
544
    ) {
545
        if ($this->isPromise($result)) {
546
            /** @var ExtendedPromiseInterface $result */
547
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
548
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
549
            });
550
        }
551
552
        if ($result instanceof \Throwable) {
553
            throw $result;
554
        }
555
556
        // If result is null-like, return null.
557
        if (null === $result) {
558
            return null;
559
        }
560
561
        if ($returnType instanceof NonNullType) {
562
            $completed = $this->completeValue(
563
                $returnType->getOfType(),
564
                $fieldNodes,
565
                $info,
566
                $path,
567
                $result
568
            );
569
570
            if ($completed === null) {
571
                throw new ExecutionException(
572
                    sprintf(
573
                        'Cannot return null for non-nullable field %s.%s.',
574
                        $info->getParentType(), $info->getFieldName()
575
                    )
576
                );
577
            }
578
579
            return $completed;
580
        }
581
582
        // If field type is List, complete each item in the list with the inner type
583
        if ($returnType instanceof ListType) {
584
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
585
        }
586
587
588
        // If field type is Scalar or Enum, serialize to a valid value, returning
589
        // null if serialization is not possible.
590
        if ($returnType instanceof LeafTypeInterface) {
591
            return $this->completeLeafValue($returnType, $result);
592
        }
593
594
        //@TODO Make a function for checking abstract type?
595
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
596
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
597
        }
598
599
        // Field type must be Object, Interface or Union and expect sub-selections.
600
        if ($returnType instanceof ObjectType) {
601
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
602
        }
603
604
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
605
    }
606
607
    /**
608
     * @param AbstractTypeInterface $returnType
609
     * @param                       $fieldNodes
610
     * @param ResolveInfo           $info
611
     * @param                       $path
612
     * @param                       $result
613
     * @return array|PromiseInterface
614
     * @throws ExecutionException
615
     * @throws InvalidTypeException
616
     * @throws \Digia\GraphQL\Error\InvariantException
617
     * @throws \Throwable
618
     */
619
    private function completeAbstractValue(
620
        AbstractTypeInterface $returnType,
621
        $fieldNodes,
622
        ResolveInfo $info,
623
        $path,
624
        &$result
625
    ) {
626
        $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

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

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

693
        $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...
694
        $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

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

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

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