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

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