Completed
Pull Request — master (#297)
by Christoffer
02:31
created

Executor::isPromise()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
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 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;
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,
107
            $operation->getSelectionSet(),
108
            $fields,
109
            $visitedFragmentNames
110
        );
111
112
        try {
113
            $result = $operation->getOperation() === 'mutation'
114
                ? $this->executeFieldsSerially($objectType, $rootValue, $path, $fields)
115
                : $this->executeFields($objectType, $rootValue, $path, $fields);
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 (ExecutionException $ex) {
222
            $this->handleError($ex);
223
        });
224
225
        return $finalResults;
226
    }
227
228
    /**
229
     * @param Schema     $schema
230
     * @param ObjectType $parentType
231
     * @param string     $fieldName
232
     *
233
     * @return Field|null
234
     */
235
    public function getFieldDefinition(
236
        Schema $schema,
237
        ObjectType $parentType,
238
        string $fieldName
239
    ): ?Field {
240
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
241
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
242
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
243
244
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
245
            return $schemaMetaFieldDefinition;
246
        }
247
248
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
249
            return $typeMetaFieldDefinition;
250
        }
251
252
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
253
            return $typeNameMetaFieldDefinition;
254
        }
255
256
        /** @noinspection PhpUnhandledExceptionInspection */
257
        $fields = $parentType->getFields();
258
259
        return $fields[$fieldName] ?? null;
260
    }
261
262
    /**
263
     * @param TypeInterface $fieldType
264
     * @param FieldNode[]   $fieldNodes
265
     * @param ResolveInfo   $info
266
     * @param array         $path
267
     * @param mixed         $result
268
     *
269
     * @return array|mixed|null
270
     * @throws \Throwable
271
     */
272
    public function completeValueCatchingError(
273
        TypeInterface $fieldType,
274
        array $fieldNodes,
275
        ResolveInfo $info,
276
        array $path,
277
        &$result
278
    ) {
279
        if ($fieldType instanceof NonNullType) {
280
            return $this->completeValueWithLocatedError(
281
                $fieldType,
282
                $fieldNodes,
283
                $info,
284
                $path,
285
                $result
286
            );
287
        }
288
289
        try {
290
            $completed = $this->completeValueWithLocatedError(
291
                $fieldType,
292
                $fieldNodes,
293
                $info,
294
                $path,
295
                $result
296
            );
297
298
            if ($completed instanceof ExtendedPromiseInterface) {
299
                return $completed->then(null, function ($error) use ($fieldNodes, $path) {
300
                    if ($error instanceof \Exception) {
301
                        $this->handleError(
302
                            $this->buildLocatedError($error, $fieldNodes, $path)
303
                        );
304
                    } else {
305
                        $this->handleError(
306
                            $this->buildLocatedError(
307
                                new ExecutionException($error ?? 'An unknown error occurred.'),
308
                                $fieldNodes,
309
                                $path
310
                            )
311
                        );
312
                    }
313
314
                    return new FulfilledPromise(null);
315
                });
316
            }
317
318
            return $completed;
319
        } catch (\Throwable $ex) {
320
            $this->handleError($this->buildLocatedError($ex, $fieldNodes, $path));
321
            return null;
322
        }
323
    }
324
325
    /**
326
     * @param TypeInterface $fieldType
327
     * @param FieldNode[]   $fieldNodes
328
     * @param ResolveInfo   $info
329
     * @param array         $path
330
     * @param mixed         $result
331
     *
332
     * @return array|mixed
333
     * @throws \Throwable
334
     */
335
    public function completeValueWithLocatedError(
336
        TypeInterface $fieldType,
337
        array $fieldNodes,
338
        ResolveInfo $info,
339
        array $path,
340
        $result
341
    ) {
342
        try {
343
            $completed = $this->completeValue(
344
                $fieldType,
345
                $fieldNodes,
346
                $info,
347
                $path,
348
                $result
349
            );
350
351
            return $completed;
352
        } catch (\Throwable $ex) {
353
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
354
        }
355
    }
