Passed
Push — master ( d044ba...6d4515 )
by Christoffer
02:32
created

Executor::handleError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\ErrorHandlerInterface;
6
use Digia\GraphQL\Error\ExecutionException;
7
use Digia\GraphQL\Error\GraphQLException;
8
use Digia\GraphQL\Error\InvalidTypeException;
9
use Digia\GraphQL\Error\InvariantException;
10
use Digia\GraphQL\Error\UndefinedException;
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 InvalidArgumentException;
26
use React\Promise\ExtendedPromiseInterface;
27
use React\Promise\FulfilledPromise;
28
use React\Promise\PromiseInterface;
29
use Throwable;
30
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
31
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
32
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
33
use function Digia\GraphQL\Util\toString;
34
use function React\Promise\all as promiseAll;
35
36
class Executor
37
{
38
    /**
39
     * @var ExecutionContext
40
     */
41
    protected $context;
42
43
    /**
44
     * @var OperationDefinitionNode
45
     */
46
    protected $operation;
47
48
    /**
49
     * @var mixed
50
     */
51
    protected $rootValue;
52
53
    /**
54
     * @var FieldCollector
55
     */
56
    protected $fieldCollector;
57
58
    /**
59
     * @var array
60
     */
61
    protected $finalResult;
62
63
    /**
64
     * @var ErrorHandlerInterface
65
     */
66
    protected $errorHandler;
67
68
    /**
69
     * @var array
70
     */
71
    private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
72
73
    /**
74
     * Executor constructor.
75
     * @param ExecutionContext      $context
76
     * @param FieldCollector        $fieldCollector
77
     * @param ErrorHandlerInterface $errorHandler
78
     */
79
    public function __construct(
80
        ExecutionContext $context,
81
        FieldCollector $fieldCollector,
82
        ErrorHandlerInterface $errorHandler
83
    ) {
84
        $this->context        = $context;
85
        $this->fieldCollector = $fieldCollector;
86
        $this->errorHandler   = $errorHandler;
87
    }
88
89
    /**
90
     * @return array|null
91
     * @throws ExecutionException
92
     * @throws \Throwable
93
     */
94
    public function execute(): ?array
95
    {
96
        $schema    = $this->context->getSchema();
97
        $operation = $this->context->getOperation();
98
        $rootValue = $this->context->getRootValue();
99
100
        $objectType = $this->getOperationType($schema, $operation);
101
102
        $fields               = [];
103
        $visitedFragmentNames = [];
104
        $path                 = [];
105
106
        $fields = $this->fieldCollector->collectFields(
107
            $objectType,
108
            $operation->getSelectionSet(),
109
            $fields,
110
            $visitedFragmentNames
111
        );
112
113
        try {
114
            $result = $operation->getOperation() === 'mutation'
115
                ? $this->executeFieldsSerially($objectType, $rootValue, $path, $fields)
116
                : $this->executeFields($objectType, $rootValue, $path, $fields);
117
        } catch (\Throwable $ex) {
118
            $this->handleError(new ExecutionException($ex->getMessage(), $fields, null, null, null, null, $ex));
119
            return null;
120
        }
121
122
        return $result;
123
    }
124
125
    /**
126
     * @param Schema                  $schema
127
     * @param OperationDefinitionNode $operation
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
                return $mutationType;
145
            case 'subscription':
146
                $subscriptionType = $schema->getSubscriptionType();
147
                if (null === $subscriptionType) {
148
                    throw new ExecutionException(
149
                        'Schema is not configured for subscriptions',
150
                        [$operation]
151
                    );
152
                }
153
                return $subscriptionType;
154
            default:
155
                throw new ExecutionException(
156
                    'Can only execute queries, mutations and subscriptions',
157
                    [$operation]
158
                );
159
        }
160
    }
161
162
    /**
163
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
164
     *
165
     * @param ObjectType $objectType
166
     * @param mixed      $rootValue
167
     * @param array      $path
168
     * @param array      $fields
169
     * @return array
170
     * @throws InvalidArgumentException
171
     * @throws Throwable
172
     */
173
    public function executeFieldsSerially(
174
        ObjectType $objectType,
175
        $rootValue,
176
        array $path,
177
        array $fields
178
    ): array {
179
        $finalResults = [];
180
181
        $promise = new FulfilledPromise([]);
182
183
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
184
            $fieldPath   = $path;
185
            $fieldPath[] = $fieldName;
186
            try {
187
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
188
            } catch (UndefinedException $ex) {
189
                return null;
190
            }
191
192
            if ($this->isPromise($result)) {
193
                /** @var ExtendedPromiseInterface $result */
194
                return $result->then(function ($resolvedResult) use ($fieldName, $results) {
195
                    $results[$fieldName] = $resolvedResult;
196
                    return $results;
197
                });
198
            }
199
200
            $results[$fieldName] = $result;
201
202
            return $results;
203
        };
204
205
        foreach ($fields as $fieldName => $fieldNodes) {
206
            $promise = $promise->then(function ($resolvedResults) use (
207
                $resolve,
208
                $fieldName,
209
                $path,
210
                $objectType,
211
                $rootValue,
212
                $fieldNodes
213
            ) {
214
                return $resolve($resolvedResults, $fieldName, $path, $objectType, $rootValue, $fieldNodes);
215
            });
216
        }
217
218
        $promise->then(function ($resolvedResults) use (&$finalResults) {
219
            $finalResults = $resolvedResults ?? [];
220
        })->otherwise(function ($ex) {
221
            $this->context->addError($ex);
222
        });
223
224
        return $finalResults;
225
    }
