Completed
Pull Request — master (#135)
by Quang
05:36 queued 02:34
created

ExecutionStrategy::defaultTypeResolver()   D

Complexity

Conditions 10
Paths 10

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 19
nc 10
nop 4

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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

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

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