356
357
    /**
358
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
359
     *
360
     * @param ObjectType $objectType
361
     * @param mixed      $rootValue
362
     * @param array      $path
363
     * @param array      $fields
364
     *
365
     * @return array
366
     * @throws ExecutionException
367
     * @throws \Throwable
368
     */
369
    protected function executeFields(
370
        ObjectType $objectType,
371
        $rootValue,
372
        array $path,
373
        array $fields
374
    ): array {
375
        $finalResults       = [];
376
        $doesContainPromise = false;
377
378
        foreach ($fields as $fieldName => $fieldNodes) {
379
            $fieldPath   = $path;
380
            $fieldPath[] = $fieldName;
381
382
            try {
383
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
384
            } catch (UndefinedException $ex) {
385
                continue;
386
            }
387
388
            $doesContainPromise = $doesContainPromise || $result instanceof ExtendedPromiseInterface;
389
390
            $finalResults[$fieldName] = $result;
391
        }
392
393
        if ($doesContainPromise) {
394
            $keys    = \array_keys($finalResults);
395
            $promise = promiseAll(\array_values($finalResults));
396
397
            $promise->then(function ($values) use ($keys, &$finalResults) {
398
                /** @noinspection ForeachSourceInspection */
399
                foreach ($values as $i => $value) {
400
                    $finalResults[$keys[$i]] = $value;
401
                }
402
            });
403
        }
404
405
        return $finalResults;
406
    }
407
408
    /**
409
     * @param ObjectType  $parentType
410
     * @param mixed       $rootValue
411
     * @param FieldNode[] $fieldNodes
412
     * @param array       $path
413
     *
414
     * @return array|mixed|null
415
     * @throws UndefinedException
416
     * @throws \Throwable
417
     */
418
    protected function resolveField(
419
        ObjectType $parentType,
420
        $rootValue,
421
        array $fieldNodes,
422
        array $path
423
    ) {
424
        /** @var FieldNode $fieldNode */
425
        $fieldNode = $fieldNodes[0];
426
427
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
428
429
        if (null === $field) {
430
            throw new UndefinedException('Undefined field definition.');
431
        }
432
433
        $info = $this->createResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
434
435
        $resolveCallback = $this->determineResolveCallback($field, $parentType);
436
437
        $result = $this->resolveFieldValueOrError(
438
            $field,
439
            $fieldNode,
440
            $resolveCallback,
441
            $rootValue,
442
            $this->context,
443
            $info
444
        );
445
446
        $result = $this->completeValueCatchingError(
447
            $field->getType(),
448
            $fieldNodes,
449
            $info,
450
            $path,
451
            $result
452
        );
453
454
        return $result;
455
    }
456
457
    /**
458
     * @param Field      $field
459
     * @param ObjectType $objectType
460
     *
461
     * @return callable|mixed|null
462
     */
463
    protected function determineResolveCallback(Field $field, ObjectType $objectType)
464
    {
465
        if ($field->hasResolveCallback()) {
466
            return $field->getResolveCallback();
467
        }
468
469
        if ($objectType->hasResolveCallback()) {
470
            return $objectType->getResolveCallback();
471
        }
472
473
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
474
    }
475
476
    /**
477
     * @param TypeInterface $returnType
478
     * @param FieldNode[]   $fieldNodes
479
     * @param ResolveInfo   $info
480
     * @param array         $path
481
     * @param mixed         $result
482
     *
483
     * @return array|mixed
484
     * @throws InvariantException
485
     * @throws InvalidTypeException
486
     * @throws ExecutionException
487
     * @throws \Throwable
488
     */
489
    protected function completeValue(
490
        TypeInterface $returnType,
491
        array $fieldNodes,
492
        ResolveInfo $info,
493
        array $path,
494
        &$result
495
    ) {
496
        if ($result instanceof ExtendedPromiseInterface) {
497
            /** @var ExtendedPromiseInterface $result */
498
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
499
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
500
            });
501
        }
502
503
        if ($result instanceof \Throwable) {
504
            throw $result;
505
        }
506
507
        // If result is null-like, return null.
508
        if (null === $result) {
509
            return null;
510
        }
