Passed
Pull Request — master (#321)
by Christoffer
04:21 queued 01:06
created

AbstractExecutionStrategy::completeValue()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 64
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 27
nc 9
nop 5
dl 0
loc 64
rs 8.0555
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Digia\GraphQL\Execution\Strategy;
4
5
use Digia\GraphQL\Error\GraphQLException;
6
use Digia\GraphQL\Error\InvalidTypeException;
7
use Digia\GraphQL\Error\InvariantException;
8
use Digia\GraphQL\Execution\ExecutionContext;
9
use Digia\GraphQL\Execution\ExecutionException;
10
use Digia\GraphQL\Execution\InvalidReturnTypeException;
11
use Digia\GraphQL\Execution\ResolveInfo;
12
use Digia\GraphQL\Execution\UndefinedFieldException;
13
use Digia\GraphQL\Execution\ValuesResolver;
14
use Digia\GraphQL\Language\Node\FieldNode;
15
use Digia\GraphQL\Language\Node\NodeInterface;
16
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
17
use Digia\GraphQL\Schema\Schema;
18
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
19
use Digia\GraphQL\Type\Definition\Field;
20
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
21
use Digia\GraphQL\Type\Definition\ListType;
22
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
23
use Digia\GraphQL\Type\Definition\NonNullType;
24
use Digia\GraphQL\Type\Definition\ObjectType;
25
use Digia\GraphQL\Type\Definition\SerializableTypeInterface;
26
use Digia\GraphQL\Type\Definition\TypeInterface;
27
use Digia\GraphQL\Util\ConversionException;
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
33
abstract class AbstractExecutionStrategy implements ExecutionStrategyInterface
34
{
35
    /**
36
     * @var ExecutionContext
37
     */
38
    protected $context;
39
40
    /**
41
     * @var FieldCollector
42
     */
43
    protected $fieldCollector;
44
45
    /**
46
     * @var ValuesResolver
47
     */
48
    protected $valuesResolver;
49
50
    /**
51
     * @var callable
52
     */
53
    protected $typeResolverCallback;
54
55
    /**
56
     * @var callable
57
     */
58
    protected $fieldResolverCallback;
59
60
    /**
61
     * @param ObjectType $parentType
62
     * @param mixed      $rootValue
63
     * @param array      $path
64
     * @param array      $fields
65
     * @return array|PromiseInterface
66
     * @throws \Throwable
67
     * @throws ExecutionException
68
     */
69
    abstract public function executeFields(ObjectType $parentType, $rootValue, array $path, array $fields);
70
71
    /**
72
     * AbstractExecutionStrategy constructor.
73
     *
74
     * @param ExecutionContext $context
75
     * @param FieldCollector   $fieldCollector
76
     * @param ValuesResolver   $valueResolver
77
     * @param callable|null    $typeResolverCallback
78
     * @param callable|null    $fieldResolverCallback
79
     */
80
    public function __construct(
81
        ExecutionContext $context,
82
        FieldCollector $fieldCollector,
83
        ValuesResolver $valueResolver,
84
        ?callable $typeResolverCallback = null,
85
        ?callable $fieldResolverCallback = null
86
    ) {
87
        $this->context               = $context;
88
        $this->fieldCollector        = $fieldCollector;
89
        $this->valuesResolver        = $valueResolver;
90
        $this->typeResolverCallback  = $typeResolverCallback ?? [$this, 'defaultTypeResolver'];
91
        $this->fieldResolverCallback = $fieldResolverCallback ?? [$this, 'defaultFieldResolver'];
92
    }
93
94
    /**
95
     * @inheritdoc
96
     * @throws ExecutionException
97
     * @throws InvalidTypeException
98
     * @throws InvariantException
99
     * @throws ConversionException
100
     * @throws \Throwable
101
     */
102
    public function execute()
103
    {
104
        $schema    = $this->context->getSchema();
105
        $operation = $this->context->getOperation();
106
        $rootValue = $this->context->getRootValue();
107
108
        $objectType = $this->getOperationType($schema, $operation);
109
110
        $fields               = [];
111
        $visitedFragmentNames = [];
112
        $path                 = [];
113
114
        $fields = $this->fieldCollector->collectFields(
115
            $objectType,
0 ignored issues
show
Bug introduced by
It seems like $objectType can also be of type null; however, parameter $runtimeType of Digia\GraphQL\Execution\...lector::collectFields() does only seem to accept Digia\GraphQL\Type\Definition\ObjectType, maybe add an additional type check? ( Ignorable by Annotation )

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

115
            /** @scrutinizer ignore-type */ $objectType,
Loading history...
116
            $operation->getSelectionSet(),
0 ignored issues
show
Bug introduced by
It seems like $operation->getSelectionSet() can also be of type null; however, parameter $selectionSet of Digia\GraphQL\Execution\...lector::collectFields() does only seem to accept Digia\GraphQL\Language\Node\SelectionSetNode, maybe add an additional type check? ( Ignorable by Annotation )

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

116
            /** @scrutinizer ignore-type */ $operation->getSelectionSet(),
Loading history...
117
            $fields,
118
            $visitedFragmentNames
119
        );
120
121
        // Errors from sub-fields of a NonNull type may propagate to the top level,
122
        // at which point we still log the error and null the parent field, which
123
        // in this case is the entire response.
124
        try {
125
            $result = $this->executeFields($objectType, $rootValue, $path, $fields);
0 ignored issues
show
Bug introduced by
It seems like $objectType can also be of type null; however, parameter $parentType of Digia\GraphQL\Execution\...rategy::executeFields() does only seem to accept Digia\GraphQL\Type\Definition\ObjectType, maybe add an additional type check? ( Ignorable by Annotation )

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

125
            $result = $this->executeFields(/** @scrutinizer ignore-type */ $objectType, $rootValue, $path, $fields);
Loading history...
126
        } catch (ExecutionException $exception) {
127
            $this->context->addError($exception);
128
            return null;
129
        } catch (\Throwable $exception) {
130
            $exception = !$exception instanceof ExecutionException
131
                ? $this->normalizeException($exception, $fields, $path)
132
                : $exception;
133
134
            $this->context->addError($exception);
135
136
            return null;
137
        }
138
139
        return $result;
140
    }
141
142
    /**
143
     * @param Schema                  $schema
144
     * @param OperationDefinitionNode $operation
145
     *
146
     * @return ObjectType|null
147
     * @throws ExecutionException
148
     */
149
    protected function getOperationType(Schema $schema, OperationDefinitionNode $operation): ?ObjectType
150
    {
151
        switch ($operation->getOperation()) {
152
            case 'query':
153
                return $schema->getQueryType();
154
            case 'mutation':
155
                $mutationType = $schema->getMutationType();
156
157
                if (null === $mutationType) {
158
                    throw new ExecutionException('Schema is not configured for mutations.', [$operation]);
159
                }
160
161
                return $mutationType;
162
            case 'subscription':
163
                $subscriptionType = $schema->getSubscriptionType();
164
165
                if (null === $subscriptionType) {
166
                    throw new ExecutionException('Schema is not configured for subscriptions.', [$operation]);
167
                }
168
169
                return $subscriptionType;
170
            default:
171
                throw new ExecutionException('Can only execute queries, mutations and subscriptions.', [$operation]);
172
        }
173
    }
174
175
    /**
176
     * Resolves the field on the given source object. In particular, this
177
     * figures out the value that the field returns by calling its resolve function,
178
     * then calls completeValue to complete promises, serialize scalars, or execute
179
     * the sub-selection-set for objects.
180
     *
181
     * @param ObjectType  $parentType
182
     * @param mixed       $rootValue
183
     * @param FieldNode[] $fieldNodes
184
     * @param string[]    $path
185
     *
186
     * @return mixed
187
     * @throws \Throwable
188
     * @throws UndefinedFieldException
189
     */
190
    protected function resolveField(ObjectType $parentType, $rootValue, array $fieldNodes, array $path)
191
    {
192
        $fieldNode = $fieldNodes[0];
193
194
        $fieldName = $fieldNode->getNameValue();
195
        $field     = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldName);
0 ignored issues
show
Bug introduced by
It seems like $fieldName can also be of type null; however, parameter $fieldName of Digia\GraphQL\Execution\...y::getFieldDefinition() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

195
        $field     = $this->getFieldDefinition($this->context->getSchema(), $parentType, /** @scrutinizer ignore-type */ $fieldName);
Loading history...
196
197
        if (null === $field) {
198
            throw new UndefinedFieldException($fieldName);
0 ignored issues
show
Bug introduced by
It seems like $fieldName can also be of type null; however, parameter $fieldName of Digia\GraphQL\Execution\...xception::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

198
            throw new UndefinedFieldException(/** @scrutinizer ignore-type */ $fieldName);
Loading history...
199
        }
200
201
        $info = $this->createResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
202
203
        $resolveCallback = $this->determineResolveCallback($field, $parentType);
204
205
        $result = $this->resolveFieldValueOrError(
206
            $field,
207
            $fieldNode,
208
            $resolveCallback,
209
            $rootValue,
210
            $this->context,
211
            $info
212
        );
213
214
        $result = $this->completeValueCatchingError(
215
            $field->getType(),
0 ignored issues
show
Bug introduced by
It seems like $field->getType() can also be of type null; however, parameter $returnType of Digia\GraphQL\Execution\...eteValueCatchingError() does only seem to accept Digia\GraphQL\Type\Definition\TypeInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

215
            /** @scrutinizer ignore-type */ $field->getType(),
Loading history...
216
            $fieldNodes,
217
            $info,
218
            $path,
219
            $result
220
        );
221
222
        return $result;
223
    }
224
225
    /**
226
     * @param Field            $field
227
     * @param FieldNode        $fieldNode
228
     * @param callable         $resolveCallback
229
     * @param mixed            $rootValue
230
     * @param ExecutionContext $context
231
     * @param ResolveInfo      $info
232
     *
233
     * @return array|\Throwable
234
     */
235
    protected function resolveFieldValueOrError(
236
        Field $field,
237
        FieldNode $fieldNode,
238
        ?callable $resolveCallback,
239
        $rootValue,
240
        ExecutionContext $context,
241
        ResolveInfo $info
242
    ) {
243
        try {
244
            // Build an associative array of arguments from the field.arguments AST, using the
245
            // variables scope to fulfill any variable references.
246
            $result = \call_user_func(
247
                $resolveCallback,
248
                $rootValue,
249
                $this->valuesResolver->coerceArgumentValues($field, $fieldNode, $context->getVariableValues()),
250
                $context->getContextValue(),
251
                $info
252
            );
253
254
            if ($result instanceof PromiseInterface) {
255
                return $result->then(null, function ($exception) use ($fieldNode, $info) {
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result->then(nul...ion(...) { /* ... */ }) returns the type React\Promise\PromiseInterface which is incompatible with the documented return type Throwable|array.
Loading history...
256
                    return !$exception instanceof ExecutionException
257
                        ? $this->normalizeException($exception, [$fieldNode], $info->getPath())
0 ignored issues
show
Bug introduced by
It seems like $info->getPath() can also be of type null; however, parameter $path of Digia\GraphQL\Execution\...y::normalizeException() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

257
                        ? $this->normalizeException($exception, [$fieldNode], /** @scrutinizer ignore-type */ $info->getPath())
Loading history...
258
                        : $exception;
259
                });
260
            }
261
262
            return $result;
263
        } catch (\Throwable $exception) {
264
            return $exception;
265
        }
266
    }
267
268
    /**
269
     * Normalizes exceptions which are usually a \Throwable,
270
     * but can even be a string or null when resolving promises.
271
     *
272
     * @param mixed           $exception
273
     * @param NodeInterface[] $nodes
274
     * @param array           $path
275
     * @return ExecutionException
276
     */
277
    protected function normalizeException($exception, array $nodes, array $path = []): ExecutionException
278
    {
279
        if ($exception instanceof \Throwable) {
280
            return new ExecutionException(
281
                $exception->getMessage(),
282
                $nodes,
283
                null,
284
                null,
285
                $path,
286
                null,
287
                $exception
288
            );
289
        }
290
291
        if (\is_string($exception)) {
292
            return new ExecutionException(
293
                $exception,
294
                $nodes,
295
                null,
296
                null,
297
                $path
298
            );
299
        }
300
301
        return new ExecutionException(
302
            '',
303
            $nodes,
304
            null,
305
            null,
306
            $path
307
        );
308
    }
309
310
    /**
311
     * This is a small wrapper around completeValue which detects and logs error in the execution context.
312
     *
313
     * @param TypeInterface $returnType
314
     * @param FieldNode[]   $fieldNodes
315
     * @param ResolveInfo   $info
316
     * @param array         $path
317
     * @param mixed         $result
318
     *
319
     * @return array|null|PromiseInterface
320
     * @throws \Throwable
321
     */
322
    public function completeValueCatchingError(
323
        TypeInterface $returnType,
324
        array $fieldNodes,
325
        ResolveInfo $info,
326
        array $path,
327
        &$result
328
    ) {
329
        try {
330
            $completed = $result instanceof PromiseInterface
331
                ? $result->then(function ($resolvedResult) use ($returnType, $fieldNodes, $info, $path) {
332
                    return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolvedResult);
333
                })
334
                : $this->completeValue($returnType, $fieldNodes, $info, $path, $result);
335
336
            if ($completed instanceof PromiseInterface) {
337
                // Note: we don't rely on a `catch` method, but we do expect "thenable"
338
                // to take a second callback for the error case.
339
                return $completed->then(null, function ($exception) use ($fieldNodes, $path, $returnType) {
340
                    $this->handleFieldError($exception, $fieldNodes, $path, $returnType);
341
                });
342
            }
343
344
            return $completed;
345
        } catch (\Throwable $exception) {
346
            $this->handleFieldError($exception, $fieldNodes, $path, $returnType);
347
            return null;
348
        }
349
    }
