Passed
Pull Request — master (#318)
by Sam
03:16
created

Executor::createDefaultErrorHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\Handler\ErrorHandler;
6
use Digia\GraphQL\Error\Handler\ErrorHandlerInterface;
7
use Digia\GraphQL\Error\GraphQLException;
8
use Digia\GraphQL\Error\InvalidTypeException;
9
use Digia\GraphQL\Error\InvariantException;
10
use Digia\GraphQL\Error\Handler\AddErrorMiddleware;
11
use Digia\GraphQL\Language\Node\FieldNode;
12
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
13
use Digia\GraphQL\Schema\Schema;
14
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
15
use Digia\GraphQL\Type\Definition\Field;
16
use Digia\GraphQL\Type\Definition\InterfaceType;
17
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
18
use Digia\GraphQL\Type\Definition\ListType;
19
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
20
use Digia\GraphQL\Type\Definition\NonNullType;
21
use Digia\GraphQL\Type\Definition\ObjectType;
22
use Digia\GraphQL\Type\Definition\SerializableTypeInterface;
23
use Digia\GraphQL\Type\Definition\TypeInterface;
24
use Digia\GraphQL\Type\Definition\UnionType;
25
use React\Promise\ExtendedPromiseInterface;
26
use React\Promise\FulfilledPromise;
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 React\Promise\all as promiseAll;
33
34
class Executor
35
{
36
    /**
37
     * @var ExecutionContext
38
     */
39
    protected $context;
40
41
    /**
42
     * @var OperationDefinitionNode
43
     */
44
    protected $operation;
45
46
    /**
47
     * @var mixed
48
     */
49
    protected $rootValue;
50
51
    /**
52
     * @var FieldCollector
53
     */
54
    protected $fieldCollector;
55
56
    /**
57
     * @var array
58
     */
59
    protected $finalResult;
60
61
    /**
62
     * @var ErrorHandlerInterface|null
63
     */
64
    protected $errorHandler;
65
66
    /**
67
     * @var array
68
     */
69
    private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
70
71
    /**
72
     * Executor constructor.
73
     *
74
     * @param ExecutionContext           $context
75
     * @param FieldCollector             $fieldCollector
76
     * @param ErrorHandlerInterface|null $errorHandler
77
     */
78
    public function __construct(
79
        ExecutionContext $context,
80
        FieldCollector $fieldCollector,
81
        ?ErrorHandlerInterface $errorHandler = null
82
    ) {
83
        $this->context        = $context;
84
        $this->fieldCollector = $fieldCollector;
85
        $this->errorHandler   = $errorHandler ?? $this->createDefaultErrorHandler();
86
    }
87
88
    /**
89
     * @return array|null
90
     * @throws ExecutionException
91
     * @throws \Throwable
92
     */
93
    public function execute(): ?array
94
    {
95
        $schema    = $this->context->getSchema();
96
        $operation = $this->context->getOperation();
97
        $rootValue = $this->context->getRootValue();
98
99
        $objectType = $this->getOperationType($schema, $operation);
100
101
        $fields               = [];
102
        $visitedFragmentNames = [];
103
        $path                 = [];
104
105
        $fields = $this->fieldCollector->collectFields(
106
            $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\...lector::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

106
            /** @scrutinizer ignore-type */ $objectType,
Loading history...
107
            $operation->getSelectionSet(),
0 ignored issues
show
Bug introduced by
It seems like $operation->getSelectionSet() can also be of type null; however, parameter $selectionSet of Digia\GraphQL\Execution\...lector::collectFields() does only seem to accept Digia\GraphQL\Language\Node\SelectionSetNode, 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

107
            /** @scrutinizer ignore-type */ $operation->getSelectionSet(),
Loading history...
108
            $fields,
109
            $visitedFragmentNames
110
        );
111
112
        try {
113
            $result = $operation->getOperation() === 'mutation'
114
                ? $this->executeFieldsSerially($objectType, $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

114
                ? $this->executeFieldsSerially(/** @scrutinizer ignore-type */ $objectType, $rootValue, $path, $fields)
Loading history...
115
                : $this->executeFields($objectType, $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

115
                : $this->executeFields(/** @scrutinizer ignore-type */ $objectType, $rootValue, $path, $fields);
Loading history...
116
        } catch (\Throwable $ex) {
117
            $this->handleError(new ExecutionException($ex->getMessage(), null, null, null, null, null, $ex));
118
            return null;
119
        }
120
121
        return $result;
122
    }
123
124
    /**
125
     * @param Schema                  $schema
126
     * @param OperationDefinitionNode $operation
127
     *
128
     * @return ObjectType|null
129
     * @throws ExecutionException
130
     */
131
    public function getOperationType(Schema $schema, OperationDefinitionNode $operation): ?ObjectType
132
    {
133
        switch ($operation->getOperation()) {
134
            case 'query':
135
                return $schema->getQueryType();
136
            case 'mutation':
137
                $mutationType = $schema->getMutationType();
138
                if (null === $mutationType) {
139
                    throw new ExecutionException(
140
                        'Schema is not configured for mutations',
141
                        [$operation]
142
                    );
143
                }
144
145
                return $mutationType;
146
            case 'subscription':
147
                $subscriptionType = $schema->getSubscriptionType();
148
                if (null === $subscriptionType) {
149
                    throw new ExecutionException(
150
                        'Schema is not configured for subscriptions',
151
                        [$operation]
152
                    );
153
                }
154
155
                return $subscriptionType;
156
            default:
157
                throw new ExecutionException(
158
                    'Can only execute queries, mutations and subscriptions',
159
                    [$operation]
160
                );
161
        }
162
    }
163
164
    /**
165
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
166
     *
167
     * @param ObjectType $objectType
168
     * @param mixed      $rootValue
169
     * @param array      $path
170
     * @param array      $fields
171
     *
172
     * @return array
173
     */
174
    public function executeFieldsSerially(
175
        ObjectType $objectType,
176
        $rootValue,
177
        array $path,
178
        array $fields
179
    ): array {
180
        $finalResults = [];
181
182
        $promise = new FulfilledPromise([]);
183
184
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
185
            $fieldPath   = $path;
186
            $fieldPath[] = $fieldName;
187
            try {
188
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
189
            } catch (UndefinedException $ex) {
190
                return null;
191
            }
192
193
            if ($result instanceof ExtendedPromiseInterface) {
194
                return $result->then(function ($resolvedResult) use ($fieldName, $results) {
195
                    $results[$fieldName] = $resolvedResult;
196
197
                    return $results;
198
                });
199
            }
200
201
            $results[$fieldName] = $result;
202
203
            return $results;
204
        };
205
206
        foreach ($fields as $fieldName => $fieldNodes) {
207
            $promise = $promise->then(function ($resolvedResults) use (
208
                $resolve,
209
                $fieldName,
210
                $path,
211
                $objectType,
212
                $rootValue,
213
                $fieldNodes
214
            ) {
215
                return $resolve($resolvedResults, $fieldName, $path, $objectType, $rootValue, $fieldNodes);
216
            });
217
        }
218
219
        $promise->then(function ($resolvedResults) use (&$finalResults) {
220
            $finalResults = $resolvedResults ?? [];
221
        })->otherwise(function (\Throwable $ex) {
222
            if (!$ex instanceof ExecutionException) {
223
                $ex = new ExecutionException($ex->getMessage(), null, null, null, null, null, $ex);
224
            }
225
            $this->handleError($ex);
226
        });
227
228
        return $finalResults;
229
    }
230
231
    /**
232
     * @param Schema     $schema
233
     * @param ObjectType $parentType
234
     * @param string     $fieldName
235
     *
236
     * @return Field|null
237
     */
238
    public function getFieldDefinition(
239
        Schema $schema,
240
        ObjectType $parentType,
241
        string $fieldName
242
    ): ?Field {
243
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
244
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
245
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
246
247
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
248
            return $schemaMetaFieldDefinition;
249
        }
250
251
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
252
            return $typeMetaFieldDefinition;
253
        }
254
255
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
256
            return $typeNameMetaFieldDefinition;
257
        }
258
259
        /** @noinspection PhpUnhandledExceptionInspection */
260
        $fields = $parentType->getFields();
261
262
        return $fields[$fieldName] ?? null;
263
    }