226
227
    /**
228
     * @param Schema     $schema
229
     * @param ObjectType $parentType
230
     * @param string     $fieldName
231
     * @return Field|null
232
     */
233
    public function getFieldDefinition(
234
        Schema $schema,
235
        ObjectType $parentType,
236
        string $fieldName
237
    ): ?Field {
238
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
239
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
240
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
241
242
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
243
            return $schemaMetaFieldDefinition;
244
        }
245
246
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
247
            return $typeMetaFieldDefinition;
248
        }
249
250
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
251
            return $typeNameMetaFieldDefinition;
252
        }
253
254
        $fields = $parentType->getFields();
255
256
        return $fields[$fieldName] ?? null;
257
    }
258
259
    /**
260
     * @param TypeInterface $fieldType
261
     * @param FieldNode[]   $fieldNodes
262
     * @param ResolveInfo   $info
263
     * @param array         $path
264
     * @param mixed         $result
265
     * @return array|mixed|null
266
     * @throws \Throwable
267
     */
268
    public function completeValueCatchingError(
269
        TypeInterface $fieldType,
270
        array $fieldNodes,
271
        ResolveInfo $info,
272
        array $path,
273
        &$result
274
    ) {
275
        if ($fieldType instanceof NonNullType) {
276
            return $this->completeValueWithLocatedError(
277
                $fieldType,
278
                $fieldNodes,
279
                $info,
280
                $path,
281
                $result
282
            );
283
        }
284
285
        try {
286
            $completed = $this->completeValueWithLocatedError(
287
                $fieldType,
288
                $fieldNodes,
289
                $info,
290
                $path,
291
                $result
292
            );
293
294
            if ($this->isPromise($completed)) {
295
                $context = $this->context;
296
                /** @var ExtendedPromiseInterface $completed */
297
                return $completed->then(null, function ($error) use ($context, $fieldNodes, $path) {
298
                    //@TODO Handle $error better
299
                    if ($error instanceof \Exception) {
300
                        $context->addError($this->buildLocatedError($error, $fieldNodes, $path));
301
                    } else {
302
                        $context->addError(
303
                            $this->buildLocatedError(
304
                                new ExecutionException($error ?? 'An unknown error occurred.'),
305
                                $fieldNodes,
306
                                $path
307
                            )
308
                        );
309
                    }
310
                    return new FulfilledPromise(null);
311
                });
312
            }
313
314
            return $completed;
315
        } catch (\Throwable $ex) {
316
            $this->handleError($this->buildLocatedError($ex, $fieldNodes, $path));
317
            return null;
318
        }
319
    }
