Completed
Pull Request — master (#217)
by Christoffer
02:30
created

Executor::buildResolveInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 11
nc 1
nop 6
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\ExecutionException;
6
use Digia\GraphQL\Error\GraphQLException;
7
use Digia\GraphQL\Error\InvalidTypeException;
8
use Digia\GraphQL\Error\InvariantException;
9
use Digia\GraphQL\Error\UndefinedException;
10
use Digia\GraphQL\Language\Node\FieldNode;
11
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
12
use Digia\GraphQL\Schema\Schema;
13
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
14
use Digia\GraphQL\Type\Definition\Field;
15
use Digia\GraphQL\Type\Definition\InterfaceType;
16
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
17
use Digia\GraphQL\Type\Definition\ListType;
18
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
19
use Digia\GraphQL\Type\Definition\NonNullType;
20
use Digia\GraphQL\Type\Definition\ObjectType;
21
use Digia\GraphQL\Type\Definition\ScalarType;
22
use Digia\GraphQL\Type\Definition\TypeInterface;
23
use Digia\GraphQL\Type\Definition\UnionType;
24
use InvalidArgumentException;
25
use React\Promise\ExtendedPromiseInterface;
26
use React\Promise\FulfilledPromise;
27
use React\Promise\PromiseInterface;
28
use Throwable;
29
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
30
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
31
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
32
use function Digia\GraphQL\Util\toString;
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 array
63
     */
64
    private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
65
66
    /**
67
     * Executor constructor.
68
     * @param ExecutionContext $context
69
     * @param FieldCollector   $fieldCollector
70
     */
71
    public function __construct(ExecutionContext $context, FieldCollector $fieldCollector)
72
    {
73
        $this->context        = $context;
74
        $this->fieldCollector = $fieldCollector;
75
    }
76
77
    /**
78
     * @return array|null
79
     * @throws ExecutionException
80
     * @throws \Throwable
81
     */
82
    public function execute(): ?array
83
    {
84
        $operation = $this->context->getOperation();
85
        $schema    = $this->context->getSchema();
86
87
        $path = [];
88
89
        $objectType = $this->getOperationType($schema, $operation);
90
91
        $fields               = [];
92
        $visitedFragmentNames = [];
93
94
        try {
95
            $fields = $this->fieldCollector->collectFields(
96
                $objectType,
97
                $this->context->getOperation()->getSelectionSet(),
98
                $fields,
99
                $visitedFragmentNames
100
            );
101
102
            $rootValue = $this->context->getRootValue();
103
104
            $result = $operation->getOperation() === 'mutation'
105
                ? $this->executeFieldsSerially($objectType, $rootValue, $path, $fields)
106
                : $this->executeFields($objectType, $rootValue, $path, $fields);
107
        } catch (\Exception $ex) {
108
            $this->context->addError(new ExecutionException($ex->getMessage()));
109
110
            return [null];
111
        }
112
113
        return $result;
114
    }
115
116
    /**
117
     * @param Schema                  $schema
118
     * @param OperationDefinitionNode $operation
119
     * @return NamedTypeInterface|ObjectType
120
     * @throws ExecutionException
121
     */
122
    public function getOperationType(Schema $schema, OperationDefinitionNode $operation): NamedTypeInterface
123
    {
124
        switch ($operation->getOperation()) {
125
            case 'query':
126
                return $schema->getQueryType();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $schema->getQueryType() could return the type null which is incompatible with the type-hinted return Digia\GraphQL\Type\Definition\NamedTypeInterface. Consider adding an additional type-check to rule them out.
Loading history...
127
            case 'mutation':
128
                $mutationType = $schema->getMutationType();
129
                if (null === $mutationType) {
130
                    throw new ExecutionException(
131
                        'Schema is not configured for mutations',
132
                        [$operation]
133
                    );
134
                }
135
                return $mutationType;
136
            case 'subscription':
137
                $subscriptionType = $schema->getSubscriptionType();
138
                if (null === $subscriptionType) {
139
                    throw new ExecutionException(
140
                        'Schema is not configured for subscriptions',
141
                        [$operation]
142
                    );
143
                }
144
                return $subscriptionType;
145
            default:
146
                throw new ExecutionException(
147
                    'Can only execute queries, mutations and subscriptions',
148
                    [$operation]
149
                );
150
        }
151
    }
152
153
    /**
154
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
155
     *
156
     * @param ObjectType $objectType
157
     * @param mixed      $rootValue
158
     * @param array      $path
159
     * @param array      $fields
160
     * @return array
161
     * @throws InvalidArgumentException
162
     * @throws Throwable
163
     */
164
    public function executeFieldsSerially(
165
        ObjectType $objectType,
166
        $rootValue,
167
        array $path,
168
        array $fields
169
    ): array {
170
        $finalResults = [];
171
172
        $promise = new FulfilledPromise([]);
173
174
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
175
            $fieldPath   = $path;
176
            $fieldPath[] = $fieldName;
177
            try {
178
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
179
            } catch (UndefinedException $ex) {
180
                return null;
181
            }
182
183
            if ($this->isPromise($result)) {
184
                /** @var ExtendedPromiseInterface $result */
185
                return $result->then(function ($resolvedResult) use ($fieldName, $results) {
186
                    $results[$fieldName] = $resolvedResult;
187
                    return $results;
188
                });
189
            }
190
191
            $results[$fieldName] = $result;
192
193
            return $results;
194
        };
195
196
        foreach ($fields as $fieldName => $fieldNodes) {
197
            $promise = $promise->then(function ($resolvedResults) use (
198
                $resolve,
199
                $fieldName,
200
                $path,
201
                $objectType,
202
                $rootValue,
203
                $fieldNodes
204
            ) {
205
                return $resolve($resolvedResults, $fieldName, $path, $objectType, $rootValue, $fieldNodes);
206
            });
207
        }
208
209
        $promise->then(function ($resolvedResults) use (&$finalResults) {
210
            $finalResults = $resolvedResults ?? [];
211
        });
212
213
        return $finalResults;
214
    }