264
265
    /**
266
     * @param TypeInterface $fieldType
267
     * @param FieldNode[]   $fieldNodes
268
     * @param ResolveInfo   $info
269
     * @param array         $path
270
     * @param mixed         $result
271
     *
272
     * @return array|mixed|null
273
     * @throws \Throwable
274
     */
275
    public function completeValueCatchingError(
276
        TypeInterface $fieldType,
277
        array $fieldNodes,
278
        ResolveInfo $info,
279
        array $path,
280
        &$result
281
    ) {
282
        if ($fieldType instanceof NonNullType) {
283
            return $this->completeValueWithLocatedError(
284
                $fieldType,
285
                $fieldNodes,
286
                $info,
287
                $path,
288
                $result
289
            );
290
        }
291
292
        try {
293
            $completed = $this->completeValueWithLocatedError(
294
                $fieldType,
295
                $fieldNodes,
296
                $info,
297
                $path,
298
                $result
299
            );
300
301
            if ($completed instanceof ExtendedPromiseInterface) {
302
                return $completed->then(null, function ($error) use ($fieldNodes, $path) {
303
                    if ($error instanceof \Exception) {
304
                        $this->handleError(
305
                            $this->buildLocatedError($error, $fieldNodes, $path)
306
                        );
307
                    } else {
308
                        $this->handleError(
309
                            $this->buildLocatedError(
310
                                new ExecutionException($error ?? 'An unknown error occurred.'),
311
                                $fieldNodes,
312
                                $path
313
                            )
314
                        );
315
                    }
316
317
                    return new FulfilledPromise(null);
318
                });
319
            }
320
321
            return $completed;
322
        } catch (\Throwable $ex) {
323
            $this->handleError($this->buildLocatedError($ex, $fieldNodes, $path));
324
            return null;
325
        }
326
    }
