Passed
Push — master ( 87d406...fa923b )
by Quang
02:41
created

Executor::completeValueWithLocatedError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 10
nc 2
nop 5
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\OperationDefinitionNode;
11
use Digia\GraphQL\Schema\Schema;
12
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
13
use Digia\GraphQL\Type\Definition\Field;
14
use Digia\GraphQL\Type\Definition\InterfaceType;
15
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
16
use Digia\GraphQL\Type\Definition\ListType;
17
use Digia\GraphQL\Type\Definition\NonNullType;
18
use Digia\GraphQL\Type\Definition\ObjectType;
19
use Digia\GraphQL\Type\Definition\TypeInterface;
20
use Digia\GraphQL\Type\Definition\UnionType;
21
use React\Promise\ExtendedPromiseInterface;
22
use React\Promise\PromiseInterface;
23
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
24
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
25
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
26
use function Digia\GraphQL\Util\toString;
27
28
/**
29
 * Class AbstractStrategy
30
 * @package Digia\GraphQL\Execution\Strategies
31
 */
32
class Executor
33
{
34
    /**
35
     * @var ExecutionContext
36
     */
37
    protected $context;
38
39
    /**
40
     * @var OperationDefinitionNode
41
     */
42
    protected $operation;
43
44
    /**
45
     * @var mixed
46
     */
47
    protected $rootValue;
48
49
50
    /**
51
     * @var FieldCollector
52
     */
53
    protected $fieldCollector;
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
        FieldCollector $fieldCollector,
75
        $rootValue
76
    ) {
77
        $this->context        = $context;
78
        $this->operation      = $operation;
79
        $this->fieldCollector = $fieldCollector;
80
        $this->rootValue      = $rootValue;
81
    }
82
83
84
    /**
85
     * @return array|null
86
     * @throws ExecutionException
87
     * @throws \Throwable
88
     */
89
    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...
90
    {
91
        $operation = $this->context->getOperation();
92
        $schema    = $this->context->getSchema();
93
94
        $path = [];
95
96
        $objectType = $this->getOperationType($schema, $operation);
97
98
        $fields               = [];
99
        $visitedFragmentNames = [];
100
        try {
101
            $fields = $this->fieldCollector->collectFields(
102
                $objectType,
103
                $this->operation->getSelectionSet(),
104
                $fields,
105
                $visitedFragmentNames
106
            );
107
108
            $result = ($operation->getOperation() === 'mutation')
109
                ? $this->executeFieldsSerially($objectType, $this->rootValue, $path, $fields)
110
                : $this->executeFields($objectType, $this->rootValue, $path, $fields);
111
112
        } catch (\Exception $ex) {
113
            $this->context->addError(
114
                new ExecutionException($ex->getMessage())
115
            );
116
117
            //@TODO return [null]
118
            return [$ex->getMessage()];
119
        }
120
121
        return $result;
122
    }
123
124
    /**
125
     * @param Schema                  $schema
126
     * @param OperationDefinitionNode $operation
127
     * @throws ExecutionException
128
     */
129
    public function getOperationType(Schema $schema, OperationDefinitionNode $operation)
130
    {
131
        switch ($operation->getOperation()) {
132
            case 'query':
133
                return $schema->getQueryType();
134
            case 'mutation':
135
                $mutationType = $schema->getMutationType();
136
                if (!$mutationType) {
137
                    throw new ExecutionException(
138
                        'Schema is not configured for mutations',
139
                        [$operation]
140
                    );
141
                }
142
                return $mutationType;
143
            case 'subscription':
144
                $subscriptionType = $schema->getSubscriptionType();
145
                if (!$subscriptionType) {
146
                    throw new ExecutionException(
147
                        'Schema is not configured for subscriptions',
148
                        [$operation]
149
                    );
150
                }
151
                return $subscriptionType;
152
            default:
153
                throw new ExecutionException(
154
                    'Can only execute queries, mutations and subscriptions',
155
                    [$operation]
156
                );
157
        }
158
    }
159
160
    /**
161
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
162
     * @param ObjectType $objectType
163
     * @param            $rootValue
164
     * @param            $path
165
     * @param  array     $fields
166
     * @return array
167
     * @throws InvalidTypeException
168
     * @throws \Digia\GraphQL\Error\ExecutionException
169
     * @throws \Digia\GraphQL\Error\InvariantException
170
     * @throws \Throwable
171
     */