350
351
    /**
352
     * Implements the instructions for completeValue as defined in the
353
     * "Field entries" section of the spec.
354
     *
355
     * If the field type is Non-Null, then this recursively completes the value
356
     * for the inner type. It throws a field error if that completion returns null,
357
     * as per the "Nullability" section of the spec.
358
     *
359
     * If the field type is a List, then this recursively completes the value
360
     * for the inner type on each item in the list.
361
     *
362
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
363
     * value of the type by calling the `serialize` method of GraphQL type
364
     * definition.
365
     *
366
     * If the field is an abstract type, determine the runtime type of the value
367
     * and then complete based on that type
368
     *
369
     * Otherwise, the field type expects a sub-selection set, and will complete the
370
     * value by evaluating all sub-selections.
371
     *
372
     * @param TypeInterface $returnType
373
     * @param FieldNode[]   $fieldNodes
374
     * @param ResolveInfo   $info
375
     * @param array         $path
376
     * @param mixed         $result
377
     *
378
     * @return array|null|PromiseInterface
379
     * @throws \Throwable
380
     */
381
    protected function completeValue(
382
        TypeInterface $returnType,
383
        array $fieldNodes,
384
        ResolveInfo $info,
385
        array $path,
386
        &$result
387
    ) {
388
        // If result is an Error, throw a located error.
389
        if ($result instanceof \Throwable) {
390
            throw $result;
391
        }
392
393
        // If field type is NonNull, complete for inner type, and throw field error if result is null.
394
        if ($returnType instanceof NonNullType) {
395
            $completed = $this->completeValue(
396
                $returnType->getOfType(),
397
                $fieldNodes,
398
                $info,
399
                $path,
400
                $result
401
            );
402
403
            if (null !== $completed) {
404
                return $completed;
405
            }
406
407
            throw new ExecutionException(
408
                \sprintf(
409
                    'Cannot return null for non-nullable field %s.%s.',
410
                    (string)$info->getParentType(),
411
                    $info->getFieldName()
412
                )
413
            );
414
        }
415
416
        // If result is null, return null.
417
        if (null === $result) {
418
            return null;
419
        }
420
421
        // If field type is a leaf type, Scalar or Enum, serialize to a valid value,
422
        // returning null if serialization is not possible.
423
        if ($returnType instanceof ListType) {
424
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
425
        }
426
427
        // If field type is Scalar or Enum, serialize to a valid value, returning
428
        // null if serialization is not possible.
429
        if ($returnType instanceof LeafTypeInterface) {
430
            return $this->completeLeafValue($returnType, $result);
431
        }
432
433
        // If field type is an abstract type, Interface or Union, determine the
434
        // runtime Object type and complete for that type.
435
        if ($returnType instanceof AbstractTypeInterface) {
436
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
437
        }
438
439
        // If field type is Object, execute and complete all sub-selections.
440
        if ($returnType instanceof ObjectType) {
441
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
442
        }
443
444
        throw new ExecutionException(\sprintf('Cannot complete value of unexpected type "%s".', (string)$returnType));
445
    }