327
328
    /**
329
     * @param TypeInterface $fieldType
330
     * @param FieldNode[]   $fieldNodes
331
     * @param ResolveInfo   $info
332
     * @param array         $path
333
     * @param mixed         $result
334
     *
335
     * @return array|mixed
336
     * @throws \Throwable
337
     */
338
    public function completeValueWithLocatedError(
339
        TypeInterface $fieldType,
340
        array $fieldNodes,
341
        ResolveInfo $info,
342
        array $path,
343
        $result
344
    ) {
345
        try {
346
            $completed = $this->completeValue(
347
                $fieldType,
348
                $fieldNodes,
349
                $info,
350
                $path,
351
                $result
352
            );
353
354
            return $completed;
355
        } catch (\Throwable $ex) {
356
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
357
        }
358
    }
359
360
    /**
361
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
362
     *
363
     * @param ObjectType $objectType
364
     * @param mixed      $rootValue
365
     * @param array      $path
366
     * @param array      $fields
367
     *
368
     * @return array
369
     * @throws ExecutionException
370
     * @throws \Throwable
371
     */
372
    protected function executeFields(
373
        ObjectType $objectType,
374
        $rootValue,
375
        array $path,
376
        array $fields
377
    ): array {
378
        $finalResults       = [];
379
        $doesContainPromise = false;
380
381
        foreach ($fields as $fieldName => $fieldNodes) {
382
            $fieldPath   = $path;
383
            $fieldPath[] = $fieldName;
384
385
            try {
386
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
387
            } catch (UndefinedException $ex) {
388
                continue;
389
            }
390
391
            $doesContainPromise = $doesContainPromise || $result instanceof ExtendedPromiseInterface;
392
393
            $finalResults[$fieldName] = $result;
394
        }
395
396
        if ($doesContainPromise) {
397
            $keys    = \array_keys($finalResults);
398
            $promise = promiseAll(\array_values($finalResults));
399
400
            $promise->then(function ($values) use ($keys, &$finalResults) {
401
                /** @noinspection ForeachSourceInspection */
402
                foreach ($values as $i => $value) {
403
                    $finalResults[$keys[$i]] = $value;
404
                }
405
            });
406
        }
407
408
        return $finalResults;
409
    }