320
321
322
    /**
323
     * @param TypeInterface $fieldType
324
     * @param FieldNode[]   $fieldNodes
325
     * @param ResolveInfo   $info
326
     * @param array         $path
327
     * @param mixed         $result
328
     * @return array|mixed
329
     * @throws \Throwable
330
     */
331
    public function completeValueWithLocatedError(
332
        TypeInterface $fieldType,
333
        array $fieldNodes,
334
        ResolveInfo $info,
335
        array $path,
336
        $result
337
    ) {
338
        try {
339
            $completed = $this->completeValue(
340
                $fieldType,
341
                $fieldNodes,
342
                $info,
343
                $path,
344
                $result
345
            );
346
347
            return $completed;
348
        } catch (\Throwable $ex) {
349
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
350
        }
351
    }
352
353
    /**
354
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
355
     *
356
     * @param ObjectType $objectType
357
     * @param mixed      $rootValue
358
     * @param array      $path
359
     * @param array      $fields
360
     * @return array
361
     * @throws \Throwable
362
     */
363
    protected function executeFields(
364
        ObjectType $objectType,
365
        $rootValue,
366
        array $path,
367
        array $fields
368
    ): array {
369
        $finalResults       = [];
370
        $doesContainPromise = false;
371
372
        foreach ($fields as $fieldName => $fieldNodes) {
373
            $fieldPath   = $path;
374
            $fieldPath[] = $fieldName;
375
376
            try {
377
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
378
            } catch (UndefinedException $ex) {
379
                continue;
380
            }
381
382
            $doesContainPromise = $doesContainPromise || $this->isPromise($result);
383
384
            $finalResults[$fieldName] = $result;
385
        }
386
387
        if ($doesContainPromise) {
388
            $keys    = \array_keys($finalResults);
389
            $promise = promiseAll(\array_values($finalResults));
390
391
            $promise->then(function ($values) use ($keys, &$finalResults) {
392
                /** @noinspection ForeachSourceInspection */
393
                foreach ($values as $i => $value) {
394
                    $finalResults[$keys[$i]] = $value;
395
                }
396
            });
397
        }
398
399
        return $finalResults;
400
    }
401
402
    /**
403
     * @param ObjectType  $parentType
404
     * @param mixed       $rootValue
405
     * @param FieldNode[] $fieldNodes
406
     * @param array       $path
407
     * @return array|mixed|null
408
     * @throws UndefinedException
409
     * @throws Throwable
410
     */
411
    protected function resolveField(
412
        ObjectType $parentType,
413
        $rootValue,
414
        array $fieldNodes,
415
        array $path
416
    ) {
417
        /** @var FieldNode $fieldNode */
418
        $fieldNode = $fieldNodes[0];
419
420
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
421
422
        if (null === $field) {
423
            throw new UndefinedException('Undefined field definition.');
424
        }
425
426
        $info = $this->createResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
427
428
        $resolveCallback = $this->determineResolveCallback($field, $parentType);
429
430
        $result = $this->resolveFieldValueOrError(
431
            $field,
432
            $fieldNode,
433
            $resolveCallback,
434
            $rootValue,
435
            $this->context,
436
            $info
437
        );
438
439
        $result = $this->completeValueCatchingError(
440
            $field->getType(),
441
            $fieldNodes,
442
            $info,
443
            $path,
444
            $result
445
        );
446
447
        return $result;
448
    }
449
450
    /**
451
     * @param Field      $field
452
     * @param ObjectType $objectType
453
     * @return callable|mixed|null
454
     */
455
    protected function determineResolveCallback(Field $field, ObjectType $objectType)
456
    {
457
        if ($field->hasResolveCallback()) {
458
            return $field->getResolveCallback();
459
        }
460
461
        if ($objectType->hasResolveCallback()) {
462
            return $objectType->getResolveCallback();
463
        }
464
465
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
466
    }