446
447
    /**
448
     * @param ListType    $returnType
449
     * @param FieldNode[] $fieldNodes
450
     * @param ResolveInfo $info
451
     * @param array       $path
452
     * @param mixed       $result
453
     *
454
     * @return array|\React\Promise\Promise
455
     * @throws \Throwable
456
     */
457
    protected function completeListValue(
458
        ListType $returnType,
459
        array $fieldNodes,
460
        ResolveInfo $info,
461
        array $path,
462
        &$result
463
    ) {
464
        if (!\is_array($result) && !$result instanceof \Traversable) {
465
            throw new InvariantException(
466
                \sprintf(
467
                    'Expected Array or Traversable, but did not find one for field %s.%s.',
468
                    (string)$info->getParentType(),
469
                    $info->getFieldName()
470
                )
471
            );
472
        }
473
474
        $itemType           = $returnType->getOfType();
475
        $completedItems     = [];
476
        $doesContainPromise = false;
477
478
        foreach ($result as $key => $item) {
479
            $fieldPath          = $path;
480
            $fieldPath[]        = $key;
481
            $completedItem      = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
482
            $completedItems[]   = $completedItem;
483
            $doesContainPromise = $doesContainPromise || $completedItem instanceof PromiseInterface;
484
        }
485
486
        return $doesContainPromise
487
            ? \React\Promise\all($completedItems)
488
            : $completedItems;
489
    }