410
411
    /**
412
     * @param ObjectType  $parentType
413
     * @param mixed       $rootValue
414
     * @param FieldNode[] $fieldNodes
415
     * @param array       $path
416
     *
417
     * @return array|mixed|null
418
     * @throws UndefinedException
419
     * @throws \Throwable
420
     */
421
    protected function resolveField(
422
        ObjectType $parentType,
423
        $rootValue,
424
        array $fieldNodes,
425
        array $path
426
    ) {
427
        /** @var FieldNode $fieldNode */
428
        $fieldNode = $fieldNodes[0];
429
430
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
0 ignored issues
show
Bug introduced by
It seems like $fieldNode->getNameValue() can also be of type null; however, parameter $fieldName of Digia\GraphQL\Execution\...r::getFieldDefinition() does only seem to accept string, 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

430
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, /** @scrutinizer ignore-type */ $fieldNode->getNameValue());
Loading history...
431
432
        if (null === $field) {
433
            throw new UndefinedException('Undefined field definition.');
434
        }
435
436
        $info = $this->createResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
437
438
        $resolveCallback = $this->determineResolveCallback($field, $parentType);
439
440
        $result = $this->resolveFieldValueOrError(
441
            $field,
442
            $fieldNode,
443
            $resolveCallback,
444
            $rootValue,
445
            $this->context,
446
            $info
447
        );
448
449
        $result = $this->completeValueCatchingError(
450
            $field->getType(),
0 ignored issues
show
Bug introduced by
It seems like $field->getType() can also be of type null; however, parameter $fieldType of Digia\GraphQL\Execution\...eteValueCatchingError() does only seem to accept Digia\GraphQL\Type\Definition\TypeInterface, 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

450
            /** @scrutinizer ignore-type */ $field->getType(),
Loading history...
451
            $fieldNodes,
452
            $info,
453
            $path,
454
            $result
455
        );
456
457
        return $result;
458
    }
459
460
    /**
461
     * @param Field      $field
462
     * @param ObjectType $objectType
463
     *
464
     * @return callable|mixed|null
465
     */
466
    protected function determineResolveCallback(Field $field, ObjectType $objectType)
467
    {
468
        if ($field->hasResolveCallback()) {
469
            return $field->getResolveCallback();
470
        }
471
472
        if ($objectType->hasResolveCallback()) {
473
            return $objectType->getResolveCallback();
474
        }
475
476
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
477
    }
478
479
    /**
480
     * @param TypeInterface $returnType
481
     * @param FieldNode[]   $fieldNodes
482
     * @param ResolveInfo   $info
483
     * @param array         $path
484
     * @param mixed         $result
485
     *
486
     * @return array|mixed
487
     * @throws InvariantException
488
     * @throws InvalidTypeException
489
     * @throws ExecutionException
490
     * @throws \Throwable
491
     */
492
    protected function completeValue(
493
        TypeInterface $returnType,
494
        array $fieldNodes,
495
        ResolveInfo $info,
496
        array $path,
497
        &$result
498
    ) {
499
        if ($result instanceof ExtendedPromiseInterface) {
500
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
501
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
502
            });
503
        }
504
505
        if ($result instanceof \Throwable) {
506
            throw $result;
507
        }
508
509
        // If field type is NonNull, complete for inner type, and throw field error
510
        if ($returnType instanceof NonNullType) {
511
            $completed = $this->completeValue(
512
                $returnType->getOfType(),
513
                $fieldNodes,
514
                $info,
515
                $path,
516
                $result
517
            );
518
519
            if ($completed === null) {
520
                throw new ExecutionException(
521
                    \sprintf(
522
                        'Cannot return null for non-nullable field %s.%s.',
523
                        $info->getParentType(),
524
                        $info->getFieldName()
525
                    )
526
                );
527
            }
528
529
            return $completed;
530
        }