467
468
    /**
469
     * @param TypeInterface $returnType
470
     * @param FieldNode[]   $fieldNodes
471
     * @param ResolveInfo   $info
472
     * @param array         $path
473
     * @param mixed         $result
474
     * @return array|mixed
475
     * @throws InvariantException
476
     * @throws InvalidTypeException
477
     * @throws ExecutionException
478
     * @throws \Throwable
479
     */
480
    protected function completeValue(
481
        TypeInterface $returnType,
482
        array $fieldNodes,
483
        ResolveInfo $info,
484
        array $path,
485
        &$result
486
    ) {
487
        if ($this->isPromise($result)) {
488
            /** @var ExtendedPromiseInterface $result */
489
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
490
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
491
            });
492
        }
493
494
        if ($result instanceof \Throwable) {
495
            throw $result;
496
        }
497
498
        // If result is null-like, return null.
499
        if (null === $result) {
500
            return null;
501
        }
502
503
        if ($returnType instanceof NonNullType) {
504
            $completed = $this->completeValue(
505
                $returnType->getOfType(),
506
                $fieldNodes,
507
                $info,
508
                $path,
509
                $result
510
            );
511
512
            if ($completed === null) {
513
                throw new ExecutionException(
514
                    \sprintf(
515
                        'Cannot return null for non-nullable field %s.%s.',
516
                        $info->getParentType(),
517
                        $info->getFieldName()
518
                    )
519
                );
520
            }
521
522
            return $completed;
523
        }
524
525
        // If field type is List, complete each item in the list with the inner type
526
        if ($returnType instanceof ListType) {
527
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
528
        }
529
530
        // If field type is Scalar or Enum, serialize to a valid value, returning
531
        // null if serialization is not possible.
532
        if ($returnType instanceof LeafTypeInterface) {
533
            return $this->completeLeafValue($returnType, $result);
534
        }
535
536
        // TODO: Make a function for checking abstract type?
537
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
538
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
539
        }
540
541
        // Field type must be Object, Interface or Union and expect sub-selections.
542
        if ($returnType instanceof ObjectType) {
543
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
544
        }
545
546
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
547
    }
548
549
    /**
550
     * @param AbstractTypeInterface $returnType
551
     * @param FieldNode[]           $fieldNodes
552
     * @param ResolveInfo           $info
553
     * @param array                 $path
554
     * @param mixed                 $result
555
     * @return array|PromiseInterface
556
     * @throws ExecutionException
557
     * @throws InvalidTypeException
558
     * @throws InvariantException
559
     * @throws \Throwable
560
     */
561
    protected function completeAbstractValue(
562
        AbstractTypeInterface $returnType,
563
        array $fieldNodes,
564
        ResolveInfo $info,
565
        array $path,
566
        &$result
567
    ) {
568
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
569
570
        if (null === $runtimeType) {
571
            // TODO: Display warning
572
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
573
        }
574
575
        if ($this->isPromise($runtimeType)) {
576
            /** @var ExtendedPromiseInterface $runtimeType */
577
            return $runtimeType->then(function ($resolvedRuntimeType) use (
578
                $returnType,
579
                $fieldNodes,
580
                $info,
581
                $path,
582
                &$result
583
            ) {
584
                return $this->completeObjectValue(
585
                    $this->ensureValidRuntimeType($resolvedRuntimeType, $returnType, $info, $result),
586
                    $fieldNodes,
587
                    $info,
588
                    $path,
589
                    $result
590
                );
591
            });
592
        }
593
594
        return $this->completeObjectValue(
595
            $this->ensureValidRuntimeType($runtimeType, $returnType, $info, $result),
596
            $fieldNodes,
597
            $info,
598
            $path,
599
            $result
600
        );
601
    }
602
603
    /**
604
     * @param NamedTypeInterface|string $runtimeTypeOrName
605
     * @param NamedTypeInterface        $returnType
606
     * @param ResolveInfo               $info
607
     * @param mixed                     $result
608
     * @return TypeInterface|ObjectType|null
609
     * @throws ExecutionException
610
     * @throws InvariantException
611
     */