215
216
    /**
217
     * @param Schema     $schema
218
     * @param ObjectType $parentType
219
     * @param string     $fieldName
220
     * @return Field|null
221
     */
222
    public function getFieldDefinition(
223
        Schema $schema,
224
        ObjectType $parentType,
225
        string $fieldName
226
    ): ?Field {
227
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
228
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
229
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
230
231
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
232
            return $schemaMetaFieldDefinition;
233
        }
234
235
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
236
            return $typeMetaFieldDefinition;
237
        }
238
239
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
240
            return $typeNameMetaFieldDefinition;
241
        }
242
243
        $fields = $parentType->getFields();
244
245
        return $fields[$fieldName] ?? null;
246
    }
247
248
    /**
249
     * @param TypeInterface $fieldType
250
     * @param FieldNode[]   $fieldNodes
251
     * @param ResolveInfo   $info
252
     * @param array         $path
253
     * @param mixed         $result
254
     * @return array|mixed|null
255
     * @throws \Throwable
256
     */
257
    public function completeValueCatchingError(
258
        TypeInterface $fieldType,
259
        array $fieldNodes,
260
        ResolveInfo $info,
261
        array $path,
262
        &$result
263
    ) {
264
        if ($fieldType instanceof NonNullType) {
265
            return $this->completeValueWithLocatedError(
266
                $fieldType,
267
                $fieldNodes,
268
                $info,
269
                $path,
270
                $result
271
            );
272
        }
273
274
        try {
275
            $completed = $this->completeValueWithLocatedError(
276
                $fieldType,
277
                $fieldNodes,
278
                $info,
279
                $path,
280
                $result
281
            );
282
283
            if ($this->isPromise($completed)) {
284
                $context = $this->context;
285
                /** @var ExtendedPromiseInterface $completed */
286
                return $completed->then(null, function ($error) use ($context, $fieldNodes, $path) {
287
                    //@TODO Handle $error better
288
                    if ($error instanceof \Exception) {
289
                        $context->addError($this->buildLocatedError($error, $fieldNodes, $path));
290
                    } else {
291
                        $context->addError(
292
                            $this->buildLocatedError(
293
                                new ExecutionException($error ?? 'An unknown error occurred.'),
294
                                $fieldNodes,
295
                                $path
296
                            )
297
                        );
298
                    }
299
                    return new FulfilledPromise(null);
300
                });
301
            }
302
303
            return $completed;
304
        } catch (\Exception $ex) {
305
            $this->context->addError($this->buildLocatedError($ex, $fieldNodes, $path));
306
            return null;
307
        }
308
    }