531
532
        // If result is null, return null.
533
        if (null === $result) {
534
            return null;
535
        }
536
537
        // If field type is List, complete each item in the list with the inner type
538
        if ($returnType instanceof ListType) {
539
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
540
        }
541
542
        // If field type is Scalar or Enum, serialize to a valid value, returning
543
        // null if serialization is not possible.
544
        if ($returnType instanceof LeafTypeInterface) {
545
            return $this->completeLeafValue($returnType, $result);
546
        }
547
548
        // TODO: Make a function for checking abstract type?
549
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
550
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
551
        }
552
553
        // Field type must be Object, Interface or Union and expect sub-selections.
554
        if ($returnType instanceof ObjectType) {
555
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
556
        }
557
558
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
559
    }
560
561
    /**
562
     * @param AbstractTypeInterface $returnType
563
     * @param FieldNode[]           $fieldNodes
564
     * @param ResolveInfo           $info
565
     * @param array                 $path
566
     * @param mixed                 $result
567
     *
568
     * @return array|PromiseInterface
569
     * @throws ExecutionException
570
     * @throws InvalidTypeException
571
     * @throws InvariantException
572
     * @throws \Throwable
573
     */
574
    protected function completeAbstractValue(
575
        AbstractTypeInterface $returnType,
576
        array $fieldNodes,
577
        ResolveInfo $info,
578
        array $path,
579
        &$result
580
    ) {
581
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
582
583
        if (null === $runtimeType) {
584
            // TODO: Display warning
585
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
586
        }
587
588
        if ($runtimeType instanceof ExtendedPromiseInterface) {
589
            return $runtimeType->then(function ($resolvedRuntimeType) use (
590
                $returnType,
591
                $fieldNodes,
592
                $info,
593
                $path,
594
                &$result
595
            ) {
596
                return $this->completeObjectValue(
597
                    $this->ensureValidRuntimeType($resolvedRuntimeType, $returnType, $info, $result),
598
                    $fieldNodes,
599
                    $info,
600
                    $path,
601
                    $result
602
                );
603
            });
604
        }
605
606
        return $this->completeObjectValue(
607
            $this->ensureValidRuntimeType($runtimeType, $returnType, $info, $result),
608
            $fieldNodes,
609
            $info,
610
            $path,
611
            $result
612
        );
613
    }
614
615
    /**
616
     * @param NamedTypeInterface|string $runtimeTypeOrName
617
     * @param NamedTypeInterface        $returnType
618
     * @param ResolveInfo               $info
619
     * @param mixed                     $result
620
     *
621
     * @return TypeInterface|ObjectType|null
622
     * @throws ExecutionException
623
     * @throws InvariantException
624
     */
625
    protected function ensureValidRuntimeType(
626
        $runtimeTypeOrName,
627
        NamedTypeInterface $returnType,
628
        ResolveInfo $info,
629
        &$result
630
    ) {
631
        /** @var NamedTypeInterface $runtimeType */
632
        $runtimeType = \is_string($runtimeTypeOrName)
633
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
634
            : $runtimeTypeOrName;
635
636
        $runtimeTypeName = $runtimeType->getName();
637
        $returnTypeName  = $returnType->getName();
638
639
        if (!$runtimeType instanceof ObjectType) {
640
            $parentTypeName = $info->getParentType()->getName();
641
            $fieldName      = $info->getFieldName();
642
643
            throw new ExecutionException(
644
                \sprintf(
645
                    'Abstract type %s must resolve to an Object type at runtime for field %s.%s ' .
646
                    'with value "%s", received "%s".',
647
                    $returnTypeName,
648
                    $parentTypeName,
649
                    $fieldName,
650
                    $result,
651
                    $runtimeTypeName
652
                )
653
            );
654
        }
655
656
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
657
            throw new ExecutionException(
658
                \sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeTypeName, $returnTypeName)
659
            );
660
        }
661
662
        if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