172
    protected function executeFields(
173
        ObjectType $objectType,
174
        $rootValue,
175
        $path,
176
        array $fields
177
    ): array {
178
        $finalResults      = [];
179
        $isContainsPromise = false;
180
181
        foreach ($fields as $fieldName => $fieldNodes) {
182
            $fieldPath   = $path;
183
            $fieldPath[] = $fieldName;
184
185
            try {
186
                $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...
187
            } catch (UndefinedException $ex) {
188
                continue;
189
            }
190
191
            $isContainsPromise = $isContainsPromise || $this->isPromise($result);
192
193
            $finalResults[$fieldName] = $result;
194
        }
195
196
        if ($isContainsPromise) {
197
            $keys    = array_keys($finalResults);
198
            $promise = \React\Promise\all(array_values($finalResults));
199
            $promise->then(function ($values) use ($keys, &$finalResults) {
200
                foreach ($values as $i => $value) {
201
                    $finalResults[$keys[$i]] = $value;
202
                }
203
            });
204
        }
205
206
        return $finalResults;
207
    }
208
209
    /**
210
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
211
     *
212
     * @param ObjectType $objectType
213
     * @param            $rootValue
214
     * @param            $path
215
     * @param  array     $fields
216
     * @return array
217
     * @throws InvalidTypeException
218
     * @throws \Digia\GraphQL\Error\ExecutionException
219
     * @throws \Digia\GraphQL\Error\InvariantException
220
     * @throws \Throwable
221
     */
222
    public function executeFieldsSerially(
223
        ObjectType $objectType,
224
        $rootValue,
225
        $path,
226
        array $fields
227
    ) {
228
229
        $finalResults = [];
230
231
        $promise = new \React\Promise\FulfilledPromise([]);
232
233
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
234
            $fieldPath   = $path;
235
            $fieldPath[] = $fieldName;
236
            try {
237
                $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...
238
            } catch (UndefinedException $ex) {
239
                return null;
240
            }
241
242
            if ($this->isPromise($result)) {
243
                /** @var ExtendedPromiseInterface $result */
244
                return $result->then(function ($resolvedResult) use ($fieldName, $results) {
245
                    $results[$fieldName] = $resolvedResult;
246
                    return $results;
247
                });
248
            }
249
250
            $results[$fieldName] = $result;
251
252
            return $results;
253
        };
254
255
        foreach ($fields as $fieldName => $fieldNodes) {
256
            $promise = $promise->then(function ($resolvedResults) use (
257
                $resolve,
258
                $fieldName,
259
                $path,
260
                $objectType,
261
                $rootValue,
262
                $fieldNodes
263
            ) {
264
                return $resolve($resolvedResults, $fieldName, $path, $objectType, $rootValue, $fieldNodes);
265
            });
266
        }
267
268
        $promise->then(function ($resolvedResults) use (&$finalResults) {
269
            $finalResults = $resolvedResults ?? [];
270
        });
271
272
        return $finalResults;
273
    }
274
275
    /**
276
     * @param Schema     $schema
277
     * @param ObjectType $parentType
278
     * @param string     $fieldName
279
     * @return \Digia\GraphQL\Type\Definition\Field|null
280
     * @throws InvalidTypeException
281
     * @throws \Digia\GraphQL\Error\InvariantException
282
     */
283
    public function getFieldDefinition(
284
        Schema $schema,
285
        ObjectType $parentType,
286
        string $fieldName
287
    ) {
288
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
289
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
290
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
291
292
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
293
            return $schemaMetaFieldDefinition;
294
        }
295
296
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
297
            return $typeMetaFieldDefinition;
298
        }
299
300
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
301
            return $typeNameMetaFieldDefinition;
302
        }
303
304
        $fields = $parentType->getFields();
305
306
        return $fields[$fieldName] ?? null;
307
    }
308
309
310
    /**
311
     * @param ObjectType $parentType
312
     * @param            $rootValue
313
     * @param array      $fieldNodes
314
     * @param            $path
315
     * @return array|null|\Throwable
316
     * @throws InvalidTypeException
317
     * @throws \Digia\GraphQL\Error\ExecutionException
318
     * @throws \Digia\GraphQL\Error\InvariantException
319
     * @throws \Throwable
320
     */
321
    protected function resolveField(
322
        ObjectType $parentType,
323
        $rootValue,
324
        array $fieldNodes,
325
        $path
326
    ) {
327
        /** @var FieldNode $fieldNode */
328
        $fieldNode = $fieldNodes[0];
329
330
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
331
332
        if (null === $field) {
333
            throw new UndefinedException('Undefined field definition.');
334
        }
335
336
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
337
338
        $resolveFunction = $this->determineResolveFunction($field, $parentType, $this->context);
339
340
        $result = $this->resolveFieldValueOrError(
341
            $field,
342
            $fieldNode,
343
            $resolveFunction,
344
            $rootValue,
345
            $this->context,
346
            $info
347
        );
348
349
        $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...
350
            $field->getType(),
351
            $fieldNodes,
352
            $info,
353
            $path,
354
            $result// $result is passed as $source
355
        );
