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

Executor   F

Complexity

Total Complexity 100

Size/Duplication

Total Lines 941
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 941
rs 1.263
c 0
b 0
f 0
wmc 100

23 Methods

Rating   Name   Duplication   Size   Complexity  
B execute() 0 30 3
B completeListValue() 0 34 6
A completeObjectValue() 0 19 3
B completeAbstractValue() 0 39 3
B getOperationType() 0 27 6
A __construct() 0 4 1
B getFieldDefinition() 0 24 6
A completeLeafValue() 0 13 2
C completeValue() 0 67 11
B ensureValidRuntimeType() 0 50 5
A completeValueWithLocatedError() 0 19 2
B buildLocatedError() 0 20 5
B executeFieldsSerially() 0 50 4
A isPromise() 0 3 1
A resolveFieldValueOrError() 0 20 2
A createResolveInfo() 0 19 1
C defaultFieldResolver() 0 23 8
B executeFields() 0 36 6
B resolveField() 0 37 2
C defaultTypeResolver() 0 46 11
B completeValueCatchingError() 0 50 5
B executeSubFields() 0 25 4
A determineResolveCallback() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like Executor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Executor, and based on these observations, apply Extract Interface, too.

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