0 ignored issues
show
introduced by
The condition $runtimeType !== $this->...runtimeType->getName()) is always true.
Loading history...
663
            throw new ExecutionException(
664
                \sprintf(
665
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
666
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
667
                    'type instance as referenced anywhere else within the schema.',
668
                    $runtimeTypeName,
669
                    $returnTypeName
670
                )
671
            );
672
        }
673
674
        return $runtimeType;
675
    }
676
677
    /**
678
     * @param mixed                 $value
679
     * @param mixed                 $context
680
     * @param ResolveInfo           $info
681
     * @param AbstractTypeInterface $abstractType
682
     *
683
     * @return NamedTypeInterface|mixed|null
684
     * @throws InvariantException
685
     */
686
    protected function defaultTypeResolver(
687
        $value,
688
        $context,
689
        ResolveInfo $info,
690
        AbstractTypeInterface $abstractType
691
    ) {
692
        if (\is_array($value) && isset($value['__typename'])) {
693
            return $value['__typename'];
694
        }
695
696
        /** @var ObjectType[] $possibleTypes */
697
        $possibleTypes           = $info->getSchema()->getPossibleTypes($abstractType);
698
        $promisedIsTypeOfResults = [];
699
700
        foreach ($possibleTypes as $index => $type) {
701
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
702
703
            if ($isTypeOfResult instanceof ExtendedPromiseInterface) {
704
                $promisedIsTypeOfResults[$index] = $isTypeOfResult;
705
                continue;
706
            }
707
708
            if ($isTypeOfResult === true) {
709
                return $type;
710
            }
711
712
            if (\is_array($value)) {
713
                // TODO: Make `type` configurable
714
                /** @noinspection NestedPositiveIfStatementsInspection */
715
                if (isset($value['type']) && $value['type'] === $type->getName()) {
716
                    return $type;
717
                }
718
            }
719
        }
720
721
        if (!empty($promisedIsTypeOfResults)) {
722
            return promiseAll($promisedIsTypeOfResults)
723
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
724
                    /** @noinspection ForeachSourceInspection */
725
                    foreach ($isTypeOfResults as $index => $result) {
726
                        if ($result) {
727
                            return $possibleTypes[$index];
728
                        }
729
                    }
730
731
                    return null;
732
                });
733
        }
734
735
        return null;
736
    }
737
738
    /**
739
     * @param ListType    $returnType
740
     * @param FieldNode[] $fieldNodes
741
     * @param ResolveInfo $info
742
     * @param array       $path
743
     * @param mixed       $result
744
     *
745
     * @return array|\React\Promise\Promise
746
     * @throws \Throwable
747
     */
748
    protected function completeListValue(
749
        ListType $returnType,
750
        array $fieldNodes,
751
        ResolveInfo $info,
752
        array $path,
753
        &$result
754
    ) {
755
        $itemType = $returnType->getOfType();
756
757
        $completedItems     = [];
758
        $doesContainPromise = false;
759
760
        if (!\is_array($result) && !($result instanceof \Traversable)) {
761
            /** @noinspection ThrowRawExceptionInspection */
762
            throw new \Exception(
763
                \sprintf(
764
                    'Expected Array or Traversable, but did not find one for field %s.%s.',
765
                    $info->getParentType()->getName(),
766
                    $info->getFieldName()
767
                )
768
            );
769
        }
770
771
        foreach ($result as $key => $item) {
772
            $fieldPath          = $path;
773
            $fieldPath[]        = $key;
774
            $completedItem      = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
775
            $completedItems[]   = $completedItem;
776
            $doesContainPromise = $doesContainPromise || $completedItem instanceof ExtendedPromiseInterface;
777
        }
778
779
        return $doesContainPromise
780
            ? promiseAll($completedItems)
781
            : $completedItems;
782
    }
783
784
    /**
785
     * @param LeafTypeInterface|SerializableTypeInterface $returnType
786
     * @param mixed                                       $result
787
     *
788
     * @return mixed
789
     * @throws ExecutionException
790
     */
791
    protected function completeLeafValue($returnType, &$result)