490
491
    /**
492
     * @param LeafTypeInterface|SerializableTypeInterface $returnType
493
     * @param mixed                                       $result
494
     *
495
     * @return array|PromiseInterface
496
     * @throws InvalidReturnTypeException
497
     */
498
    protected function completeLeafValue($returnType, &$result)
499
    {
500
        $result = $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

500
        /** @scrutinizer ignore-call */ 
501
        $result = $returnType->serialize($result);
Loading history...
501
502
        if (null === $result) {
503
            throw new InvalidReturnTypeException($returnType, $result);
504
        }
505
506
        return $result;
507
    }
508
509
    /**
510
     * @param AbstractTypeInterface $returnType
511
     * @param FieldNode[]           $fieldNodes
512
     * @param ResolveInfo           $info
513
     * @param string[]              $path
514
     * @param mixed                 $result
515
     *
516
     * @return array|PromiseInterface
517
     * @throws \Throwable
518
     */
519
    protected function completeAbstractValue(
520
        AbstractTypeInterface $returnType,
521
        array $fieldNodes,
522
        ResolveInfo $info,
523
        array $path,
524
        &$result
525
    ) {
526
        $runtimeType = $returnType->hasResolveTypeCallback()
527
            ? $returnType->resolveType($result, $this->context->getContextValue(), $info)
528
            : \call_user_func(
529
                $this->typeResolverCallback,
530
                $result,
531
                $this->context->getContextValue(),
532
                $info,
533
                $returnType
534
            );
535
536
        if ($runtimeType instanceof PromiseInterface) {
537
            return $runtimeType->then(function ($resolvedRuntimeType) use (
538
                $returnType,
539
                $fieldNodes,
540
                $info,
541
                $path,
542
                &$result
543
            ) {
544
                return $this->completeObjectValue(
545
                    $this->ensureValidRuntimeType($resolvedRuntimeType, $returnType, $info, $result),
546
                    $fieldNodes,
547
                    $info,
548
                    $path,
549
                    $result
550
                );
551
            });
552
        }
553
554
        $returnType = $this->ensureValidRuntimeType($runtimeType, $returnType, $info, $result);
555
556
        return $this->completeObjectValue(
557
            $returnType,
558
            $fieldNodes,
559
            $info,
560
            $path,
561
            $result
562
        );
563
    }