612
    protected function ensureValidRuntimeType(
613
        $runtimeTypeOrName,
614
        NamedTypeInterface $returnType,
615
        ResolveInfo $info,
616
        &$result
617
    ) {
618
        /** @var NamedTypeInterface $runtimeType */
619
        $runtimeType = \is_string($runtimeTypeOrName)
620
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
621
            : $runtimeTypeOrName;
622
623
        $runtimeTypeName = $runtimeType->getName();
624
        $returnTypeName  = $returnType->getName();
625
626
        if (!$runtimeType instanceof ObjectType) {
627
            $parentTypeName = $info->getParentType()->getName();
628
            $fieldName      = $info->getFieldName();
629
630
            throw new ExecutionException(
631
                \sprintf(
632
                    'Abstract type %s must resolve to an Object type at runtime for field %s.%s ' .
633
                    'with value "%s", received "%s".',
634
                    $returnTypeName,
635
                    $parentTypeName,
636
                    $fieldName,
637
                    $result,
638
                    $runtimeTypeName
639
                )
640
            );
641
        }
642
643
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
644
            throw new ExecutionException(
645
                \sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeTypeName, $returnTypeName)
646
            );
647
        }
648
649
        if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
0 ignored issues
show
introduced by
The condition $runtimeType is always true. If $runtimeType can have other possible types, add them to src/Execution/Executor.php:618
Loading history...
650
            throw new ExecutionException(
651
                \sprintf(
652
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
653
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
654
                    'type instance as referenced anywhere else within the schema.',
655
                    $runtimeTypeName,
656
                    $returnTypeName
657
                )
658
            );
659
        }
660
661
        return $runtimeType;
662
    }
663
664
    /**
665
     * @param mixed                 $value
666
     * @param mixed                 $context
667
     * @param ResolveInfo           $info
668
     * @param AbstractTypeInterface $abstractType
669
     * @return NamedTypeInterface|mixed|null
670
     * @throws InvariantException
671
     */
672
    protected function defaultTypeResolver(
673
        $value,
674
        $context,
675
        ResolveInfo $info,
676
        AbstractTypeInterface $abstractType
677
    ) {
678
        if (\is_array($value) && isset($value['__typename'])) {
679
            return $value['__typename'];
680
        }
681
682
        /** @var ObjectType[] $possibleTypes */
683
        $possibleTypes           = $info->getSchema()->getPossibleTypes($abstractType);
684
        $promisedIsTypeOfResults = [];
685
686
        foreach ($possibleTypes as $index => $type) {
687
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
688
689
            if ($this->isPromise($isTypeOfResult)) {
690
                $promisedIsTypeOfResults[$index] = $isTypeOfResult;
691
                continue;
692
            }
693
694
            if ($isTypeOfResult === true) {
695
                return $type;
696
            }
697
698
            if (\is_array($value)) {
699
                // TODO: Make `type` configurable
700
                /** @noinspection NestedPositiveIfStatementsInspection */
701
                if (isset($value['type']) && $value['type'] === $type->getName()) {
702
                    return $type;
703
                }
704
            }
705
        }
706
707
        if (!empty($promisedIsTypeOfResults)) {
708
            return promiseAll($promisedIsTypeOfResults)
709
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
710
                    /** @noinspection ForeachSourceInspection */
711
                    foreach ($isTypeOfResults as $index => $result) {
712
                        if ($result) {
713
                            return $possibleTypes[$index];
714
                        }
715
                    }
716
                    return null;
717
                });
718
        }
719
720
        return null;
721
    }
722
723
    /**
724
     * @param ListType    $returnType
725
     * @param FieldNode[] $fieldNodes
726
     * @param ResolveInfo $info
727
     * @param array       $path
728
     * @param mixed       $result
729
     * @return array|\React\Promise\Promise
730
     * @throws \Throwable
731
     */