792
    {
793
        $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

793
        /** @scrutinizer ignore-call */ 
794
        $serializedResult = $returnType->serialize($result);
Loading history...
794
795
        if ($serializedResult === null) {
796
            // TODO: Make a method for this type of exception
797
            throw new ExecutionException(
798
                \sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
799
            );
800
        }
801
802
        return $serializedResult;
803
    }
804
805
    /**
806
     * @param ObjectType  $returnType
807
     * @param array       $fieldNodes
808
     * @param ResolveInfo $info
809
     * @param array       $path
810
     * @param mixed       $result
811
     *
812
     * @return array
813
     * @throws ExecutionException
814
     * @throws InvalidTypeException
815
     * @throws InvariantException
816
     * @throws \Throwable
817
     */
818
    protected function completeObjectValue(
819
        ObjectType $returnType,
820
        array $fieldNodes,
821
        ResolveInfo $info,
822
        array $path,
823
        &$result
824
    ): array {
825
        if (null !== $returnType->getIsTypeOf()) {
0 ignored issues
show
introduced by
The condition null !== $returnType->getIsTypeOf() is always true.
Loading history...
826
            $isTypeOf = $returnType->isTypeOf($result, $this->context->getContextValue(), $info);
827
828
            // TODO: Check for promise?
829
            if (!$isTypeOf) {
830
                throw new ExecutionException(
831
                    sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
832
                );
833
            }
834
        }
835
836
        return $this->executeSubFields($returnType, $fieldNodes, $path, $result);
837
    }
838
839
    /**
840
     * @param Field            $field
841
     * @param FieldNode        $fieldNode
842
     * @param callable         $resolveCallback
843
     * @param mixed            $rootValue
844
     * @param ExecutionContext $context
845
     * @param ResolveInfo      $info
846
     *
847
     * @return array|\Throwable
848
     */
849
    protected function resolveFieldValueOrError(
850
        Field $field,
851
        FieldNode $fieldNode,
852
        ?callable $resolveCallback,
853
        $rootValue,
854
        ExecutionContext $context,
855
        ResolveInfo $info
856
    ) {
857
        try {
858
            $result = $resolveCallback(
859
                $rootValue,
860
                coerceArgumentValues($field, $fieldNode, $context->getVariableValues()),
861
                $context->getContextValue(),
862
                $info
863
            );
864
        } catch (\Throwable $error) {
865
            return $error;
866
        }
867
868
        return $result;
869
    }
870
871
    /**
872
     * @param ObjectType  $returnType
873
     * @param FieldNode[] $fieldNodes
874
     * @param array       $path
875
     * @param mixed       $result
876
     *
877
     * @return array
878
     * @throws ExecutionException
879
     * @throws InvalidTypeException
880
     * @throws InvariantException
881
     * @throws \Throwable
882
     */
883
    protected function executeSubFields(
884
        ObjectType $returnType,
885
        array $fieldNodes,
886
        array $path,
887
        &$result
888
    ): array {
889
        $subFields            = [];
890
        $visitedFragmentNames = [];
891
892
        foreach ($fieldNodes as $fieldNode) {
893
            if (null !== $fieldNode->getSelectionSet()) {
894
                $subFields = $this->fieldCollector->collectFields(
895
                    $returnType,
896
                    $fieldNode->getSelectionSet(),
897
                    $subFields,
898
                    $visitedFragmentNames
899
                );
900
            }
901
        }
902
903
        if (!empty($subFields)) {
904
            return $this->executeFields($returnType, $result, $path, $subFields);
905
        }
906
907
        return $result;
908
    }
909
910
    /**
911
     * @param \Throwable $originalException
912
     * @param array      $nodes
913
     * @param array      $path
914
     *
915
     * @return ExecutionException
916
     */