564
565
    /**
566
     * @param ObjectType  $returnType
567
     * @param array       $fieldNodes
568
     * @param ResolveInfo $info
569
     * @param array       $path
570
     * @param mixed       $result
571
     *
572
     * @return array|PromiseInterface
573
     * @throws ExecutionException
574
     * @throws InvalidReturnTypeException
575
     * @throws \Throwable
576
     */
577
    protected function completeObjectValue(
578
        ObjectType $returnType,
579
        array $fieldNodes,
580
        ResolveInfo $info,
581
        array $path,
582
        &$result
583
    ) {
584
        // If there is an isTypeOfCallback predicate function, call it with the
585
        // current result. If isTypeOfCallback returns false, then raise an error rather
586
        // than continuing execution.
587
        if ($returnType->hasIsTypeOfCallback()) {
588
            $isTypeOf = $returnType->isTypeOf($result, $this->context->getContextValue(), $info);
589
590
            if ($isTypeOf instanceof PromiseInterface) {
591
                return $isTypeOf->then(function ($resolvedIsTypeOf) use ($returnType, $result, $fieldNodes, $path) {
592
                    if (true === $resolvedIsTypeOf) {
593
                        return $this->executeSubFields($returnType, $fieldNodes, $path, $result);
594
                    }
595
596
                    throw new InvalidReturnTypeException($returnType, $result, $fieldNodes);
597
                });
598
            }
599
600
            if (false === $isTypeOf) {
601
                throw new InvalidReturnTypeException($returnType, $result, $fieldNodes);
602
            }
603
        }
604
605
        return $this->executeSubFields($returnType, $fieldNodes, $path, $result);
606
    }