511
512
        if ($returnType instanceof NonNullType) {
513
            $completed = $this->completeValue(
514
                $returnType->getOfType(),
515
                $fieldNodes,
516
                $info,
517
                $path,
518
                $result
519
            );
520
521
            if ($completed === null) {
522
                throw new ExecutionException(
523
                    \sprintf(
524
                        'Cannot return null for non-nullable field %s.%s.',
525
                        $info->getParentType(),
526
                        $info->getFieldName()
527
                    )
528
                );
529
            }
530
531
            return $completed;
532
        }
533
534
        // If field type is List, complete each item in the list with the inner type
535
        if ($returnType instanceof ListType) {
536
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
537
        }
538
539
        // If field type is Scalar or Enum, serialize to a valid value, returning
540
        // null if serialization is not possible.
541
        if ($returnType instanceof LeafTypeInterface) {
542
            return $this->completeLeafValue($returnType, $result);
543
        }
544
545
        // TODO: Make a function for checking abstract type?
546
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
547
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
548
        }
549
550
        // Field type must be Object, Interface or Union and expect sub-selections.
551
        if ($returnType instanceof ObjectType) {
552
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
553
        }
554
555
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
556
    }
557
558
    /**
559
     * @param AbstractTypeInterface $returnType
560
     * @param FieldNode[]           $fieldNodes
561
     * @param ResolveInfo           $info
562
     * @param array                 $path
563
     * @param mixed                 $result
564
     *
565
     * @return array|PromiseInterface
566
     * @throws ExecutionException
567
     * @throws InvalidTypeException
568
     * @throws InvariantException
569
     * @throws \Throwable
570
     */
571
    protected function completeAbstractValue(
572
        AbstractTypeInterface $returnType,
573
        array $fieldNodes,
574
        ResolveInfo $info,
575
        array $path,
576
        &$result
577
    ) {
578
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
579
580
        if (null === $runtimeType) {
581
            // TODO: Display warning
582
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
583
        }
584
585
        if ($runtimeType instanceof ExtendedPromiseInterface) {
586
            return $runtimeType->then(function ($resolvedRuntimeType) use (
587
                $returnType,
588
                $fieldNodes,
589
                $info,
590
                $path,
591
                &$result
592
            ) {
593
                return $this->completeObjectValue(
594
                    $this->ensureValidRuntimeType($resolvedRuntimeType, $returnType, $info, $result),
595
                    $fieldNodes,
596
                    $info,
597
                    $path,
598
                    $result
599
                );
600
            });
601
        }
602
603
        return $this->completeObjectValue(
604
            $this->ensureValidRuntimeType($runtimeType, $returnType, $info, $result),
605
            $fieldNodes,
606
            $info,
607
            $path,
608
            $result
609
        );
610
    }
611
612
    /**
613
     * @param NamedTypeInterface|string $runtimeTypeOrName
614
     * @param NamedTypeInterface        $returnType
615
     * @param ResolveInfo               $info
616
     * @param mixed                     $result
617
     *
618
     * @return TypeInterface|ObjectType|null
619
     * @throws ExecutionException
620
     * @throws InvariantException
621
     */
622
    protected function ensureValidRuntimeType(
623
        $runtimeTypeOrName,
624
        NamedTypeInterface $returnType,
625
        ResolveInfo $info,
626
        &$result
627
    ) {
628
        /** @var NamedTypeInterface $runtimeType */
629
        $runtimeType = \is_string($runtimeTypeOrName)
630
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
631
            : $runtimeTypeOrName;
632
633
        $runtimeTypeName = $runtimeType->getName();
634
        $returnTypeName  = $returnType->getName();
635
636
        if (!$runtimeType instanceof ObjectType) {
637
            $parentTypeName = $info->getParentType()->getName();
638
            $fieldName      = $info->getFieldName();
639
640
            throw new ExecutionException(
641
                \sprintf(
642
                    'Abstract type %s must resolve to an Object type at runtime for field %s.%s ' .
643
                    'with value "%s", received "%s".',
644
                    $returnTypeName,
645
                    $parentTypeName,
646
                    $fieldName,
647
                    $result,
648
                    $runtimeTypeName
649
                )
650
            );
651
        }
652
653
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
654
            throw new ExecutionException(
655
                \sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeTypeName, $returnTypeName)
656
            );