356
357
        return $result;
358
    }
359
360
    /**
361
     * @param array            $fieldNodes
362
     * @param FieldNode        $fieldNode
363
     * @param Field            $field
364
     * @param ObjectType       $parentType
365
     * @param array|null       $path
366
     * @param ExecutionContext $context
367
     * @return ResolveInfo
368
     */
369
    private function buildResolveInfo(
370
        array $fieldNodes,
371
        FieldNode $fieldNode,
372
        Field $field,
373
        ObjectType $parentType,
374
        ?array $path,
375
        ExecutionContext $context
376
    ) {
377
        return new ResolveInfo(
378
            $fieldNode->getNameValue(),
379
            $fieldNodes,
380
            $field->getType(),
381
            $parentType,
382
            $path,
383
            $context->getSchema(),
384
            $context->getFragments(),
385
            $context->getRootValue(),
386
            $context->getOperation(),
387
            $context->getVariableValues()
388
        );
389
    }
390
391
    /**
392
     * @param Field            $field
393
     * @param ObjectType       $objectType
394
     * @param ExecutionContext $context
395
     * @return callable|mixed|null
396
     */
397
    private function determineResolveFunction(
398
        Field $field,
399
        ObjectType $objectType,
400
        ExecutionContext $context
401
    ) {
402
403
        if ($field->hasResolve()) {
404
            return $field->getResolve();
405
        }
406
407
        if ($objectType->hasResolve()) {
408
            return $objectType->getResolve();
409
        }
410
411
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
412
    }
413
414
    /**
415
     * @param TypeInterface $fieldType
416
     * @param array         $fieldNodes
417
     * @param ResolveInfo   $info
418
     * @param               $path
419
     * @param               $result
420
     * @return null
421
     * @throws \Throwable
422
     */
423
    public function completeValueCatchingError(
424
        TypeInterface $fieldType,
425
        array $fieldNodes,
426
        ResolveInfo $info,
427
        $path,
428
        &$result
429
    ) {
430
        if ($fieldType instanceof NonNullType) {
431
            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...
432
                $fieldType,
433
                $fieldNodes,
434
                $info,
435
                $path,
436
                $result
437
            );
438
        }
439
440
        try {
441
            $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...
442
                $fieldType,
443
                $fieldNodes,
444
                $info,
445
                $path,
446
                $result
447
            );
448
449
            if ($this->isPromise($completed)) {
450
                $context = $this->context;
451
                /** @var ExtendedPromiseInterface $completed */
452
                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...
453
                    //@TODO Handle $error better
454
                    if ($error instanceof \Exception) {
455
                        $context->addError($this->buildLocatedError($error, $fieldNodes, $path));
456
                    } else {
457
                        $context->addError(
458
                            $this->buildLocatedError(
459
                                new ExecutionException($error ?? 'An unknown error occurred.'),
460
                                $fieldNodes,
461
                                $path
462
                            )
463
                        );
464
                    }
465
                    return new \React\Promise\FulfilledPromise(null);
466
                });
467
            }
468
469
            return $completed;
470
        } catch (ExecutionException $ex) {
471
            $this->context->addError($ex);
472
            return null;
473
        } catch (\Exception $ex) {
474
            $this->context->addError($this->buildLocatedError($ex, $fieldNodes, $path));
475
            return null;
476
        }
477
    }
478
479
    /**
480
     * @param TypeInterface $fieldType
481
     * @param array         $fieldNodes
482
     * @param ResolveInfo   $info
483
     * @param               $path
484
     * @param               $result
485
     * @throws \Throwable
486
     */
487
    public function completeValueWithLocatedError(
488
        TypeInterface $fieldType,
489
        array $fieldNodes,
490
        ResolveInfo $info,
491
        $path,
492
        $result
493
    ) {
494
        try {
495
            $completed = $this->completeValue(
496
                $fieldType,
497
                $fieldNodes,
498
                $info,
499
                $path,
500
                $result
501
            );
502
503
            return $completed;
504
        } catch (\Throwable $ex) {
505
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
506
        }
507
    }
