AbstractExecutionStrategy   F
last analyzed

Complexity

Total Complexity 93

Size/Duplication

Total Lines 885
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 322
dl 0
loc 885
rs 2
c 1
b 0
f 0
wmc 93

21 Methods

Rating   Name   Duplication   Size   Complexity  
A executeSubFields() 0 21 4
A createResolveInfo() 0 19 1
B completeValue() 0 64 9
A completeObjectValue() 0 29 5
A __construct() 0 10 1
A getFieldDefinition() 0 17 6
A handleFieldError() 0 17 2
A resolveFieldValueOrError() 0 29 4
A getOperationType() 0 23 6
A completeAbstractValue() 0 43 3
B defaultFieldResolver() 0 23 8
A completeLeafValue() 0 9 2
A normalizeException() 0 30 3
A buildLocatedError() 0 21 5
A execute() 0 38 4
A completeValueCatchingError() 0 26 4
A ensureValidRuntimeType() 0 50 5
A determineResolveCallback() 0 15 4
A completeListValue() 0 32 6
A resolveField() 0 33 2
B defaultTypeResolver() 0 42 9

How to fix   Complexity   

Complex Class

Complex classes like AbstractExecutionStrategy 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 AbstractExecutionStrategy, and based on these observations, apply Extract Interface, too.

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 callable
47
     */
48
    protected $typeResolverCallback;
49
50
    /**
51
     * @var callable
52
     */
53
    protected $fieldResolverCallback;
54
55
    /**
56
     * @param ObjectType $parentType
57
     * @param mixed      $rootValue
58
     * @param array      $path
59
     * @param array      $fields
60
     * @return array|PromiseInterface
61
     * @throws \Throwable
62
     * @throws ExecutionException
63
     */
64
    abstract public function executeFields(ObjectType $parentType, $rootValue, array $path, array $fields);
65
66
    /**
67
     * AbstractExecutionStrategy constructor.
68
     *
69
     * @param ExecutionContext $context
70
     * @param FieldCollector   $fieldCollector
71
     * @param callable|null    $typeResolverCallback
72
     * @param callable|null    $fieldResolverCallback
73
     */
74
    public function __construct(
75
        ExecutionContext $context,
76
        FieldCollector $fieldCollector,
77
        ?callable $typeResolverCallback = null,
78
        ?callable $fieldResolverCallback = null
79
    ) {
80
        $this->context               = $context;
81
        $this->fieldCollector        = $fieldCollector;
82
        $this->typeResolverCallback  = $typeResolverCallback ?? [$this, 'defaultTypeResolver'];
83
        $this->fieldResolverCallback = $fieldResolverCallback ?? [$this, 'defaultFieldResolver'];
84
    }
85
86
    /**
87
     * @inheritdoc
88
     * @throws ExecutionException
89
     * @throws InvalidTypeException
90
     * @throws InvariantException
91
     * @throws ConversionException
92
     * @throws \Throwable
93
     */
94
    public function execute()
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,
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

107
            /** @scrutinizer ignore-type */ $objectType,
Loading history...
108
            $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

108
            /** @scrutinizer ignore-type */ $operation->getSelectionSet(),
Loading history...
109
            $fields,
110
            $visitedFragmentNames
111
        );
112
113
        // Errors from sub-fields of a NonNull type may propagate to the top level,
114
        // at which point we still log the error and null the parent field, which
115
        // in this case is the entire response.
116
        try {
117
            $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

117
            $result = $this->executeFields(/** @scrutinizer ignore-type */ $objectType, $rootValue, $path, $fields);
Loading history...
118
        } catch (ExecutionException $exception) {
119
            $this->context->addError($exception);
120
            return null;
121
        } catch (\Throwable $exception) {
122
            $exception = !$exception instanceof ExecutionException
123
                ? $this->normalizeException($exception, $fields, $path)
124
                : $exception;
125
126
            $this->context->addError($exception);
127
128
            return null;
129
        }
130
131
        return $result;
132
    }