657
        }
658
659
        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:628
Loading history...
660
            throw new ExecutionException(
661
                \sprintf(
662
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
663
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
664
                    'type instance as referenced anywhere else within the schema.',
665
                    $runtimeTypeName,
666
                    $returnTypeName
667
                )
668
            );
669
        }
670
671
        return $runtimeType;
672
    }
673
674
    /**
675
     * @param mixed                 $value
676
     * @param mixed                 $context
677
     * @param ResolveInfo           $info
678
     * @param AbstractTypeInterface $abstractType
679
     *
680
     * @return NamedTypeInterface|mixed|null
681
     * @throws InvariantException
682
     */
683
    protected function defaultTypeResolver(
684
        $value,
685
        $context,
686
        ResolveInfo $info,
687
        AbstractTypeInterface $abstractType
688
    ) {
689
        if (\is_array($value) && isset($value['__typename'])) {
690
            return $value['__typename'];
691
        }
692
693
        /** @var ObjectType[] $possibleTypes */
694
        $possibleTypes           = $info->getSchema()->getPossibleTypes($abstractType);
695
        $promisedIsTypeOfResults = [];
696
697
        foreach ($possibleTypes as $index => $type) {
698
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
699
700
            if ($isTypeOfResult instanceof ExtendedPromiseInterface) {
701
                $promisedIsTypeOfResults[$index] = $isTypeOfResult;
702
                continue;
703
            }
704
705
            if ($isTypeOfResult === true) {
706
                return $type;
707
            }
708
709
            if (\is_array($value)) {
710
                // TODO: Make `type` configurable
711
                /** @noinspection NestedPositiveIfStatementsInspection */
712
                if (isset($value['type']) && $value['type'] === $type->getName()) {
713
                    return $type;
714
                }
715
            }
716
        }
717
718
        if (!empty($promisedIsTypeOfResults)) {
719
            return promiseAll($promisedIsTypeOfResults)
720
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
721
                    /** @noinspection ForeachSourceInspection */
722
                    foreach ($isTypeOfResults as $index => $result) {
723
                        if ($result) {
724
                            return $possibleTypes[$index];
725
                        }
726
                    }
727
728
                    return null;
729
                });
730
        }
731
732
        return null;
733
    }
734
735
    /**
736
     * @param ListType    $returnType
737
     * @param FieldNode[] $fieldNodes
738
     * @param ResolveInfo $info
739
     * @param array       $path
740
     * @param mixed       $result
741
     *
742
     * @return array|\React\Promise\Promise
743
     * @throws \Throwable
744
     */
745
    protected function completeListValue(
746
        ListType $returnType,
747
        array $fieldNodes,
748
        ResolveInfo $info,
749
        array $path,
750
        &$result
751
    ) {
752
        $itemType = $returnType->getOfType();
753
754
        $completedItems     = [];
755
        $doesContainPromise = false;
756
757
        if (!\is_array($result) && !($result instanceof \Traversable)) {
758
            /** @noinspection ThrowRawExceptionInspection */
759
            throw new \Exception(
760
                \sprintf(
761
                    'Expected Array or Traversable, but did not find one for field %s.%s.',
762
                    $info->getParentType()->getName(),
763
                    $info->getFieldName()
764
                )
765
            );
766
        }
767
768
        foreach ($result as $key => $item) {
769
            $fieldPath          = $path;
770
            $fieldPath[]        = $key;
771
            $completedItem      = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
772
            $completedItems[]   = $completedItem;
773
            $doesContainPromise = $doesContainPromise || $completedItem instanceof ExtendedPromiseInterface;
774
        }
775
776
        return $doesContainPromise
777
            ? promiseAll($completedItems)
778
            : $completedItems;
779
    }
780
781
    /**
782
     * @param LeafTypeInterface|SerializableTypeInterface $returnType
783
     * @param mixed                                       $result
784
     *
785
     * @return mixed
786
     * @throws ExecutionException
787
     */
788
    protected function completeLeafValue($returnType, &$result)
789
    {
790
        $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

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