309
310
311
    /**
312
     * @param TypeInterface $fieldType
313
     * @param FieldNode[]   $fieldNodes
314
     * @param ResolveInfo   $info
315
     * @param array         $path
316
     * @param mixed         $result
317
     * @return array|mixed
318
     * @throws \Throwable
319
     */
320
    public function completeValueWithLocatedError(
321
        TypeInterface $fieldType,
322
        array $fieldNodes,
323
        ResolveInfo $info,
324
        array $path,
325
        $result
326
    ) {
327
        try {
328
            $completed = $this->completeValue(
329
                $fieldType,
330
                $fieldNodes,
331
                $info,
332
                $path,
333
                $result
334
            );
335
336
            return $completed;
337
        } catch (\Throwable $ex) {
338
            throw $this->buildLocatedError($ex, $fieldNodes, $path);
339
        }
340
    }
341
342
    /**
343
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
344
     *
345
     * @param ObjectType $objectType
346
     * @param mixed      $rootValue
347
     * @param array      $path
348
     * @param array      $fields
349
     * @return array
350
     * @throws \Throwable
351
     */
352
    protected function executeFields(
353
        ObjectType $objectType,
354
        $rootValue,
355
        array $path,
356
        array $fields
357
    ): array {
358
        $finalResults       = [];
359
        $doesContainPromise = false;
360
361
        foreach ($fields as $fieldName => $fieldNodes) {
362
            $fieldPath   = $path;
363
            $fieldPath[] = $fieldName;
364
365
            try {
366
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
367
            } catch (UndefinedException $ex) {
368
                continue;
369
            }
370
371
            $doesContainPromise = $doesContainPromise || $this->isPromise($result);
372
373
            $finalResults[$fieldName] = $result;
374
        }
375
376
        if ($doesContainPromise) {
377
            $keys    = array_keys($finalResults);
378
            $promise = \React\Promise\all(array_values($finalResults));
379
            $promise->then(function ($values) use ($keys, &$finalResults) {
380
                /** @noinspection ForeachSourceInspection */
381
                foreach ($values as $i => $value) {
382
                    $finalResults[$keys[$i]] = $value;
383
                }
384
            });
385
        }
386
387
        return $finalResults;
388
    }
389
390
    /**
391
     * @param ObjectType  $parentType
392
     * @param mixed       $rootValue
393
     * @param FieldNode[] $fieldNodes
394
     * @param array       $path
395
     * @return array|mixed|null
396
     * @throws UndefinedException
397
     * @throws Throwable
398
     */
399
    protected function resolveField(
400
        ObjectType $parentType,
401
        $rootValue,
402
        array $fieldNodes,
403
        array $path
404
    ) {
405
        /** @var FieldNode $fieldNode */
406
        $fieldNode = $fieldNodes[0];
407
408
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
409
410
        if (null === $field) {
411
            throw new UndefinedException('Undefined field definition.');
412
        }
413
414
        $info = $this->createResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
415
416
        $resolveCallback = $this->determineResolveCallback($field, $parentType);
417
418
        $result = $this->resolveFieldValueOrError(
419
            $field,
420
            $fieldNode,
421
            $resolveCallback,
422
            $rootValue,
423
            $this->context,
424
            $info
425
        );
426
427
        $result = $this->completeValueCatchingError(
428
            $field->getType(),
429
            $fieldNodes,
430
            $info,
431
            $path,
432
            $result
433
        );
434
435
        return $result;
436
    }
437
438
    /**
439
     * @param Field      $field
440
     * @param ObjectType $objectType
441
     * @return callable|mixed|null
442
     */
443
    protected function determineResolveCallback(Field $field, ObjectType $objectType)
444
    {
445
        if ($field->hasResolveCallback()) {
446
            return $field->getResolveCallback();
447
        }
448
449
        if ($objectType->hasResolveCallback()) {
450
            return $objectType->getResolveCallback();
451
        }
452
453
        return $this->context->getFieldResolver() ?? self::$defaultFieldResolver;
454
    }