732
    protected function completeListValue(
733
        ListType $returnType,
734
        array $fieldNodes,
735
        ResolveInfo $info,
736
        array $path,
737
        &$result
738
    ) {
739
        $itemType = $returnType->getOfType();
740
741
        $completedItems     = [];
742
        $doesContainPromise = false;
743
744
        if (!\is_array($result) && !($result instanceof \Traversable)) {
745
            /** @noinspection ThrowRawExceptionInspection */
746
            throw new \Exception(
747
                \sprintf(
748
                    'Expected Array or Traversable, but did not find one for field %s.%s.',
749
                    $info->getParentType()->getName(),
750
                    $info->getFieldName()
751
                )
752
            );
753
        }
754
755
        foreach ($result as $key => $item) {
756
            $fieldPath          = $path;
757
            $fieldPath[]        = $key;
758
            $completedItem      = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
759
            $completedItems[]   = $completedItem;
760
            $doesContainPromise = $doesContainPromise || $this->isPromise($completedItem);
761
        }
762
763
        return $doesContainPromise
764
            ? promiseAll($completedItems)
765
            : $completedItems;
766
    }
767
768
    /**
769
     * @param LeafTypeInterface|SerializableTypeInterface $returnType
770
     * @param mixed                                       $result
771
     * @return mixed
772
     * @throws ExecutionException
773
     */
774
    protected function completeLeafValue($returnType, &$result)
775
    {
776
        $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

776
        /** @scrutinizer ignore-call */ 
777
        $serializedResult = $returnType->serialize($result);
Loading history...
777
778
        if ($serializedResult === null) {
779
            // TODO: Make a method for this type of exception
780
            throw new ExecutionException(
781
                \sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
782
            );
783
        }
784
785
        return $serializedResult;
786
    }
787
788
    /**
789
     * @param ObjectType  $returnType
790
     * @param array       $fieldNodes
791
     * @param ResolveInfo $info
792
     * @param array       $path
793
     * @param mixed       $result
794
     * @return array
795
     * @throws ExecutionException
796
     * @throws InvalidTypeException
797
     * @throws InvariantException
798
     * @throws \Throwable
799
     */
800
    protected function completeObjectValue(
801
        ObjectType $returnType,
802
        array $fieldNodes,
803
        ResolveInfo $info,
804
        array $path,
805
        &$result
806
    ): array {
807
        if (null !== $returnType->getIsTypeOf()) {
0 ignored issues
show
introduced by
The condition null !== $returnType->getIsTypeOf() is always true.
Loading history...
808
            $isTypeOf = $returnType->isTypeOf($result, $this->context->getContextValue(), $info);
809
810
            // TODO: Check for promise?
811
            if (!$isTypeOf) {
812
                throw new ExecutionException(
813
                    sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
814
                );
815
            }
816
        }
817
818
        return $this->executeSubFields($returnType, $fieldNodes, $path, $result);
819
    }
820
821
    /**
822
     * @param Field            $field
823
     * @param FieldNode        $fieldNode
824
     * @param callable         $resolveCallback
825
     * @param mixed            $rootValue
826
     * @param ExecutionContext $context
827
     * @param ResolveInfo      $info
828
     * @return array|\Throwable
829
     */
830
    protected function resolveFieldValueOrError(
831
        Field $field,
832
        FieldNode $fieldNode,
833
        ?callable $resolveCallback,
834
        $rootValue,
835
        ExecutionContext $context,
836
        ResolveInfo $info
837
    ) {
838
        try {
839
            $result = $resolveCallback(
840
                $rootValue,
841
                coerceArgumentValues($field, $fieldNode, $context->getVariableValues()),
842
                $context->getContextValue(),
843
                $info
844
            );
845
        } catch (\Throwable $error) {
846
            return $error;
847
        }
848
849
        return $result;
850
    }
851
852
    /**
853
     * @param ObjectType  $returnType
854
     * @param FieldNode[] $fieldNodes
855
     * @param array       $path
856
     * @param mixed       $result
857
     * @return array
858
     * @throws ExecutionException
859
     * @throws InvalidTypeException
860
     * @throws InvariantException
861
     * @throws Throwable
862
     */
