Passed
Push — master ( 341e25...abc3c7 )
by Quang
02:57
created

ExecutionStrategy::getFieldNameKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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\Type\Definition\AbstractTypeInterface;
17
use Digia\GraphQL\Type\Definition\Field;
18
use Digia\GraphQL\Type\Definition\InterfaceType;
19
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
20
use Digia\GraphQL\Type\Definition\ListType;
21
use Digia\GraphQL\Type\Definition\NonNullType;
22
use Digia\GraphQL\Type\Definition\ObjectType;
23
use Digia\GraphQL\Type\Definition\TypeInterface;
24
use Digia\GraphQL\Type\Definition\UnionType;
25
use Digia\GraphQL\Type\Schema;
26
use Digia\GraphQL\Util\ValueNodeResolver;
27
use React\Promise\ExtendedPromiseInterface;
28
use React\Promise\PromiseInterface;
29
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
30
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
31
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
32
use function Digia\GraphQL\Util\toString;
33
use function Digia\GraphQL\Util\typeFromAST;
34
35
/**
36
 * Class AbstractStrategy
37
 * @package Digia\GraphQL\Execution\Strategies
38
 */
39
abstract class ExecutionStrategy
40
{
41
    /**
42
     * @var ExecutionContext
43
     */
44
    protected $context;
45
46
    /**
47
     * @var OperationDefinitionNode
48
     */
49
    protected $operation;
50
51
    /**
52
     * @var mixed
53
     */
54
    protected $rootValue;
55
56
57
    /**
58
     * @var ValuesResolver
59
     */
60
    protected $valuesResolver;
61
62
    /**
63
     * @var array
64
     */
65
    protected $finalResult;
66
67
    /**
68
     * @var array
69
     */
70
    protected static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
71
72
    /**
73
     * AbstractStrategy constructor.
74
     * @param ExecutionContext        $context
75
     *
76
     * @param OperationDefinitionNode $operation
77
     */
78
    public function __construct(
79
        ExecutionContext $context,
80
        OperationDefinitionNode $operation,
81
        $rootValue
82
    ) {
83
        $this->context   = $context;
84
        $this->operation = $operation;
85
        $this->rootValue = $rootValue;
86
        // TODO: Inject the ValuesResolver instance by using a builder for this class.
87
        $this->valuesResolver = new ValuesResolver(new ValueNodeResolver());
88
    }
89
90
    /**
91
     * @return array|null
92
     */
93
    abstract function execute(): ?array;
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
94
95
    /**
96
     * @param ObjectType       $runtimeType
97
     * @param SelectionSetNode $selectionSet
98
     * @param                  $fields
99
     * @param                  $visitedFragmentNames
100
     * @return mixed
101
     * @throws InvalidTypeException
102
     * @throws \Digia\GraphQL\Error\ExecutionException
103
     * @throws \Digia\GraphQL\Error\InvariantException
104
     */
105
    protected function collectFields(
106
        ObjectType $runtimeType,
107
        SelectionSetNode $selectionSet,
108
        &$fields,
109
        &$visitedFragmentNames
110
    ) {
111
        foreach ($selectionSet->getSelections() as $selection) {
112
            // Check if this Node should be included first
113
            if (!$this->shouldIncludeNode($selection)) {
114
                continue;
115
            }
116
            // Collect fields
117
            if ($selection instanceof FieldNode) {
118
                $fieldName = $this->getFieldNameKey($selection);
119
120
                if (!isset($fields[$fieldName])) {
121
                    $fields[$fieldName] = [];
122
                }
123
124
                $fields[$fieldName][] = $selection;
125
            } elseif ($selection instanceof InlineFragmentNode) {
126
                if (!$this->doesFragmentConditionMatch($selection, $runtimeType)) {
127
                    continue;
128
                }
129
130
                $this->collectFields($runtimeType, $selection->getSelectionSet(), $fields, $visitedFragmentNames);
131
            } elseif ($selection instanceof FragmentSpreadNode) {
132
                $fragmentName = $selection->getNameValue();
133
134
                if (!empty($visitedFragmentNames[$fragmentName])) {
135
                    continue;
136
                }
137
138
                $visitedFragmentNames[$fragmentName] = true;
139
                /** @var FragmentDefinitionNode $fragment */
140
                $fragment = $this->context->getFragments()[$fragmentName];
141
                $this->collectFields($runtimeType, $fragment->getSelectionSet(), $fields, $visitedFragmentNames);
142
            }
143
        }
144
145
        return $fields;
146
    }
147
148
149
    /**
150
     * @param $node
151
     * @return bool
152
     * @throws InvalidTypeException
153
     * @throws \Digia\GraphQL\Error\ExecutionException
154
     * @throws \Digia\GraphQL\Error\InvariantException
155
     */
156
    private function shouldIncludeNode(NodeInterface $node): bool
157
    {
158
159
        $contextVariables = $this->context->getVariableValues();
160
161
        $skip = $this->valuesResolver->getDirectiveValues(GraphQLSkipDirective(), $node, $contextVariables);
162
163
        if ($skip && $skip['if'] === true) {
164
            return false;
165
        }
166
167
        $include = $this->valuesResolver->getDirectiveValues(GraphQLIncludeDirective(), $node, $contextVariables);
168
169
        if ($include && $include['if'] === false) {
170
            return false;
171
        }
172
173
        return true;
174
    }
175
176
    /**
177
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
178
     * @param ObjectType                                $type
179
     * @return bool
180
     * @throws InvalidTypeException
181
     */
182
    private function doesFragmentConditionMatch(
183
        NodeInterface $fragment,
184
        ObjectType $type
185
    ): bool {
186
        $typeConditionNode = $fragment->getTypeCondition();
0 ignored issues
show
Bug introduced by
The method getTypeCondition() does not exist on Digia\GraphQL\Language\Node\NodeInterface. It seems like you code against a sub-type of Digia\GraphQL\Language\Node\NodeInterface such as Digia\GraphQL\Language\Node\InlineFragmentNode or Digia\GraphQL\Language\Node\FragmentDefinitionNode. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

186
        /** @scrutinizer ignore-call */ 
187
        $typeConditionNode = $fragment->getTypeCondition();
Loading history...
187
188
        if (!$typeConditionNode) {
189
            return true;
190
        }
191
192
        $conditionalType = typeFromAST($this->context->getSchema(), $typeConditionNode);
193
194
        if ($conditionalType === $type) {
195
            return true;
196
        }
197
198
        if ($conditionalType instanceof AbstractTypeInterface) {
199
            return $this->context->getSchema()->isPossibleType($conditionalType, $type);
200
        }
201
202
        return false;
203
    }
204
205
    /**
206
     * @TODO: consider to move this to FieldNode
207
     * @param FieldNode $node
208
     * @return string
209
     */
210
    private function getFieldNameKey(FieldNode $node): string
211
    {
212
        return $node->getAlias() ? $node->getAlias()->getValue() : $node->getNameValue();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $node->getAlias()...: $node->getNameValue() could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
213
    }
214
215
    /**
216
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
217
     * @param ObjectType $objectType
218
     * @param            $rootValue
219
     * @param            $path
220
     * @param            $fields
221
     * @return array
222
     * @throws InvalidTypeException
223
     * @throws \Digia\GraphQL\Error\ExecutionException
224
     * @throws \Digia\GraphQL\Error\InvariantException
225
     * @throws \Throwable
226
     */
227
    protected function executeFields(
228
        ObjectType $objectType,
229
        $rootValue,
230
        $path,
231
        $fields
232
    ): array {
233
        $finalResults      = [];
234
        $isContainsPromise = false;
235
236
        foreach ($fields as $fieldName => $fieldNodes) {
237
            $fieldPath   = $path;
238
            $fieldPath[] = $fieldName;
239
240
            try {
241
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->resolveField($obj...fieldNodes, $fieldPath) targeting Digia\GraphQL\Execution\...trategy::resolveField() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
242
            } catch (UndefinedException $ex) {
243
                continue;
244
            }
245
246
            $isContainsPromise = $isContainsPromise || $this->isPromise($result);
247
248
            $finalResults[$fieldName] = $result;
249
        }
250
251
        if ($isContainsPromise) {
252
            $keys    = array_keys($finalResults);
253
            $promise = \React\Promise\all(array_values($finalResults));
254
            $promise->then(function ($values) use ($keys, &$finalResults) {
255
                foreach ($values as $i => $value) {
256
                    $finalResults[$keys[$i]] = $value;
257
                }
258
            });
259
        }
260
261
        return $finalResults;
262
    }
263
264
    /**
265
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
266
     *
267
     * @param ObjectType $objectType
268
     * @param            $rootValue
269
     * @param            $path
270
     * @param            $fields
271
     * @return array
272
     * @throws InvalidTypeException
273
     * @throws \Digia\GraphQL\Error\ExecutionException
274
     * @throws \Digia\GraphQL\Error\InvariantException
275
     * @throws \Throwable
276
     */
277
    public function executeFieldsSerially(
278
        ObjectType $objectType,
279
        $rootValue,
280
        $path,
281
        $fields
282
    ) {
283
284
        $finalResults = [];
285
286
        $promise = new \React\Promise\FulfilledPromise([]);
287
288
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
289
            $fieldPath   = $path;
290
            $fieldPath[] = $fieldName;
291
            try {
292
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $this->resolveField($obj...fieldNodes, $fieldPath) targeting Digia\GraphQL\Execution\...trategy::resolveField() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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

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

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

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

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

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