455
456
    /**
457
     * @param TypeInterface $returnType
458
     * @param FieldNode[]   $fieldNodes
459
     * @param ResolveInfo   $info
460
     * @param array         $path
461
     * @param mixed         $result
462
     * @return array|mixed
463
     * @throws InvariantException
464
     * @throws InvalidTypeException
465
     * @throws ExecutionException
466
     * @throws \Throwable
467
     */
468
    protected function completeValue(
469
        TypeInterface $returnType,
470
        array $fieldNodes,
471
        ResolveInfo $info,
472
        array $path,
473
        &$result
474
    ) {
475
        if ($this->isPromise($result)) {
476
            /** @var ExtendedPromiseInterface $result */
477
            return $result->then(function (&$value) use ($returnType, $fieldNodes, $info, $path) {
478
                return $this->completeValue($returnType, $fieldNodes, $info, $path, $value);
479
            });
480
        }
481
482
        if ($result instanceof \Throwable) {
483
            throw $result;
484
        }
485
486
        // If result is null-like, return null.
487
        if (null === $result) {
488
            return null;
489
        }
490
491
        if ($returnType instanceof NonNullType) {
492
            $completed = $this->completeValue(
493
                $returnType->getOfType(),
494
                $fieldNodes,
495
                $info,
496
                $path,
497
                $result
498
            );
499
500
            if ($completed === null) {
501
                throw new ExecutionException(
502
                    \sprintf(
503
                        'Cannot return null for non-nullable field %s.%s.',
504
                        $info->getParentType(),
505
                        $info->getFieldName()
506
                    )
507
                );
508
            }
509
510
            return $completed;
511
        }
512
513
        // If field type is List, complete each item in the list with the inner type
514
        if ($returnType instanceof ListType) {
515
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
516
        }
517
518
        // If field type is Scalar or Enum, serialize to a valid value, returning
519
        // null if serialization is not possible.
520
        if ($returnType instanceof LeafTypeInterface) {
521
            return $this->completeLeafValue($returnType, $result);
522
        }
523
524
        // TODO: Make a function for checking abstract type?
525
        if ($returnType instanceof InterfaceType || $returnType instanceof UnionType) {
526
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
527
        }
528
529
        // Field type must be Object, Interface or Union and expect sub-selections.
530
        if ($returnType instanceof ObjectType) {
531
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
532
        }
533
534
        throw new ExecutionException("Cannot complete value of unexpected type \"{$returnType}\".");
535
    }
536
537
    /**
538
     * @param AbstractTypeInterface $returnType
539
     * @param FieldNode[]           $fieldNodes
540
     * @param ResolveInfo           $info
541
     * @param array                 $path
542
     * @param mixed                 $result
543
     * @return array|PromiseInterface
544
     * @throws ExecutionException
545
     * @throws InvalidTypeException
546
     * @throws InvariantException
547
     * @throws \Throwable
548
     */
549
    protected function completeAbstractValue(
550
        AbstractTypeInterface $returnType,
551
        array $fieldNodes,
552
        ResolveInfo $info,
553
        array $path,
554
        &$result
555
    ) {
556
        $runtimeType = $returnType->resolveType($result, $this->context->getContextValue(), $info);
557
558
        if (null === $runtimeType) {
559
            // TODO: Display warning
560
            $runtimeType = $this->defaultTypeResolver($result, $this->context->getContextValue(), $info, $returnType);
561
        }
562
563
        if ($this->isPromise($runtimeType)) {
564
            /** @var ExtendedPromiseInterface $runtimeType */
565
            return $runtimeType->then(function ($resolvedRuntimeType) use (
566
                $returnType,
567
                $fieldNodes,
568
                $info,
569
                $path,
570
                &$result
571
            ) {
572
                return $this->completeObjectValue(
573
                    $this->ensureValidRuntimeType(
574
                        $resolvedRuntimeType, $returnType, $info, $result
575
                    ),
576
                    $fieldNodes,
577
                    $info,
578
                    $path,
579
                    $result
580
                );
581
            });
582
        }
583
584
        return $this->completeObjectValue(
585
            $this->ensureValidRuntimeType(
586
                $runtimeType, $returnType, $info, $result
587
            ),
588
            $fieldNodes,
589
            $info,
590
            $path,
591
            $result
592
        );
593
    }