863
    protected function executeSubFields(
864
        ObjectType $returnType,
865
        array $fieldNodes,
866
        array $path,
867
        &$result
868
    ): array {
869
        $subFields            = [];
870
        $visitedFragmentNames = [];
871
872
        foreach ($fieldNodes as $fieldNode) {
873
            if (null !== $fieldNode->getSelectionSet()) {
874
                $subFields = $this->fieldCollector->collectFields(
875
                    $returnType,
876
                    $fieldNode->getSelectionSet(),
877
                    $subFields,
878
                    $visitedFragmentNames
879
                );
880
            }
881
        }
882
883
        if (!empty($subFields)) {
884
            return $this->executeFields($returnType, $result, $path, $subFields);
885
        }
886
887
        return $result;
888
    }
889
890
    /**
891
     * @param mixed $value
892
     * @return bool
893
     */
894
    protected function isPromise($value): bool
895
    {
896
        return $value instanceof ExtendedPromiseInterface;
897
    }
898
899
    /**
900
     * @param \Throwable $originalException
901
     * @param array      $nodes
902
     * @param array      $path
903
     * @return ExecutionException
904
     */
905
    protected function buildLocatedError(
906
        \Throwable $originalException,
907
        array $nodes = [],
908
        array $path = []
909
    ): ExecutionException {
910
        return new ExecutionException(
911
            $originalException->getMessage(),
912
            $originalException instanceof GraphQLException
913
                ? $originalException->getNodes()
914
                : $nodes,
915
            $originalException instanceof GraphQLException
916
                ? $originalException->getSource()
917
                : null,
918
            $originalException instanceof GraphQLException
919
                ? $originalException->getPositions()
920
                : null,
921
            $originalException instanceof GraphQLException
922
                ? ($originalException->getPath() ?? $path)
923
                : $path,
924
            null,
925
            $originalException
926
        );
927
    }
928
929
    /**
930
     * @param FieldNode[]      $fieldNodes
931
     * @param FieldNode        $fieldNode
932
     * @param Field            $field
933
     * @param ObjectType       $parentType
934
     * @param array|null       $path
935
     * @param ExecutionContext $context
936
     * @return ResolveInfo
937
     */
938
    protected function createResolveInfo(
939
        array $fieldNodes,
940
        FieldNode $fieldNode,
941
        Field $field,
942
        ObjectType $parentType,
943
        ?array $path,
944
        ExecutionContext $context
945
    ): ResolveInfo {
946
        return new ResolveInfo(
947
            $fieldNode->getNameValue(),
948
            $fieldNodes,
949
            $field->getType(),
950
            $parentType,
951
            $path,
952
            $context->getSchema(),
953
            $context->getFragments(),
954
            $context->getRootValue(),
955
            $context->getOperation(),
956
            $context->getVariableValues()
957
        );
958
    }
959
960
    /**
961
     * @param ExecutionException $error
962
     */
963
    protected function handleError(ExecutionException $error)
964
    {
965
        $this->errorHandler->handleError($error);
966
        $this->context->addError($error);
967
    }
968
969
    /**
970
     * Try to resolve a field without any field resolver function.
971
     *
972
     * @param array|object $rootValue
973
     * @param array        $arguments
974
     * @param mixed        $contextValues
975
     * @param ResolveInfo  $info
976
     * @return mixed|null
977
     */
978
    public static function defaultFieldResolver($rootValue, array $arguments, $contextValues, ResolveInfo $info)
979
    {
980
        $fieldName = $info->getFieldName();
981
        $property  = null;
982
983
        if (\is_array($rootValue) && isset($rootValue[$fieldName])) {
984
            $property = $rootValue[$fieldName];
985
        }
986
987
        if (\is_object($rootValue)) {
988
            $getter = 'get' . \ucfirst($fieldName);
989
            if (\method_exists($rootValue, $getter)) {
990
                $property = $rootValue->{$getter}();
991
            } elseif (\method_exists($rootValue, $fieldName)) {
992
                $property = $rootValue->{$fieldName}($rootValue, $arguments, $contextValues, $info);
993
            } elseif (\property_exists($rootValue, $fieldName)) {
994
                $property = $rootValue->{$fieldName};
995
            }
996
        }
997
998
        return $property instanceof \Closure
999
            ? $property($rootValue, $arguments, $contextValues, $info)
1000
            : $property;
1001
    }
1002
}
1003