Passed
Push — master ( 8c2102...5de2c3 )
by Quang
02:40
created

ExecutionStrategy::shouldIncludeNode()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

850
        /** @scrutinizer ignore-call */ 
851
        $serializedResult = $returnType->serialize($result);
Loading history...
851
852
        if ($serializedResult === null) {
853
            //@TODO Make a function for this type of exception
854
            throw new ExecutionException(
855
                sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
856
            );
857
        }
858
859
        return $serializedResult;
860
    }
861
862
    /**
863
     * @param ObjectType  $returnType
864
     * @param             $fieldNodes
865
     * @param ResolveInfo $info
866
     * @param             $path
867
     * @param             $result
868
     * @return array
869
     * @throws ExecutionException
870
     * @throws InvalidTypeException
871
     * @throws \Digia\GraphQL\Error\InvariantException
872
     * @throws \Throwable
873
     */
874
    private function completeObjectValue(
875
        ObjectType $returnType,
876
        $fieldNodes,
877
        ResolveInfo $info,
878
        $path,
879
        &$result
880
    ) {
881
882
        if (null !== $returnType->getIsTypeOf()) {
883
            $isTypeOf = $returnType->isTypeOf($result, $this->context->getContextValue(), $info);
884
            //@TODO check for promise?
885
            if (!$isTypeOf) {
886
                throw new ExecutionException(
887
                    sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
888
                );
889
            }
890
        }
891
892
        return $this->collectAndExecuteSubFields(
893
            $returnType,
894
            $fieldNodes,
895
            $info,
896
            $path,
897
            $result
898
        );
899
    }
900
901
    /**
902
     * @param Field            $field
903
     * @param FieldNode        $fieldNode
904
     * @param callable         $resolveFunction
905
     * @param                  $rootValue
906
     * @param ExecutionContext $context
907
     * @param ResolveInfo      $info
908
     * @return array|\Throwable
909
     */
910
    private function resolveFieldValueOrError(
911
        Field $field,
912
        FieldNode $fieldNode,
913
        ?callable $resolveFunction,
914
        $rootValue,
915
        ExecutionContext $context,
916
        ResolveInfo $info
917
    ) {
918
        try {
919
            $args = coerceArgumentValues($field, $fieldNode, $context->getVariableValues());
920
921
            return $resolveFunction($rootValue, $args, $context->getContextValue(), $info);
922
        } catch (\Throwable $error) {
923
            return $error;
924
        }
925
    }
926
927
    /**
928
     * Try to resolve a field without any field resolver function.
929
     *
930
     * @param array|object $rootValue
931
     * @param              $args
932
     * @param              $context
933
     * @param ResolveInfo  $info
934
     * @return mixed|null
935
     */
936
    public static function defaultFieldResolver($rootValue, $args, $context, ResolveInfo $info)
937
    {
938
        $fieldName = $info->getFieldName();
939
        $property  = null;
940
941
        if (is_array($rootValue) && isset($rootValue[$fieldName])) {
942
            $property = $rootValue[$fieldName];
943
        }
944
945
        if (is_object($rootValue)) {
946
            $getter = 'get' . ucfirst($fieldName);
947
            if (method_exists($rootValue, $getter)) {
948
                $property = $rootValue->{$getter}();
949
            } elseif (method_exists($rootValue, $fieldName)) {
950
                $property = $rootValue->{$fieldName}($rootValue, $args, $context, $info);
951
            } elseif (property_exists($rootValue, $fieldName)) {
952
                $property = $rootValue->{$fieldName};
953
            }
954
        }
955
956
957
        return $property instanceof \Closure ? $property($rootValue, $args, $context, $info) : $property;
958
    }
959
960
    /**
961
     * @param ObjectType  $returnType
962
     * @param             $fieldNodes
963
     * @param ResolveInfo $info
964
     * @param             $path
965
     * @param             $result
966
     * @return array
967
     * @throws InvalidTypeException
968
     * @throws \Digia\GraphQL\Error\ExecutionException
969
     * @throws \Digia\GraphQL\Error\InvariantException
970
     * @throws \Throwable
971
     */
972
    private function collectAndExecuteSubFields(
973
        ObjectType $returnType,
974
        $fieldNodes,
975
        ResolveInfo $info,
976
        $path,
977
        &$result
978
    ) {
979
        $subFields            = [];
980
        $visitedFragmentNames = [];
981
982
        foreach ($fieldNodes as $fieldNode) {
983
            /** @var FieldNode $fieldNode */
984
            if ($fieldNode->getSelectionSet() !== null) {
985
                $subFields = $this->collectFields(
986
                    $returnType,
987
                    $fieldNode->getSelectionSet(),
988
                    $subFields,
989
                    $visitedFragmentNames
990
                );
991
            }
992
        }
993
994
        if (!empty($subFields)) {
995
            return $this->executeFields($returnType, $result, $path, $subFields);
996
        }
997
998
        return $result;
999
    }
1000
1001
    /**
1002
     * @param $value
1003
     * @return bool
1004
     */
1005
    protected function isPromise($value): bool
1006
    {
1007
        return $value instanceof ExtendedPromiseInterface;
1008
    }
1009
1010
    /**
1011
     * @param \Throwable   $originalException
1012
     * @param array        $nodes
1013
     * @param string|array $path
1014
     * @return GraphQLException
1015
     */
1016
    protected function buildLocatedError(
1017
        \Throwable $originalException,
1018
        array $nodes = [],
1019
        array $path = []
1020
    ): ExecutionException {
1021
        return new ExecutionException(
1022
            $originalException->getMessage(),
1023
            $originalException instanceof GraphQLException ? $originalException->getNodes() : $nodes,
1024
            $originalException instanceof GraphQLException ? $originalException->getSource() : null,
1025
            $originalException instanceof GraphQLException ? $originalException->getPositions() : null,
1026
            $originalException instanceof GraphQLException ? ($originalException->getPath() ?? $path) : $path,
1027
            $originalException
1028
        );
1029
    }
1030
}
1031