594
595
    /**
596
     * @param NamedTypeInterface|string $runtimeTypeOrName
597
     * @param AbstractTypeInterface     $returnType
598
     * @param ResolveInfo               $info
599
     * @param mixed                     $result
600
     * @return TypeInterface|ObjectType|null
601
     * @throws ExecutionException
602
     * @throws InvariantException
603
     */
604
    protected function ensureValidRuntimeType(
605
        $runtimeTypeOrName,
606
        AbstractTypeInterface $returnType,
607
        ResolveInfo $info,
608
        &$result
609
    ) {
610
        /** @var NamedTypeInterface $runtimeType */
611
        $runtimeType = \is_string($runtimeTypeOrName)
612
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
613
            : $runtimeTypeOrName;
614
615
        $runtimeTypeName = $runtimeType->getName();
616
        $returnTypeName  = $returnType->getName();
617
618
        if (!$runtimeType instanceof ObjectType) {
619
            $parentTypeName = $info->getParentType()->getName();
620
            $fieldName      = $info->getFieldName();
621
622
            throw new ExecutionException(
623
                \sprintf(
624
                    'Abstract type %s must resolve to an Object type at runtime for field %s.%s ' .
625
                    'with value "%s", received "%s".',
626
                    $returnTypeName,
627
                    $parentTypeName,
628
                    $fieldName,
629
                    $result,
630
                    $runtimeTypeName
631
                )
632
            );
633
        }
634
635
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
636
            throw new ExecutionException(
637
                \sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeTypeName, $returnTypeName)
638
            );
639
        }
640
641
        if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
0 ignored issues
show
introduced by
The condition $runtimeType !== $this->...runtimeType->getName()) is always true.
Loading history...
642
            throw new ExecutionException(
643
                \sprintf(
644
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
645
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
646
                    'type instance as referenced anywhere else within the schema.',
647
                    $runtimeTypeName,
648
                    $returnTypeName
649
                )
650
            );
651
        }
652
653
        return $runtimeType;
654
    }
655
656
    /**
657
     * @param mixed                 $value
658
     * @param mixed                 $context
659
     * @param ResolveInfo           $info
660
     * @param AbstractTypeInterface $abstractType
661
     * @return NamedTypeInterface|mixed|null
662
     * @throws InvariantException
663
     */
664
    protected function defaultTypeResolver(
665
        $value,
666
        $context,
667
        ResolveInfo $info,
668
        AbstractTypeInterface $abstractType
669
    ) {
670
        /** @var ObjectType[] $possibleTypes */
671
        $possibleTypes           = $info->getSchema()->getPossibleTypes($abstractType);
672
        $promisedIsTypeOfResults = [];
673
674
        foreach ($possibleTypes as $index => $type) {
675
            $isTypeOfResult = $type->isTypeOf($value, $context, $info);
676
            if (null !== $isTypeOfResult) {
677
                if ($this->isPromise($isTypeOfResult)) {
678
                    $promisedIsTypeOfResults[$index] = $isTypeOfResult;
679
                    continue;
680
                }
681
682
                if ($isTypeOfResult === true) {
683
                    return $type;
684
                }
685
686
                if (\is_array($value)) {
687
                    // TODO: Make `type` configurable
688
                    /** @noinspection NestedPositiveIfStatementsInspection */
689
                    if (isset($value['type']) && $value['type'] === $type->getName()) {
690
                        return $type;
691
                    }
692
                }
693
            }
694
        }
695
696
        if (!empty($promisedIsTypeOfResults)) {
697
            return \React\Promise\all($promisedIsTypeOfResults)
698
                ->then(function ($isTypeOfResults) use ($possibleTypes) {
699
                    /** @noinspection ForeachSourceInspection */
700
                    foreach ($isTypeOfResults as $index => $result) {
701
                        if ($result) {
702
                            return $possibleTypes[$index];
703
                        }
704
                    }
705
                    return null;
706
                });
707
        }
708
709
        return null;
710
    }
