Completed
Pull Request — master (#199)
by Quang
03:08
created

Executor::executeFields()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 8.439
c 0
b 0
f 0
cc 6
eloc 18
nc 8
nop 4
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
class Executor
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
    /**
83
     * @return ?array
84
     * @throws \Throwable
85
     */
86
    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...
87
    {
88
        $operation = $this->context->getOperation()->getOperation();
89
        $schema    = $this->context->getSchema();
90
91
        $path = [];
92
93
        $objectType = ($operation === 'mutation')
94
            ? $schema->getMutationType()
95
            : $schema->getQueryType();
96
97
        $fields = [];
98
        $visitedFragmentNames = [];
99
        try {
100
            $fields = $this->collectFields(
101
                $objectType,
0 ignored issues
show
Bug introduced by
It seems like $objectType can also be of type null; however, parameter $runtimeType of Digia\GraphQL\Execution\Executor::collectFields() does only seem to accept Digia\GraphQL\Type\Definition\ObjectType, 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

101
                /** @scrutinizer ignore-type */ $objectType,
Loading history...
102
                $this->operation->getSelectionSet(),
103
                $fields,
104
                $visitedFragmentNames
105
            );
106
107
            $result = ($operation === 'mutation')
108
                ? $this->executeFieldsSerially($objectType, $this->rootValue, $path, $fields)
0 ignored issues
show
Bug introduced by
It seems like $objectType can also be of type null; however, parameter $objectType of Digia\GraphQL\Execution\...executeFieldsSerially() does only seem to accept Digia\GraphQL\Type\Definition\ObjectType, 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

108
                ? $this->executeFieldsSerially(/** @scrutinizer ignore-type */ $objectType, $this->rootValue, $path, $fields)
Loading history...
109
                : $this->executeFields($objectType, $this->rootValue, $path, $fields);
0 ignored issues
show
Bug introduced by
It seems like $objectType can also be of type null; however, parameter $objectType of Digia\GraphQL\Execution\Executor::executeFields() does only seem to accept Digia\GraphQL\Type\Definition\ObjectType, 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

109
                : $this->executeFields(/** @scrutinizer ignore-type */ $objectType, $this->rootValue, $path, $fields);
Loading history...
110
111
        } catch (\Exception $ex) {
112
            $this->context->addError(
113
                new ExecutionException($ex->getMessage())
114
            );
115
116
            //@TODO return [null]
117
            return [$ex->getMessage()];
118
        }
119
120
        return $result;
121
    }
122
123
    /**
124
     * @param ObjectType       $runtimeType
125
     * @param SelectionSetNode $selectionSet
126
     * @param                  $fields
127
     * @param                  $visitedFragmentNames
128
     * @return mixed
129
     * @throws InvalidTypeException
130
     * @throws \Digia\GraphQL\Error\ExecutionException
131
     * @throws \Digia\GraphQL\Error\InvariantException
132
     */
133
    protected function collectFields(
134
        ObjectType $runtimeType,
135
        SelectionSetNode $selectionSet,
136
        &$fields,
137
        &$visitedFragmentNames
138
    ) {
139
        foreach ($selectionSet->getSelections() as $selection) {
140
            // Check if this Node should be included first
141
            if (!$this->shouldIncludeNode($selection)) {
142
                continue;
143
            }
144
            // Collect fields
145
            if ($selection instanceof FieldNode) {
146
                $fieldName = $this->getFieldNameKey($selection);
147
148
                if (!isset($fields[$fieldName])) {
149
                    $fields[$fieldName] = [];
150
                }
151
152
                $fields[$fieldName][] = $selection;
153
            } elseif ($selection instanceof InlineFragmentNode) {
154
                if (!$this->doesFragmentConditionMatch($selection, $runtimeType)) {
155
                    continue;
156
                }
157
158
                $this->collectFields($runtimeType, $selection->getSelectionSet(), $fields, $visitedFragmentNames);
159
            } elseif ($selection instanceof FragmentSpreadNode) {
160
                $fragmentName = $selection->getNameValue();
161
162
                if (!empty($visitedFragmentNames[$fragmentName])) {
163
                    continue;
164
                }
165
166
                $visitedFragmentNames[$fragmentName] = true;
167
                /** @var FragmentDefinitionNode $fragment */
168
                $fragment = $this->context->getFragments()[$fragmentName];
169
                $this->collectFields($runtimeType, $fragment->getSelectionSet(), $fields, $visitedFragmentNames);
170
            }
171
        }
172
173
        return $fields;
174
    }
175
176
177
    /**
178
     * @param $node
179
     * @return bool
180
     * @throws InvalidTypeException
181
     * @throws \Digia\GraphQL\Error\ExecutionException
182
     * @throws \Digia\GraphQL\Error\InvariantException
183
     */
184
    private function shouldIncludeNode(NodeInterface $node): bool
185
    {
186
187
        $contextVariables = $this->context->getVariableValues();
188
189
        $skip = coerceDirectiveValues(SkipDirective(), $node, $contextVariables);
190
191
        if ($skip && $skip['if'] === true) {
192
            return false;
193
        }
194
195
        $include = coerceDirectiveValues(IncludeDirective(), $node, $contextVariables);
196
197
        if ($include && $include['if'] === false) {
198
            return false;
199
        }
200
201
        return true;
202
    }
203
204
    /**
205
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
206
     * @param ObjectType                                $type
207
     * @return bool
208
     * @throws InvalidTypeException
209
     */
210
    private function doesFragmentConditionMatch(
211
        NodeInterface $fragment,
212
        ObjectType $type
213
    ): bool {
214
        $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

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

690
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
691
692
        if (null === $runtimeType) {
693
            //@TODO Show warning
694
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
695
        }
696
697
        if ($this->isPromise($runtimeType)) {
698
            /** @var ExtendedPromiseInterface $runtimeType */
699
            return $runtimeType->then(function ($resolvedRuntimeType) use (
700
                $returnType,
701
                $fieldNodes,
702
                $info,
703
                $path,
704
                &$result
705
            ) {
706
                return $this->completeObjectValue(
707
                    $this->ensureValidRuntimeType(
708
                        $resolvedRuntimeType,
709
                        $returnType,
710
                        $fieldNodes,
711
                        $info,
712
                        $result
713
                    ),
714
                    $fieldNodes,
715
                    $info,
716
                    $path,
717
                    $result
718
                );
719
            });
720
        }
721
722
        return $this->completeObjectValue(
723
            $this->ensureValidRuntimeType(
724
                $runtimeType,
725
                $returnType,
726
                $fieldNodes,
727
                $info,
728
                $result
729
            ),
730
            $fieldNodes,
731
            $info,
732
            $path,
733
            $result
734
        );
735
    }
736
737
    /**
738
     * @param                       $runtimeTypeOrName
739
     * @param AbstractTypeInterface $returnType
740
     * @param                       $fieldNodes
741
     * @param ResolveInfo           $info
742
     * @param                       $result
743
     * @return TypeInterface|ObjectType|null
744
     * @throws ExecutionException
745
     */
746
    private function ensureValidRuntimeType(
747
        $runtimeTypeOrName,
748
        AbstractTypeInterface $returnType,
749
        $fieldNodes,
750
        ResolveInfo $info,
751
        &$result
752
    ) {
753
        $runtimeType = is_string($runtimeTypeOrName)
754
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
755
            : $runtimeTypeOrName;
756
757
        $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

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

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

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

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