607
608
    /**
609
     * @param ObjectType  $returnType
610
     * @param FieldNode[] $fieldNodes
611
     * @param array       $path
612
     * @param mixed       $result
613
     *
614
     * @return array|PromiseInterface
615
     * @throws \Throwable
616
     */
617
    protected function executeSubFields(ObjectType $returnType, array $fieldNodes, array $path, &$result)
618
    {
619
        $subFields            = [];
620
        $visitedFragmentNames = [];
621
622
        foreach ($fieldNodes as $fieldNode) {
623
            if (null !== $fieldNode->getSelectionSet()) {
624
                $subFields = $this->fieldCollector->collectFields(
625
                    $returnType,
626
                    $fieldNode->getSelectionSet(),
627
                    $subFields,
628
                    $visitedFragmentNames
629
                );
630
            }
631
        }
632
633
        if (!empty($subFields)) {
634
            return $this->executeFields($returnType, $result, $path, $subFields);
635
        }
636
637
        return $result;
638
    }
639
640
    /**
641
     * @param NamedTypeInterface|string $runtimeTypeOrName
642
     * @param NamedTypeInterface        $returnType
643
     * @param ResolveInfo               $info
644
     * @param mixed                     $result
645
     *
646
     * @return TypeInterface|ObjectType|null
647
     * @throws ExecutionException
648
     * @throws InvariantException
649
     */
650
    protected function ensureValidRuntimeType(
651
        $runtimeTypeOrName,
652
        NamedTypeInterface $returnType,
653
        ResolveInfo $info,
654
        &$result
655
    ) {
656
        /** @var NamedTypeInterface $runtimeType */
657
        $runtimeType = \is_string($runtimeTypeOrName)
658
            ? $this->context->getSchema()->getType($runtimeTypeOrName)
659
            : $runtimeTypeOrName;
660
661
        $runtimeTypeName = $runtimeType->getName();
662
        $returnTypeName  = $returnType->getName();
663
664
        if (!$runtimeType instanceof ObjectType) {
665
            $parentTypeName = $info->getParentType()->getName();
666
            $fieldName      = $info->getFieldName();
667
668
            throw new ExecutionException(
669
                \sprintf(
670
                    'Abstract type %s must resolve to an Object type at runtime for field %s.%s ' .
671
                    'with value "%s", received "%s".',
672
                    $returnTypeName,
673
                    $parentTypeName,
674
                    $fieldName,
675
                    $result,
676
                    $runtimeTypeName
677
                )
678
            );
679
        }
680
681
        if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
682
            throw new ExecutionException(
683
                \sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeTypeName, $returnTypeName)
684
            );