133
134
    /**
135
     * @param Schema                  $schema
136
     * @param OperationDefinitionNode $operation
137
     *
138
     * @return ObjectType|null
139
     * @throws ExecutionException
140
     */
141
    protected function getOperationType(Schema $schema, OperationDefinitionNode $operation): ?ObjectType
142
    {
143
        switch ($operation->getOperation()) {
144
            case 'query':
145
                return $schema->getQueryType();
146
            case 'mutation':
147
                $mutationType = $schema->getMutationType();
148
149
                if (null === $mutationType) {
150
                    throw new ExecutionException('Schema is not configured for mutations.', [$operation]);
151
                }
152
153
                return $mutationType;
154
            case 'subscription':
155
                $subscriptionType = $schema->getSubscriptionType();
156
157
                if (null === $subscriptionType) {
158
                    throw new ExecutionException('Schema is not configured for subscriptions.', [$operation]);
159
                }
160
161
                return $subscriptionType;
162
            default:
163
                throw new ExecutionException('Can only execute queries, mutations and subscriptions.', [$operation]);
164
        }
165
    }
166
167
    /**
168
     * Resolves the field on the given source object. In particular, this
169
     * figures out the value that the field returns by calling its resolve function,
170
     * then calls completeValue to complete promises, serialize scalars, or execute
171
     * the sub-selection-set for objects.
172
     *
173
     * @param ObjectType  $parentType
174
     * @param mixed       $rootValue
175
     * @param FieldNode[] $fieldNodes
176
     * @param string[]    $path
177
     *
178
     * @return mixed
179
     * @throws \Throwable
180
     * @throws UndefinedFieldException
181
     */
182
    protected function resolveField(ObjectType $parentType, $rootValue, array $fieldNodes, array $path)
183
    {
184
        $fieldNode = $fieldNodes[0];
185
186
        $fieldName = $fieldNode->getNameValue();
187
        $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

187
        $field     = $this->getFieldDefinition($this->context->getSchema(), $parentType, /** @scrutinizer ignore-type */ $fieldName);
Loading history...
188
189
        if (null === $field) {
190
            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

190
            throw new UndefinedFieldException(/** @scrutinizer ignore-type */ $fieldName);
Loading history...
191
        }
192
193
        $info = $this->createResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
194
195
        $resolveCallback = $this->determineResolveCallback($field, $parentType);
196
197
        $result = $this->resolveFieldValueOrError(
198
            $field,
199
            $fieldNode,
200
            $resolveCallback,
201
            $rootValue,
202
            $this->context,
203
            $info
204
        );
205
206
        $result = $this->completeValueCatchingError(
207
            $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

207
            /** @scrutinizer ignore-type */ $field->getType(),
Loading history...
208
            $fieldNodes,
209
            $info,
210
            $path,
211
            $result
212
        );
213
214
        return $result;
215
    }
216
217
    /**
218
     * @param Field            $field
219
     * @param FieldNode        $fieldNode
220
     * @param callable         $resolveCallback
221
     * @param mixed            $rootValue
222
     * @param ExecutionContext $context
223
     * @param ResolveInfo      $info
224
     *
225
     * @return array|\Throwable
226
     */
227
    protected function resolveFieldValueOrError(
228
        Field $field,
229
        FieldNode $fieldNode,
230
        ?callable $resolveCallback,
231
        $rootValue,
232
        ExecutionContext $context,
233
        ResolveInfo $info
234
    ) {
235
        try {
236
            // Build an associative array of arguments from the field.arguments AST, using the
237
            // variables scope to fulfill any variable references.
238
            $result = $resolveCallback(
239
                $rootValue,
240
                ValuesResolver::coerceArgumentValues($field, $fieldNode, $context->getVariableValues()),
241
                $context->getContextValue(),
242
                $info
243
            );
244
245
            if ($result instanceof PromiseInterface) {
246
                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...
247
                    return !$exception instanceof ExecutionException
248
                        ? $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

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

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

819
            /** @scrutinizer ignore-type */ $fieldNode->getNameValue(),
Loading history...
820
            $fieldNodes,
821
            $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

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