711
712
    /**
713
     * @param ListType    $returnType
714
     * @param FieldNode[] $fieldNodes
715
     * @param ResolveInfo $info
716
     * @param array       $path
717
     * @param mixed       $result
718
     * @return array|\React\Promise\Promise
719
     * @throws \Throwable
720
     */
721
    protected function completeListValue(
722
        ListType $returnType,
723
        array $fieldNodes,
724
        ResolveInfo $info,
725
        array $path,
726
        &$result
727
    ) {
728
        $itemType = $returnType->getOfType();
729
730
        $completedItems     = [];
731
        $doesContainPromise = false;
732
733
        if (!\is_array($result) && !($result instanceof \Traversable)) {
734
            /** @noinspection ThrowRawExceptionInspection */
735
            throw new \Exception(
736
                \sprintf(
737
                    'Expected Array or Traversable, but did not find one for field %s.%s.',
738
                    $info->getParentType()->getName(),
739
                    $info->getFieldName()
740
                )
741
            );
742
        }
743
744
        foreach ($result as $key => $item) {
745
            $fieldPath          = $path;
746
            $fieldPath[]        = $key;
747
            $completedItem      = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
748
            $completedItems[]   = $completedItem;
749
            $doesContainPromise = $doesContainPromise || $this->isPromise($completedItem);
750
        }
751
752
        return $doesContainPromise
753
            ? \React\Promise\all($completedItems)
754
            : $completedItems;
755
    }
756
757
    /**
758
     * @param LeafTypeInterface $returnType
759
     * @param mixed             $result
760
     * @return mixed
761
     * @throws ExecutionException
762
     */
763
    protected function completeLeafValue(LeafTypeInterface $returnType, &$result)
764
    {
765
        /** @var ScalarType $returnType */
766
        $serializedResult = $returnType->serialize($result);
767
768
        if ($serializedResult === null) {
769
            // TODO: Make a function for this type of exception
770
            throw new ExecutionException(
771
                \sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
772
            );
773
        }
774
775
        return $serializedResult;
776
    }
777
778
    /**
779
     * @param ObjectType  $returnType
780
     * @param array       $fieldNodes
781
     * @param ResolveInfo $info
782
     * @param array       $path
783
     * @param mixed       $result
784
     * @return array
785
     * @throws ExecutionException
786
     * @throws InvalidTypeException
787
     * @throws InvariantException
788
     * @throws \Throwable
789
     */
790
    protected function completeObjectValue(
791
        ObjectType $returnType,
792
        array $fieldNodes,
793
        ResolveInfo $info,
794
        array $path,
795
        &$result
796
    ): array {
797
        if (null !== $returnType->getIsTypeOf()) {
798
            $isTypeOf = $returnType->isTypeOf($result, $this->context->getContextValue(), $info);
799
800
            // TODO: Check for promise?
801
            if (!$isTypeOf) {
802
                throw new ExecutionException(
803
                    sprintf('Expected value of type "%s" but received: %s.', (string)$returnType, toString($result))
804
                );
805
            }
806
        }
807
808
        return $this->executeSubFields($returnType, $fieldNodes, $path, $result);
809
    }
810
811
    /**
812
     * @param Field            $field
813
     * @param FieldNode        $fieldNode
814
     * @param callable         $resolveCallback
815
     * @param mixed            $rootValue
816
     * @param ExecutionContext $context
817
     * @param ResolveInfo      $info
818
     * @return array|\Throwable
819
     */
820
    protected function resolveFieldValueOrError(
821
        Field $field,
822
        FieldNode $fieldNode,
823
        ?callable $resolveCallback,
824
        $rootValue,
825
        ExecutionContext $context,
826
        ResolveInfo $info
827
    ) {
828
        try {
829
            $result = $resolveCallback(
830
                $rootValue,
831
                coerceArgumentValues($field, $fieldNode, $context->getVariableValues()),
832
                $context->getContextValue(),
833
                $info
834
            );
835
        } catch (\Throwable $error) {
836
            return $error;
837
        }
838
839
        return $result;
840
    }
841
842
    /**
843
     * @param ObjectType  $returnType
844
     * @param FieldNode[] $fieldNodes
845
     * @param array       $path
846
     * @param mixed       $result
847
     * @return array
848
     * @throws ExecutionException
849
     * @throws InvalidTypeException
850
     * @throws InvariantException
851
     * @throws Throwable
852
     */