685
        }
686
687
        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...
688
            throw new ExecutionException(
689
                \sprintf(
690
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
691
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
692
                    'type instance as referenced anywhere else within the schema.',
693
                    $runtimeTypeName,
694
                    $returnTypeName
695
                )
696
            );
697
        }
698
699
        return $runtimeType;
700
    }
701
702
    /**
703
     * @param Schema     $schema
704
     * @param ObjectType $parentType
705
     * @param string     $fieldName
706
     *
707
     * @return Field|null
708
     * @throws InvariantException
709
     */
710
    public function getFieldDefinition(Schema $schema, ObjectType $parentType, string $fieldName): ?Field
711
    {
712
        if ($fieldName === '__schema' && $schema->getQueryType() === $parentType) {
713
            return SchemaMetaFieldDefinition();
714
        }
715
716
        if ($fieldName === '__type' && $schema->getQueryType() === $parentType) {
717
            return TypeMetaFieldDefinition();
718
        }
719
720
        if ($fieldName === '__typename') {
721
            return TypeNameMetaFieldDefinition();
722
        }
723
724
        $fields = $parentType->getFields();
725
726
        return $fields[$fieldName] ?? null;
727
    }
728
729
    /**
730
     * @param Field      $field
731
     * @param ObjectType $parentType
732
     *
733
     * @return callable
734
     */
735
    protected function determineResolveCallback(Field $field, ObjectType $parentType): callable
736
    {
737
        if ($field->hasResolveCallback()) {
738
            return $field->getResolveCallback();
739
        }
740
741
        if ($parentType->hasResolveCallback()) {
742
            return $parentType->getResolveCallback();
743
        }
744
745
        if ($this->context->hasFieldResolver()) {
746
            return $this->context->getFieldResolver();
747
        }
748
749
        return $this->fieldResolverCallback;
750
    }
751
752
    /**
753
     * @param \Throwable    $originalException
754
     * @param array         $fieldNodes
755
     * @param array         $path
756
     * @param TypeInterface $returnType
757
     * @throws ExecutionException
758
     */
759
    protected function handleFieldError(
760
        \Throwable $originalException,
761
        array $fieldNodes,
762
        array $path,
763
        TypeInterface $returnType
764
    ): void {
765
        $exception = $this->buildLocatedError($originalException, $fieldNodes, $path);
766
767
        // If the field type is non-nullable, then it is resolved without any
768
        // protection from errors, however it still properly locates the error.
769
        if ($returnType instanceof NonNullType) {
770
            throw $exception;
771
        }
772
773
        // Otherwise, error protection is applied, logging the error and resolving
774
        // a null value for this field if one is encountered.
775
        $this->context->addError($exception);
776
    }
777
778
    /**
779
     * @param \Throwable      $originalException
780
     * @param NodeInterface[] $nodes
781
     * @param string[]        $path
782
     *
783
     * @return ExecutionException
784
     */
785
    protected function buildLocatedError(
786
        \Throwable $originalException,
787
        array $nodes = [],
788
        array $path = []
789
    ): ExecutionException {
790
        return new ExecutionException(
791
            $originalException->getMessage(),
792
            $originalException instanceof GraphQLException
793
                ? $originalException->getNodes()
794
                : $nodes,
795
            $originalException instanceof GraphQLException
796
                ? $originalException->getSource()
797
                : null,
798
            $originalException instanceof GraphQLException
799
                ? $originalException->getPositions()
800
                : null,
801
            $originalException instanceof GraphQLException
802
                ? ($originalException->getPath() ?? $path)
803
                : $path,
804
            null,
805
            $originalException
806
        );
807
    }
808
809
    /**
810
     * @param FieldNode[]      $fieldNodes
811
     * @param FieldNode        $fieldNode
812
     * @param Field            $field
813
     * @param ObjectType       $parentType
814
     * @param array|null       $path
815
     * @param ExecutionContext $context
816
     *
817
     * @return ResolveInfo
818
     */