917
    protected function buildLocatedError(
918
        \Throwable $originalException,
919
        array $nodes = [],
920
        array $path = []
921
    ): ExecutionException {
922
        return new ExecutionException(
923
            $originalException->getMessage(),
924
            $originalException instanceof GraphQLException
925
                ? $originalException->getNodes()
926
                : $nodes,
927
            $originalException instanceof GraphQLException
928
                ? $originalException->getSource()
929
                : null,
930
            $originalException instanceof GraphQLException
931
                ? $originalException->getPositions()
932
                : null,
933
            $originalException instanceof GraphQLException
934
                ? ($originalException->getPath() ?? $path)
935
                : $path,
936
            null,
937
            $originalException
938
        );
939
    }
940
941
    /**
942
     * @param FieldNode[]      $fieldNodes
943
     * @param FieldNode        $fieldNode
944
     * @param Field            $field
945
     * @param ObjectType       $parentType
946
     * @param array|null       $path
947
     * @param ExecutionContext $context
948
     *
949
     * @return ResolveInfo
950
     */
951
    protected function createResolveInfo(
952
        array $fieldNodes,
953
        FieldNode $fieldNode,
954
        Field $field,
955
        ObjectType $parentType,
956
        ?array $path,
957
        ExecutionContext $context
958
    ): ResolveInfo {
959
        return new ResolveInfo(
960
            $fieldNode->getNameValue(),
0 ignored issues
show
Bug introduced by
It seems like $fieldNode->getNameValue() can also be of type null; however, parameter $fieldName of Digia\GraphQL\Execution\ResolveInfo::__construct() does only seem to accept string, 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

960
            /** @scrutinizer ignore-type */ $fieldNode->getNameValue(),
Loading history...
961
            $fieldNodes,
962
            $field->getType(),
0 ignored issues
show
Bug introduced by
It seems like $field->getType() can also be of type null; however, parameter $returnType of Digia\GraphQL\Execution\ResolveInfo::__construct() does only seem to accept Digia\GraphQL\Type\Definition\TypeInterface, 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

962
            /** @scrutinizer ignore-type */ $field->getType(),
Loading history...
963
            $parentType,
964
            $path,
965
            $context->getSchema(),
966
            $context->getFragments(),
967
            $context->getRootValue(),
968
            $context->getOperation(),
969
            $context->getVariableValues()
970
        );
971
    }
972
973
    /**
974
     * @param ExecutionException $error
975
     */
976
    protected function handleError(ExecutionException $error)
977
    {
978
        $this->errorHandler->handle($error, $this->context);
0 ignored issues
show
Bug introduced by
The method handle() 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

978
        $this->errorHandler->/** @scrutinizer ignore-call */ 
979
                             handle($error, $this->context);

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...
979
    }
980
981
    /**
982
     * Try to resolve a field without any field resolver function.
983
     *
984
     * @param array|object $rootValue
985
     * @param array        $arguments
986
     * @param mixed        $contextValues
987
     * @param ResolveInfo  $info
988
     *
989
     * @return mixed|null
990
     */
991
    public static function defaultFieldResolver($rootValue, array $arguments, $contextValues, ResolveInfo $info)
992
    {
993
        $fieldName = $info->getFieldName();
994
        $property  = null;
995
996
        if (\is_array($rootValue) && isset($rootValue[$fieldName])) {
997
            $property = $rootValue[$fieldName];
998
        }
999
1000
        if (\is_object($rootValue)) {
1001
            $getter = 'get' . \ucfirst($fieldName);
1002
            if (\method_exists($rootValue, $getter)) {
1003
                $property = $rootValue->{$getter}();
1004
            } elseif (\method_exists($rootValue, $fieldName)) {
1005
                $property = $rootValue->{$fieldName}($rootValue, $arguments, $contextValues, $info);
1006
            } elseif (\property_exists($rootValue, $fieldName)) {
1007
                $property = $rootValue->{$fieldName};
1008
            }
1009
        }
1010
1011
        return $property instanceof \Closure
1012
            ? $property($rootValue, $arguments, $contextValues, $info)
1013
            : $property;
1014
    }
1015
1016
    /**
1017
     * @return ErrorHandler
1018
     */
1019
    protected function createDefaultErrorHandler(): ErrorHandler
1020
    {
1021
        return new ErrorHandler([new AddErrorMiddleware()]);
1022
    }
1023
}
1024