508
509
    /**
510
     * @param TypeInterface $returnType
511
     * @param array         $fieldNodes
512
     * @param ResolveInfo   $info
513
     * @param               $path
514
     * @param               $result
515
     * @return array|mixed
516
     * @throws ExecutionException
517
     * @throws \Throwable
518
     */
519
    private function completeValue(
520
        TypeInterface $returnType,
521
        array $fieldNodes,
522
        ResolveInfo $info,
523
        $path,
524
        &$result
525
    ) {
526
        if ($this->isPromise($result)) {
527
            /** @var ExtendedPromiseInterface $result */
528
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
529
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
530
            });
531
        }
532
533
        if ($result instanceof \Throwable) {
534
            throw $result;
535
        }
536
537
        // If result is null-like, return null.
538
        if (null === $result) {
539
            return null;
540
        }
541
542
        if ($returnType instanceof NonNullType) {
543
            $completed = $this->completeValue(
544
                $returnType->getOfType(),
545
                $fieldNodes,
546
                $info,
547
                $path,
548
                $result
549
            );
550
551
            if ($completed === null) {
552
                throw new ExecutionException(
553
                    sprintf(
554
                        'Cannot return null for non-nullable field %s.%s.',
555
                        $info->getParentType(), $info->getFieldName()
556
                    )
557
                );
558
            }
559
560
            return $completed;
561
        }
562
563
        // If field type is List, complete each item in the list with the inner type
564
        if ($returnType instanceof ListType) {
565
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
566
        }
567
568
569
        // If field type is Scalar or Enum, serialize to a valid value, returning
570
        // null if serialization is not possible.
571
        if ($returnType instanceof LeafTypeInterface) {
572
            return $this->completeLeafValue($returnType, $result);
573
        }
574
575
        //@TODO Make a function for checking abstract type?
576
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
577
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
578
        }
579
580
        // Field type must be Object, Interface or Union and expect sub-selections.
581
        if ($returnType instanceof ObjectType) {
582
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
583
        }
584
585
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
586
    }
587
588
    /**
589
     * @param AbstractTypeInterface $returnType
590
     * @param array                 $fieldNodes
591
     * @param ResolveInfo           $info
592
     * @param                       $path
593
     * @param                       $result
594
     * @return array|PromiseInterface
595
     * @throws ExecutionException
596
     * @throws InvalidTypeException
597
     * @throws \Digia\GraphQL\Error\InvariantException
598
     * @throws \Throwable
599
     */
600
    private function completeAbstractValue(
601
        AbstractTypeInterface $returnType,
602
        array $fieldNodes,
603
        ResolveInfo $info,
604
        $path,
605
        &$result
606
    ) {
607
        $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

607
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), /** @scrutinizer ignore-type */ $info);
Loading history...
608
609
        if (null === $runtimeType) {
610
            //@TODO Show warning
611
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
612
        }
613
614
        if ($this->isPromise($runtimeType)) {
615
            /** @var ExtendedPromiseInterface $runtimeType */
616
            return $runtimeType->then(function ($resolvedRuntimeType) use (
617
                $returnType,
618
                $fieldNodes,
619
                $info,
620
                $path,
621
                &$result
622
            ) {
623
                return $this->completeObjectValue(
624
                    $this->ensureValidRuntimeType(
625
                        $resolvedRuntimeType,
626
                        $returnType,
627
                        $fieldNodes,
628
                        $info,
629
                        $result
630
                    ),
631
                    $fieldNodes,
632
                    $info,
633
                    $path,
634
                    $result
635
                );
636
            });
637
        }
638
639
        return $this->completeObjectValue(
640
            $this->ensureValidRuntimeType(
641
                $runtimeType,
642
                $returnType,
643
                $fieldNodes,
644
                $info,
645
                $result
646
            ),
647
            $fieldNodes,
648
            $info,
649
            $path,
650
            $result
651
        );
652
    }
653
654
    /**
655
     * @param                       $runtimeTypeOrName
656
     * @param AbstractTypeInterface $returnType
657
     * @param array                 $fieldNodes
658
     * @param ResolveInfo           $info
659
     * @param                       $result
660
     * @return TypeInterface|ObjectType|null
661
     * @throws ExecutionException
662
     */
663
    private function ensureValidRuntimeType(
664
        $runtimeTypeOrName,
665
        AbstractTypeInterface $returnType,
666
        array $fieldNodes,
667
        ResolveInfo $info,
668
        &$result
669
    ) {
670
        $runtimeType = is_string($runtimeTypeOrName)
671
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
672
            : $runtimeTypeOrName;
673
674
        $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

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

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

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

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