819
    protected function createResolveInfo(
820
        array $fieldNodes,
821
        FieldNode $fieldNode,
822
        Field $field,
823
        ObjectType $parentType,
824
        ?array $path,
825
        ExecutionContext $context
826
    ): ResolveInfo {
827
        return new ResolveInfo(
828
            $fieldNode->getNameValue(),
0 ignored issues
show
Bug introduced by
It seems like $fieldNode->getNameValue() can also be of type null; however, parameter $fieldName of Digia\GraphQL\Execution\ResolveInfo::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

828
            /** @scrutinizer ignore-type */ $fieldNode->getNameValue(),
Loading history...
829
            $fieldNodes,
830
            $field->getType(),
0 ignored issues
show
Bug introduced by
It seems like $field->getType() can also be of type null; however, parameter $returnType of Digia\GraphQL\Execution\ResolveInfo::__construct() does only seem to accept Digia\GraphQL\Type\Definition\TypeInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

830
            /** @scrutinizer ignore-type */ $field->getType(),
Loading history...
831
            $parentType,
832
            $path,
833
            $context->getSchema(),
834
            $context->getFragments(),
835
            $context->getRootValue(),
836
            $context->getOperation(),
837
            $context->getVariableValues()
838
        );
839
    }
840
841
    /**
842
     * @param mixed                 $value
843
     * @param mixed                 $context
844
     * @param ResolveInfo           $info
845
     * @param AbstractTypeInterface $abstractType
846
     *
847
     * @return mixed
848
     * @throws InvariantException
849
     */
850
    public static function defaultTypeResolver(
851
        $value,
852
        $context,
853
        ResolveInfo $info,
854
        AbstractTypeInterface $abstractType
855
    ) {
856
        // First, look for `__typename`.
857
        if (\is_array($value) && isset($value['__typename'])) {
858
            return $value['__typename'];
859
        }
860
861
        // Otherwise, test each possible type.
862
863
        /** @var ObjectType[] $possibleTypes */
864
        $possibleTypes = $info->getSchema()->getPossibleTypes($abstractType);
865
        $promises      = [];
866
867
        foreach ($possibleTypes as $index => $type) {
868
            $isTypeOf = $type->isTypeOf($value, $context, $info);
869
870
            if ($isTypeOf instanceof PromiseInterface) {
871
                $promises[$index] = $isTypeOf;
872
            }
873
874
            if (true === $isTypeOf) {
875
                return $type;
876
            }
877
        }
878
879
        if (!empty($promises)) {
880
            return \React\Promise\all($promises)->then(function ($resolvedPromises) use ($possibleTypes) {
881
                foreach ($resolvedPromises as $index => $result) {
882
                    if (true === $result) {
883
                        return $possibleTypes[$index];
884
                    }
885
                }
886
887
                return null;
888
            });
889
        }
890
891
        return null;
892
    }
893
894
    /**
895
     * Try to resolve a field without any field resolver function.
896
     *
897
     * @param array|object $rootValue
898
     * @param array        $arguments
899
     * @param mixed        $contextValues
900
     * @param ResolveInfo  $info
901
     *
902
     * @return mixed|null
903
     */
904
    public static function defaultFieldResolver($rootValue, array $arguments, $contextValues, ResolveInfo $info)
905
    {
906
        $fieldName = $info->getFieldName();
907
        $property  = null;
908
909
        if (\is_array($rootValue) && isset($rootValue[$fieldName])) {
910
            $property = $rootValue[$fieldName];
911
        }
912
913
        if (\is_object($rootValue)) {
914
            $getter = 'get' . \ucfirst($fieldName);
915
            if (\method_exists($rootValue, $getter)) {
916
                $property = $rootValue->{$getter}();
917
            } elseif (\method_exists($rootValue, $fieldName)) {
918
                $property = $rootValue->{$fieldName}($rootValue, $arguments, $contextValues, $info);
919
            } elseif (\property_exists($rootValue, $fieldName)) {
920
                $property = $rootValue->{$fieldName};
921
            }
922
        }
923
924
        return $property instanceof \Closure
925
            ? \call_user_func($property, $rootValue, $arguments, $contextValues, $info)
926
            : $property;
927
    }
928
}
929