Passed
Push — master ( 379cff...b86485 )
by Christoffer
02:35
created

ExecutionStrategy::buildLocatedError()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

993
            /** @scrutinizer ignore-type */ $path,
Loading history...
994
            $originalException
995
        );
996
    }
997
}
998