853
    protected function executeSubFields(
854
        ObjectType $returnType,
855
        array $fieldNodes,
856
        array $path,
857
        &$result
858
    ): array {
859
        $subFields            = [];
860
        $visitedFragmentNames = [];
861
862
        foreach ($fieldNodes as $fieldNode) {
863
            if (null !== $fieldNode->getSelectionSet()) {
864
                $subFields = $this->fieldCollector->collectFields(
865
                    $returnType,
866
                    $fieldNode->getSelectionSet(),
867
                    $subFields,
868
                    $visitedFragmentNames
869
                );
870
            }
871
        }
872
873
        if (!empty($subFields)) {
874
            return $this->executeFields($returnType, $result, $path, $subFields);
875
        }
876
877
        return $result;
878
    }
879
880
    /**
881
     * @param $value
882
     * @return bool
883
     */
884
    protected function isPromise($value): bool
885
    {
886
        return $value instanceof ExtendedPromiseInterface;
887
    }
888
889
    /**
890
     * @param \Throwable $originalException
891
     * @param array      $nodes
892
     * @param array      $path
893
     * @return ExecutionException
894
     */
895
    protected function buildLocatedError(
896
        \Throwable $originalException,
897
        array $nodes = [],
898
        array $path = []
899
    ): ExecutionException {
900
        return new ExecutionException(
901
            $originalException->getMessage(),
902
            $originalException instanceof GraphQLException
903
                ? $originalException->getNodes()
904
                : $nodes,
905
            $originalException instanceof GraphQLException
906
                ? $originalException->getSource()
907
                : null,
908
            $originalException instanceof GraphQLException
909
                ? $originalException->getPositions()
910
                : null,
911
            $originalException instanceof GraphQLException
912
                ? ($originalException->getPath() ?? $path)
913
                : $path,
914
            $originalException
915
        );
916
    }
917
918
    /**
919
     * @param FieldNode[]      $fieldNodes
920
     * @param FieldNode        $fieldNode
921
     * @param Field            $field
922
     * @param ObjectType       $parentType
923
     * @param array|null       $path
924
     * @param ExecutionContext $context
925
     * @return ResolveInfo
926
     */
927
    protected function createResolveInfo(
928
        array $fieldNodes,
929
        FieldNode $fieldNode,
930
        Field $field,
931
        ObjectType $parentType,
932
        ?array $path,
933
        ExecutionContext $context
934
    ): ResolveInfo {
935
        return new ResolveInfo(
936
            $fieldNode->getNameValue(),
937
            $fieldNodes,
938
            $field->getType(),
939
            $parentType,
940
            $path,
941
            $context->getSchema(),
942
            $context->getFragments(),
943
            $context->getRootValue(),
944
            $context->getOperation(),
945
            $context->getVariableValues()
946
        );
947
    }
948
949
    /**
950
     * Try to resolve a field without any field resolver function.
951
     *
952
     * @param array|object $rootValue
953
     * @param array        $arguments
954
     * @param mixed        $contextValues
955
     * @param ResolveInfo  $info
956
     * @return mixed|null
957
     */
958
    public static function defaultFieldResolver($rootValue, array $arguments, $contextValues, ResolveInfo $info)
959
    {
960
        $fieldName = $info->getFieldName();
961
        $property  = null;
962
963
        if (\is_array($rootValue) && isset($rootValue[$fieldName])) {
964
            $property = $rootValue[$fieldName];
965
        }
966
967
        if (\is_object($rootValue)) {
968
            $getter = 'get' . \ucfirst($fieldName);
969
            if (\method_exists($rootValue, $getter)) {
970
                $property = $rootValue->{$getter}();
971
            } elseif (\method_exists($rootValue, $fieldName)) {
972
                $property = $rootValue->{$fieldName}($rootValue, $arguments, $contextValues, $info);
973
            } elseif (\property_exists($rootValue, $fieldName)) {
974
                $property = $rootValue->{$fieldName};
975
            }
976
        }
977
978
        return \is_callable($property)
979
            ? $property($rootValue, $arguments, $contextValues, $info)
980
            : $property;
981
    }
982
}
983