Passed
Pull Request — master (#297)
by Christoffer
02:54
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 InvalidArgumentException;
26
use React\Promise\ExtendedPromiseInterface;
27
use React\Promise\FulfilledPromise;
28
use React\Promise\PromiseInterface;
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
use function React\Promise\all as promiseAll;
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 ErrorHandlerInterface|null
64
     */
65
    protected $errorHandler;
66
67
    /**
68
     * @var array
69
     */
70
    private static $defaultFieldResolver = [__CLASS__, 'defaultFieldResolver'];
71
72
    /**
73
     * Executor constructor.
74
     *
75
     * @param ExecutionContext           $context
76
     * @param FieldCollector             $fieldCollector
77
     * @param ErrorHandlerInterface|null $errorHandler
78
     */
79
    public function __construct(
80
        ExecutionContext $context,
81
        FieldCollector $fieldCollector,
82
        ?ErrorHandlerInterface $errorHandler = null
83
    ) {
84
        $this->context        = $context;
85
        $this->fieldCollector = $fieldCollector;
86
        $this->errorHandler   = $errorHandler;
87
    }
88
89
    /**
90
     * @return array|null
91
     * @throws ExecutionException
92
     * @throws \Throwable
93
     */
94
    public function execute(): ?array
95
    {
96
        $schema    = $this->context->getSchema();
97
        $operation = $this->context->getOperation();
98
        $rootValue = $this->context->getRootValue();
99
100
        $objectType = $this->getOperationType($schema, $operation);
101
102
        $fields               = [];
103
        $visitedFragmentNames = [];
104
        $path                 = [];
105
106
        $fields = $this->fieldCollector->collectFields(
107
            $objectType,
108
            $operation->getSelectionSet(),
109
            $fields,
110
            $visitedFragmentNames
111
        );
112
113
        try {
114
            $result = $operation->getOperation() === 'mutation'
115
                ? $this->executeFieldsSerially($objectType, $rootValue, $path, $fields)
116
                : $this->executeFields($objectType, $rootValue, $path, $fields);
117
        } catch (ExecutionException $ex) {
118
            $this->handleError($ex);
119
            return null;
120
        }
121
122
        return $result;
123
    }
124
125
    /**
126
     * @param Schema                  $schema
127
     * @param OperationDefinitionNode $operation
128
     *
129
     * @return ObjectType|null
130
     * @throws ExecutionException
131
     */
132
    public function getOperationType(Schema $schema, OperationDefinitionNode $operation): ?ObjectType
133
    {
134
        switch ($operation->getOperation()) {
135
            case 'query':
136
                return $schema->getQueryType();
137
            case 'mutation':
138
                $mutationType = $schema->getMutationType();
139
                if (null === $mutationType) {
140
                    throw new ExecutionException(
141
                        'Schema is not configured for mutations',
142
                        [$operation]
143
                    );
144
                }
145
146
                return $mutationType;
147
            case 'subscription':
148
                $subscriptionType = $schema->getSubscriptionType();
149
                if (null === $subscriptionType) {
150
                    throw new ExecutionException(
151
                        'Schema is not configured for subscriptions',
152
                        [$operation]
153
                    );
154
                }
155
156
                return $subscriptionType;
157
            default:
158
                throw new ExecutionException(
159
                    'Can only execute queries, mutations and subscriptions',
160
                    [$operation]
161
                );
162
        }
163
    }
164
165
    /**
166
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
167
     *
168
     * @param ObjectType $objectType
169
     * @param mixed      $rootValue
170
     * @param array      $path
171
     * @param array      $fields
172
     *
173
     * @return array
174
     * @throws InvalidArgumentException
175
     */
176
    public function executeFieldsSerially(
177
        ObjectType $objectType,
178
        $rootValue,
179
        array $path,
180
        array $fields
181
    ): array {
182
        $finalResults = [];
183
184
        $promise = new FulfilledPromise([]);
185
186
        $resolve = function ($results, $fieldName, $path, $objectType, $rootValue, $fieldNodes) {
187
            $fieldPath   = $path;
188
            $fieldPath[] = $fieldName;
189
            try {
190
                $result = $this->resolveField($objectType, $rootValue, $fieldNodes, $fieldPath);
191
            } catch (UndefinedException $ex) {
192
                return null;
193
            }
194
195
            if ($result instanceof ExtendedPromiseInterface) {
196
                return $result->then(function ($resolvedResult) use ($fieldName, $results) {
197
                    $results[$fieldName] = $resolvedResult;
198
199
                    return $results;
200
                });
201
            }
202
203
            $results[$fieldName] = $result;
204
205
            return $results;
206
        };
207
208
        foreach ($fields as $fieldName => $fieldNodes) {
209
            $promise = $promise->then(function ($resolvedResults) use (
210
                $resolve,
211
                $fieldName,
212
                $path,
213
                $objectType,
214
                $rootValue,
215
                $fieldNodes
216
            ) {
217
                return $resolve($resolvedResults, $fieldName, $path, $objectType, $rootValue, $fieldNodes);
218
            });
219
        }
220
221
        $promise->then(function ($resolvedResults) use (&$finalResults) {
222
            $finalResults = $resolvedResults ?? [];
223
        })->otherwise(function (ExecutionException $ex) {
224
            $this->handleError($ex);
225
        });
226
227
        return $finalResults;
228
    }
229
230
    /**
231
     * @param Schema     $schema
232
     * @param ObjectType $parentType
233
     * @param string     $fieldName
234
     *
235
     * @return Field|null
236
     */
237
    public function getFieldDefinition(
238
        Schema $schema,
239
        ObjectType $parentType,
240
        string $fieldName
241
    ): ?Field {
242
        $schemaMetaFieldDefinition   = SchemaMetaFieldDefinition();
243
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
244
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
245
246
        if ($fieldName === $schemaMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
247
            return $schemaMetaFieldDefinition;
248
        }
249
250
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQueryType() === $parentType) {
251
            return $typeMetaFieldDefinition;
252
        }
253
254
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
255
            return $typeNameMetaFieldDefinition;
256
        }
257
258
        $fields = $parentType->getFields();
259
260
        return $fields[$fieldName] ?? null;
261
    }
262
263
    /**
264
     * @param TypeInterface $fieldType
265
     * @param FieldNode[]   $fieldNodes
266
     * @param ResolveInfo   $info
267
     * @param array         $path
268
     * @param mixed         $result
269
     *
270
     * @return array|mixed|null
271
     * @throws \Throwable
272
     */
273
    public function completeValueCatchingError(
274
        TypeInterface $fieldType,
275
        array $fieldNodes,
276
        ResolveInfo $info,
277
        array $path,
278
        &$result
279
    ) {
280
        if ($fieldType instanceof NonNullType) {
281
            return $this->completeValueWithLocatedError(
282
                $fieldType,
283
                $fieldNodes,
284
                $info,
285
                $path,
286
                $result
287
            );
288
        }
289
290
        try {
291
            $completed = $this->completeValueWithLocatedError(
292
                $fieldType,
293
                $fieldNodes,
294
                $info,
295
                $path,
296
                $result
297
            );
298
299
            if ($this->isPromise($completed)) {
0 ignored issues
show
Bug introduced by
The method isPromise() does not exist on Digia\GraphQL\Execution\Executor. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

299
            if ($this->/** @scrutinizer ignore-call */ isPromise($completed)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

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