• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

webonyx / graphql-php / v14.11.9

pending completion
v14.11.9

push

github

spawnia
14.11.9

8491 of 9858 relevant lines covered (86.13%)

248.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.39
/src/Executor/ReferenceExecutor.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace GraphQL\Executor;
6

7
use ArrayAccess;
8
use ArrayObject;
9
use Exception;
10
use GraphQL\Error\Error;
11
use GraphQL\Error\InvariantViolation;
12
use GraphQL\Error\Warning;
13
use GraphQL\Executor\Promise\Promise;
14
use GraphQL\Executor\Promise\PromiseAdapter;
15
use GraphQL\Language\AST\DocumentNode;
16
use GraphQL\Language\AST\FieldNode;
17
use GraphQL\Language\AST\FragmentDefinitionNode;
18
use GraphQL\Language\AST\FragmentSpreadNode;
19
use GraphQL\Language\AST\InlineFragmentNode;
20
use GraphQL\Language\AST\Node;
21
use GraphQL\Language\AST\OperationDefinitionNode;
22
use GraphQL\Language\AST\SelectionNode;
23
use GraphQL\Language\AST\SelectionSetNode;
24
use GraphQL\Type\Definition\AbstractType;
25
use GraphQL\Type\Definition\Directive;
26
use GraphQL\Type\Definition\FieldDefinition;
27
use GraphQL\Type\Definition\InterfaceType;
28
use GraphQL\Type\Definition\LeafType;
29
use GraphQL\Type\Definition\ListOfType;
30
use GraphQL\Type\Definition\NonNull;
31
use GraphQL\Type\Definition\ObjectType;
32
use GraphQL\Type\Definition\ResolveInfo;
33
use GraphQL\Type\Definition\Type;
34
use GraphQL\Type\Definition\UnionType;
35
use GraphQL\Type\Introspection;
36
use GraphQL\Type\Schema;
37
use GraphQL\Utils\TypeInfo;
38
use GraphQL\Utils\Utils;
39
use RuntimeException;
40
use SplObjectStorage;
41
use stdClass;
42
use Throwable;
43
use Traversable;
44
use function array_keys;
45
use function array_merge;
46
use function array_reduce;
47
use function array_values;
48
use function count;
49
use function get_class;
50
use function is_array;
51
use function is_callable;
52
use function is_string;
53
use function sprintf;
54

55
class ReferenceExecutor implements ExecutorImplementation
56
{
57
    /** @var object */
58
    protected static $UNDEFINED;
59

60
    /** @var ExecutionContext */
61
    protected $exeContext;
62

63
    /** @var SplObjectStorage */
64
    protected $subFieldCache;
65

66
    protected function __construct(ExecutionContext $context)
67
    {
×
68
        if (! static::$UNDEFINED) {
261✔
69
            static::$UNDEFINED = Utils::undefined();
1✔
70
        }
×
71
        $this->exeContext    = $context;
261✔
72
        $this->subFieldCache = new SplObjectStorage();
261✔
73
    }
261✔
74

75
    /**
76
     * @param mixed                    $rootValue
77
     * @param mixed                    $contextValue
78
     * @param array<mixed>|Traversable $variableValues
79
     */
80
    public static function create(
81
        PromiseAdapter $promiseAdapter,
×
82
        Schema $schema,
×
83
        DocumentNode $documentNode,
×
84
        $rootValue,
×
85
        $contextValue,
×
86
        $variableValues,
×
87
        ?string $operationName,
×
88
        callable $fieldResolver
×
89
    ) : ExecutorImplementation {
×
90
        $exeContext = static::buildExecutionContext(
276✔
91
            $schema,
276✔
92
            $documentNode,
×
93
            $rootValue,
×
94
            $contextValue,
×
95
            $variableValues,
×
96
            $operationName,
×
97
            $fieldResolver,
×
98
            $promiseAdapter
×
99
        );
×
100

101
        if (is_array($exeContext)) {
276✔
102
            return new class($promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext))) implements ExecutorImplementation
103
            {
×
104
                /** @var Promise */
105
                private $result;
×
106

107
                public function __construct(Promise $result)
108
                {
×
109
                    $this->result = $result;
16✔
110
                }
16✔
111

112
                public function doExecute() : Promise
113
                {
×
114
                    return $this->result;
16✔
115
                }
×
116
            };
×
117
        }
×
118

119
        return new static($exeContext);
261✔
120
    }
×
121

122
    /**
123
     * Constructs an ExecutionContext object from the arguments passed to
124
     * execute, which we will pass throughout the other execution methods.
125
     *
126
     * @param mixed                    $rootValue
127
     * @param mixed                    $contextValue
128
     * @param array<mixed>|Traversable $rawVariableValues
129
     *
130
     * @return ExecutionContext|array<Error>
131
     */
132
    protected static function buildExecutionContext(
133
        Schema $schema,
×
134
        DocumentNode $documentNode,
×
135
        $rootValue,
×
136
        $contextValue,
×
137
        $rawVariableValues,
×
138
        ?string $operationName = null,
×
139
        ?callable $fieldResolver = null,
×
140
        ?PromiseAdapter $promiseAdapter = null
×
141
    ) {
×
142
        $errors    = [];
276✔
143
        $fragments = [];
276✔
144
        /** @var OperationDefinitionNode|null $operation */
145
        $operation                    = null;
276✔
146
        $hasMultipleAssumedOperations = false;
276✔
147
        foreach ($documentNode->definitions as $definition) {
276✔
148
            switch (true) {
×
149
                case $definition instanceof OperationDefinitionNode:
276✔
150
                    if ($operationName === null && $operation !== null) {
274✔
151
                        $hasMultipleAssumedOperations = true;
1✔
152
                    }
×
153
                    if ($operationName === null ||
274✔
154
                        (isset($definition->name) && $definition->name->value === $operationName)) {
274✔
155
                        $operation = $definition;
273✔
156
                    }
×
157
                    break;
274✔
158
                case $definition instanceof FragmentDefinitionNode:
60✔
159
                    $fragments[$definition->name->value] = $definition;
59✔
160
                    break;
59✔
161
            }
×
162
        }
×
163
        if ($operation === null) {
276✔
164
            if ($operationName === null) {
3✔
165
                $errors[] = new Error('Must provide an operation.');
2✔
166
            } else {
×
167
                $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
3✔
168
            }
×
169
        } elseif ($hasMultipleAssumedOperations) {
273✔
170
            $errors[] = new Error(
1✔
171
                'Must provide operation name if query contains multiple operations.'
1✔
172
            );
×
173
        }
×
174
        $variableValues = null;
276✔
175
        if ($operation !== null) {
276✔
176
            [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
273✔
177
                $schema,
×
178
                $operation->variableDefinitions ?? [],
273✔
179
                $rawVariableValues ?? []
273✔
180
            );
×
181
            if (count($coercionErrors ?? []) === 0) {
273✔
182
                $variableValues = $coercedVariableValues;
262✔
183
            } else {
×
184
                $errors = array_merge($errors, $coercionErrors);
12✔
185
            }
×
186
        }
×
187
        if (count($errors) > 0) {
276✔
188
            return $errors;
16✔
189
        }
×
190
        Utils::invariant($operation, 'Has operation if no errors.');
261✔
191
        Utils::invariant($variableValues !== null, 'Has variables if no errors.');
261✔
192

193
        return new ExecutionContext(
261✔
194
            $schema,
261✔
195
            $fragments,
×
196
            $rootValue,
×
197
            $contextValue,
×
198
            $operation,
×
199
            $variableValues,
×
200
            $errors,
×
201
            $fieldResolver,
×
202
            $promiseAdapter
×
203
        );
×
204
    }
×
205

206
    public function doExecute() : Promise
207
    {
×
208
        // Return a Promise that will eventually resolve to the data described by
209
        // the "Response" section of the GraphQL specification.
210
        //
211
        // If errors are encountered while executing a GraphQL field, only that
212
        // field and its descendants will be omitted, and sibling fields will still
213
        // be executed. An execution which encounters errors will still result in a
214
        // resolved Promise.
215
        $data   = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
261✔
216
        $result = $this->buildResponse($data);
261✔
217

218
        // Note: we deviate here from the reference implementation a bit by always returning promise
219
        // But for the "sync" case it is always fulfilled
220
        return $this->isPromise($result)
261✔
221
            ? $result
41✔
222
            : $this->exeContext->promiseAdapter->createFulfilled($result);
261✔
223
    }
×
224

225
    /**
226
     * @param mixed|Promise|null $data
227
     *
228
     * @return ExecutionResult|Promise
229
     */
230
    protected function buildResponse($data)
231
    {
×
232
        if ($this->isPromise($data)) {
261✔
233
            return $data->then(function ($resolved) {
234
                return $this->buildResponse($resolved);
41✔
235
            });
41✔
236
        }
×
237
        if ($data !== null) {
261✔
238
            $data = (array) $data;
257✔
239
        }
×
240

241
        return new ExecutionResult($data, $this->exeContext->errors);
261✔
242
    }
×
243

244
    /**
245
     * Implements the "Evaluating operations" section of the spec.
246
     *
247
     * @param mixed $rootValue
248
     *
249
     * @return array<mixed>|Promise|stdClass|null
250
     */
251
    protected function executeOperation(OperationDefinitionNode $operation, $rootValue)
252
    {
×
253
        $type   = $this->getOperationRootType($this->exeContext->schema, $operation);
261✔
254
        $fields = $this->collectFields($type, $operation->selectionSet, new ArrayObject(), new ArrayObject());
261✔
255
        $path   = [];
261✔
256
        // Errors from sub-fields of a NonNull type may propagate to the top level,
257
        // at which point we still log the error and null the parent field, which
258
        // in this case is the entire response.
259
        //
260
        // Similar to completeValueCatchingError.
261
        try {
×
262
            $result = $operation->operation === 'mutation'
261✔
263
                ? $this->executeFieldsSerially($type, $rootValue, $path, $fields)
6✔
264
                : $this->executeFields($type, $rootValue, $path, $fields);
261✔
265
            if ($this->isPromise($result)) {
259✔
266
                return $result->then(
41✔
267
                    null,
41✔
268
                    function ($error) : ?Promise {
269
                        if ($error instanceof Error) {
2✔
270
                            $this->exeContext->addError($error);
2✔
271

272
                            return $this->exeContext->promiseAdapter->createFulfilled(null);
2✔
273
                        }
×
274

275
                        return null;
×
276
                    }
41✔
277
                );
×
278
            }
×
279

280
            return $result;
219✔
281
        } catch (Error $error) {
2✔
282
            $this->exeContext->addError($error);
2✔
283

284
            return null;
2✔
285
        }
×
286
    }
×
287

288
    /**
289
     * Extracts the root type of the operation from the schema.
290
     *
291
     * @throws Error
292
     */
293
    protected function getOperationRootType(Schema $schema, OperationDefinitionNode $operation) : ObjectType
294
    {
×
295
        switch ($operation->operation) {
261✔
296
            case 'query':
261✔
297
                $queryType = $schema->getQueryType();
253✔
298
                if ($queryType === null) {
253✔
299
                    throw new Error(
×
300
                        'Schema does not define the required query root type.',
×
301
                        [$operation]
×
302
                    );
×
303
                }
×
304

305
                return $queryType;
253✔
306
            case 'mutation':
8✔
307
                $mutationType = $schema->getMutationType();
6✔
308
                if ($mutationType === null) {
6✔
309
                    throw new Error(
×
310
                        'Schema is not configured for mutations.',
×
311
                        [$operation]
×
312
                    );
×
313
                }
×
314

315
                return $mutationType;
6✔
316
            case 'subscription':
2✔
317
                $subscriptionType = $schema->getSubscriptionType();
2✔
318
                if ($subscriptionType === null) {
2✔
319
                    throw new Error(
×
320
                        'Schema is not configured for subscriptions.',
×
321
                        [$operation]
×
322
                    );
×
323
                }
×
324

325
                return $subscriptionType;
2✔
326
            default:
×
327
                throw new Error(
×
328
                    'Can only execute queries, mutations and subscriptions.',
×
329
                    [$operation]
×
330
                );
×
331
        }
×
332
    }
×
333

334
    /**
335
     * Given a selectionSet, adds all of the fields in that selection to
336
     * the passed in map of fields, and returns it at the end.
337
     *
338
     * CollectFields requires the "runtime type" of an object. For a field which
339
     * returns an Interface or Union type, the "runtime type" will be the actual
340
     * Object type returned by that field.
341
     */
342
    protected function collectFields(
343
        ObjectType $runtimeType,
×
344
        SelectionSetNode $selectionSet,
×
345
        ArrayObject $fields,
×
346
        ArrayObject $visitedFragmentNames
×
347
    ) : ArrayObject {
×
348
        $exeContext = $this->exeContext;
261✔
349
        foreach ($selectionSet->selections as $selection) {
261✔
350
            switch (true) {
×
351
                case $selection instanceof FieldNode:
261✔
352
                    if (! $this->shouldIncludeNode($selection)) {
261✔
353
                        break;
2✔
354
                    }
×
355
                    $name = static::getFieldEntryKey($selection);
261✔
356
                    if (! isset($fields[$name])) {
261✔
357
                        $fields[$name] = new ArrayObject();
261✔
358
                    }
×
359
                    $fields[$name][] = $selection;
261✔
360
                    break;
261✔
361
                case $selection instanceof InlineFragmentNode:
72✔
362
                    if (! $this->shouldIncludeNode($selection) ||
22✔
363
                        ! $this->doesFragmentConditionMatch($selection, $runtimeType)
22✔
364
                    ) {
×
365
                        break;
20✔
366
                    }
×
367
                    $this->collectFields(
22✔
368
                        $runtimeType,
22✔
369
                        $selection->selectionSet,
22✔
370
                        $fields,
×
371
                        $visitedFragmentNames
×
372
                    );
×
373
                    break;
22✔
374
                case $selection instanceof FragmentSpreadNode:
52✔
375
                    $fragName = $selection->name->value;
52✔
376

377
                    if (($visitedFragmentNames[$fragName] ?? false) === true || ! $this->shouldIncludeNode($selection)) {
52✔
378
                        break;
2✔
379
                    }
×
380
                    $visitedFragmentNames[$fragName] = true;
52✔
381
                    /** @var FragmentDefinitionNode|null $fragment */
382
                    $fragment = $exeContext->fragments[$fragName] ?? null;
52✔
383
                    if ($fragment === null || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
52✔
384
                        break;
×
385
                    }
×
386
                    $this->collectFields(
52✔
387
                        $runtimeType,
52✔
388
                        $fragment->selectionSet,
52✔
389
                        $fields,
×
390
                        $visitedFragmentNames
×
391
                    );
×
392
                    break;
52✔
393
            }
×
394
        }
×
395

396
        return $fields;
261✔
397
    }
×
398

399
    /**
400
     * Determines if a field should be included based on the @include and @skip
401
     * directives, where @skip has higher precedence than @include.
402
     *
403
     * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
404
     */
405
    protected function shouldIncludeNode(SelectionNode $node) : bool
406
    {
×
407
        $variableValues = $this->exeContext->variableValues;
261✔
408
        $skipDirective  = Directive::skipDirective();
261✔
409
        $skip           = Values::getDirectiveValues(
261✔
410
            $skipDirective,
261✔
411
            $node,
×
412
            $variableValues
×
413
        );
×
414
        if (isset($skip['if']) && $skip['if'] === true) {
261✔
415
            return false;
5✔
416
        }
×
417
        $includeDirective = Directive::includeDirective();
261✔
418
        $include          = Values::getDirectiveValues(
261✔
419
            $includeDirective,
261✔
420
            $node,
×
421
            $variableValues
×
422
        );
×
423

424
        return ! isset($include['if']) || $include['if'] !== false;
261✔
425
    }
×
426

427
    /**
428
     * Implements the logic to compute the key of a given fields entry
429
     */
430
    protected static function getFieldEntryKey(FieldNode $node) : string
431
    {
×
432
        return $node->alias === null ? $node->name->value : $node->alias->value;
261✔
433
    }
×
434

435
    /**
436
     * Determines if a fragment is applicable to the given type.
437
     *
438
     * @param FragmentDefinitionNode|InlineFragmentNode $fragment
439
     */
440
    protected function doesFragmentConditionMatch(Node $fragment, ObjectType $type) : bool
441
    {
×
442
        $typeConditionNode = $fragment->typeCondition;
72✔
443
        if ($typeConditionNode === null) {
72✔
444
            return true;
1✔
445
        }
×
446
        $conditionalType = TypeInfo::typeFromAST($this->exeContext->schema, $typeConditionNode);
71✔
447
        if ($conditionalType === $type) {
71✔
448
            return true;
71✔
449
        }
×
450
        if ($conditionalType instanceof AbstractType) {
18✔
451
            return $this->exeContext->schema->isSubType($conditionalType, $type);
2✔
452
        }
×
453

454
        return false;
18✔
455
    }
×
456

457
    /**
458
     * Implements the "Evaluating selection sets" section of the spec
459
     * for "write" mode.
460
     *
461
     * @param mixed             $rootValue
462
     * @param array<string|int> $path
463
     *
464
     * @return array<mixed>|Promise|stdClass
465
     */
466
    protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
467
    {
×
468
        $result = $this->promiseReduce(
6✔
469
            array_keys($fields->getArrayCopy()),
6✔
470
            function ($results, $responseName) use ($path, $parentType, $rootValue, $fields) {
471
                $fieldNodes  = $fields[$responseName];
6✔
472
                $fieldPath   = $path;
6✔
473
                $fieldPath[] = $responseName;
6✔
474
                $result      = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
6✔
475
                if ($result === static::$UNDEFINED) {
6✔
476
                    return $results;
1✔
477
                }
×
478
                $promise = $this->getPromise($result);
5✔
479
                if ($promise !== null) {
5✔
480
                    return $promise->then(static function ($resolvedResult) use ($responseName, $results) {
481
                        $results[$responseName] = $resolvedResult;
2✔
482

483
                        return $results;
2✔
484
                    });
2✔
485
                }
×
486
                $results[$responseName] = $result;
5✔
487

488
                return $results;
5✔
489
            },
6✔
490
            []
6✔
491
        );
×
492

493
        if ($this->isPromise($result)) {
6✔
494
            return $result->then(static function ($resolvedResults) {
495
                return static::fixResultsIfEmptyArray($resolvedResults);
2✔
496
            });
2✔
497
        }
×
498

499
        return static::fixResultsIfEmptyArray($result);
4✔
500
    }
×
501

502
    /**
503
     * Resolves the field on the given root value.
504
     *
505
     * In particular, this figures out the value that the field returns
506
     * by calling its resolve function, then calls completeValue to complete promises,
507
     * serialize scalars, or execute the sub-selection-set for objects.
508
     *
509
     * @param mixed             $rootValue
510
     * @param array<string|int> $path
511
     *
512
     * @return array<mixed>|Throwable|mixed|null
513
     */
514
    protected function resolveField(ObjectType $parentType, $rootValue, ArrayObject $fieldNodes, array $path)
515
    {
×
516
        $exeContext = $this->exeContext;
261✔
517
        $fieldNode  = $fieldNodes[0];
261✔
518
        $fieldName  = $fieldNode->name->value;
261✔
519
        $fieldDef   = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
261✔
520
        if ($fieldDef === null) {
261✔
521
            return static::$UNDEFINED;
7✔
522
        }
×
523
        $returnType = $fieldDef->getType();
257✔
524
        // The resolve function's optional 3rd argument is a context value that
525
        // is provided to every resolve function within an execution. It is commonly
526
        // used to represent an authenticated user, or request-specific caches.
527
        // The resolve function's optional 4th argument is a collection of
528
        // information about the current execution state.
529
        $info = new ResolveInfo(
257✔
530
            $fieldDef,
257✔
531
            $fieldNodes,
×
532
            $parentType,
×
533
            $path,
×
534
            $exeContext->schema,
257✔
535
            $exeContext->fragments,
257✔
536
            $exeContext->rootValue,
257✔
537
            $exeContext->operation,
257✔
538
            $exeContext->variableValues
257✔
539
        );
×
540
        if ($fieldDef->resolveFn !== null) {
257✔
541
            $resolveFn = $fieldDef->resolveFn;
203✔
542
        } elseif ($parentType->resolveFieldFn !== null) {
103✔
543
            $resolveFn = $parentType->resolveFieldFn;
2✔
544
        } else {
×
545
            $resolveFn = $this->exeContext->fieldResolver;
101✔
546
        }
×
547
        // Get the resolve function, regardless of if its result is normal
548
        // or abrupt (error).
549
        $result = $this->resolveFieldValueOrError(
257✔
550
            $fieldDef,
257✔
551
            $fieldNode,
×
552
            $resolveFn,
×
553
            $rootValue,
×
554
            $info
×
555
        );
×
556
        $result = $this->completeValueCatchingError(
257✔
557
            $returnType,
257✔
558
            $fieldNodes,
×
559
            $info,
×
560
            $path,
×
561
            $result
×
562
        );
×
563

564
        return $result;
255✔
565
    }
×
566

567
    /**
568
     * This method looks up the field on the given type definition.
569
     *
570
     * It has special casing for the two introspection fields, __schema
571
     * and __typename. __typename is special because it can always be
572
     * queried as a field, even in situations where no other fields
573
     * are allowed, like on a Union. __schema could get automatically
574
     * added to the query type, but that would require mutating type
575
     * definitions, which would cause issues.
576
     */
577
    protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
578
    {
×
579
        static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
261✔
580
        $schemaMetaFieldDef   = $schemaMetaFieldDef ?? Introspection::schemaMetaFieldDef();
261✔
581
        $typeMetaFieldDef     = $typeMetaFieldDef ?? Introspection::typeMetaFieldDef();
261✔
582
        $typeNameMetaFieldDef = $typeNameMetaFieldDef ?? Introspection::typeNameMetaFieldDef();
261✔
583
        if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
261✔
584
            return $schemaMetaFieldDef;
48✔
585
        }
×
586

587
        if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
261✔
588
            return $typeMetaFieldDef;
15✔
589
        }
×
590

591
        if ($fieldName === $typeNameMetaFieldDef->name) {
261✔
592
            return $typeNameMetaFieldDef;
7✔
593
        }
×
594

595
        return $parentType->findField($fieldName);
261✔
596
    }
×
597

598
    /**
599
     * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function.
600
     * Returns the result of resolveFn or the abrupt-return Error object.
601
     *
602
     * @param mixed $rootValue
603
     *
604
     * @return Throwable|Promise|mixed
605
     */
606
    protected function resolveFieldValueOrError(
607
        FieldDefinition $fieldDef,
×
608
        FieldNode $fieldNode,
×
609
        callable $resolveFn,
×
610
        $rootValue,
×
611
        ResolveInfo $info
×
612
    ) {
×
613
        try {
×
614
            // Build a map of arguments from the field.arguments AST, using the
615
            // variables scope to fulfill any variable references.
616
            $args         = Values::getArgumentValues(
257✔
617
                $fieldDef,
257✔
618
                $fieldNode,
×
619
                $this->exeContext->variableValues
257✔
620
            );
×
621
            $contextValue = $this->exeContext->contextValue;
250✔
622

623
            return $resolveFn($rootValue, $args, $contextValue, $info);
250✔
624
        } catch (Throwable $error) {
21✔
625
            return $error;
21✔
626
        }
×
627
    }
×
628

629
    /**
630
     * This is a small wrapper around completeValue which detects and logs errors
631
     * in the execution context.
632
     *
633
     * @param array<string|int> $path
634
     * @param mixed             $result
635
     *
636
     * @return array<mixed>|Promise|stdClass|null
637
     */
638
    protected function completeValueCatchingError(
639
        Type $returnType,
×
640
        ArrayObject $fieldNodes,
×
641
        ResolveInfo $info,
×
642
        array $path,
×
643
        $result
×
644
    ) {
×
645
        // Otherwise, error protection is applied, logging the error and resolving
646
        // a null value for this field if one is encountered.
647
        try {
×
648
            $promise = $this->getPromise($result);
257✔
649
            if ($promise !== null) {
257✔
650
                $completed = $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) {
651
                    return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved);
32✔
652
                });
35✔
653
            } else {
×
654
                $completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $result);
249✔
655
            }
×
656

657
            $promise = $this->getPromise($completed);
238✔
658
            if ($promise !== null) {
238✔
659
                return $promise->then(null, function ($error) use ($fieldNodes, $path, $returnType) : void {
660
                    $this->handleFieldError($error, $fieldNodes, $path, $returnType);
26✔
661
                });
41✔
662
            }
×
663

664
            return $completed;
217✔
665
        } catch (Throwable $err) {
41✔
666
            $this->handleFieldError($err, $fieldNodes, $path, $returnType);
41✔
667

668
            return null;
33✔
669
        }
×
670
    }
×
671

672
    /**
673
     * @param mixed             $rawError
674
     * @param array<string|int> $path
675
     *
676
     * @throws Error
677
     */
678
    protected function handleFieldError($rawError, ArrayObject $fieldNodes, array $path, Type $returnType) : void
679
    {
×
680
        $error = Error::createLocatedError(
57✔
681
            $rawError,
57✔
682
            $fieldNodes,
57✔
683
            $path
57✔
684
        );
×
685

686
        // If the field type is non-nullable, then it is resolved without any
687
        // protection from errors, however it still properly locates the error.
688
        if ($returnType instanceof NonNull) {
57✔
689
            throw $error;
22✔
690
        }
×
691
        // Otherwise, error protection is applied, logging the error and resolving
692
        // a null value for this field if one is encountered.
693
        $this->exeContext->addError($error);
53✔
694
    }
53✔
695

696
    /**
697
     * Implements the instructions for completeValue as defined in the
698
     * "Field entries" section of the spec.
699
     *
700
     * If the field type is Non-Null, then this recursively completes the value
701
     * for the inner type. It throws a field error if that completion returns null,
702
     * as per the "Nullability" section of the spec.
703
     *
704
     * If the field type is a List, then this recursively completes the value
705
     * for the inner type on each item in the list.
706
     *
707
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
708
     * value of the type by calling the `serialize` method of GraphQL type
709
     * definition.
710
     *
711
     * If the field is an abstract type, determine the runtime type of the value
712
     * and then complete based on that type
713
     *
714
     * Otherwise, the field type expects a sub-selection set, and will complete the
715
     * value by evaluating all sub-selections.
716
     *
717
     * @param array<string|int> $path
718
     * @param mixed             $result
719
     *
720
     * @return array<mixed>|mixed|Promise|null
721
     *
722
     * @throws Error
723
     * @throws Throwable
724
     */
725
    protected function completeValue(
726
        Type $returnType,
×
727
        ArrayObject $fieldNodes,
×
728
        ResolveInfo $info,
×
729
        array $path,
×
730
        &$result
×
731
    ) {
×
732
        // If result is an Error, throw a located error.
733
        if ($result instanceof Throwable) {
255✔
734
            throw $result;
21✔
735
        }
×
736

737
        // If field type is NonNull, complete for inner type, and throw field error
738
        // if result is null.
739
        if ($returnType instanceof NonNull) {
243✔
740
            $completed = $this->completeValue(
91✔
741
                $returnType->getWrappedType(),
91✔
742
                $fieldNodes,
×
743
                $info,
×
744
                $path,
×
745
                $result
×
746
            );
×
747
            if ($completed === null) {
91✔
748
                throw new InvariantViolation(
15✔
749
                    sprintf('Cannot return null for non-nullable field "%s.%s".', $info->parentType, $info->fieldName)
15✔
750
                );
×
751
            }
×
752

753
            return $completed;
85✔
754
        }
×
755
        // If result is null-like, return null.
756
        if ($result === null) {
243✔
757
            return null;
93✔
758
        }
×
759
        // If field type is List, complete each item in the list with the inner type
760
        if ($returnType instanceof ListOfType) {
224✔
761
            return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
104✔
762
        }
×
763
        // Account for invalid schema definition when typeLoader returns different
764
        // instance than `resolveType` or $field->getType() or $arg->getType()
765
        if ($returnType !== $this->exeContext->schema->getType($returnType->name)) {
222✔
766
            $hint = '';
1✔
767
            if ($this->exeContext->schema->getConfig()->typeLoader !== null) {
1✔
768
                $hint = sprintf(
1✔
769
                    'Make sure that type loader returns the same instance as defined in %s.%s',
×
770
                    $info->parentType,
1✔
771
                    $info->fieldName
1✔
772
                );
×
773
            }
×
774
            throw new InvariantViolation(
1✔
775
                sprintf(
1✔
776
                    'Schema must contain unique named types but contains multiple types named "%s". %s ' .
×
777
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
×
778
                    $returnType,
1✔
779
                    $hint
1✔
780
                )
×
781
            );
×
782
        }
×
783
        // If field type is Scalar or Enum, serialize to a valid value, returning
784
        // null if serialization is not possible.
785
        if ($returnType instanceof LeafType) {
221✔
786
            return $this->completeLeafValue($returnType, $result);
205✔
787
        }
×
788
        if ($returnType instanceof AbstractType) {
141✔
789
            return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
33✔
790
        }
×
791
        // Field type must be Object, Interface or Union and expect sub-selections.
792
        if ($returnType instanceof ObjectType) {
111✔
793
            return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
111✔
794
        }
×
795
        throw new RuntimeException(sprintf('Cannot complete value of unexpected type "%s".', $returnType));
×
796
    }
×
797

798
    /**
799
     * @param mixed $value
800
     */
801
    protected function isPromise($value) : bool
802
    {
×
803
        return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
261✔
804
    }
×
805

806
    /**
807
     * Only returns the value if it acts like a Promise, i.e. has a "then" function,
808
     * otherwise returns null.
809
     *
810
     * @param mixed $value
811
     */
812
    protected function getPromise($value) : ?Promise
813
    {
×
814
        if ($value === null || $value instanceof Promise) {
258✔
815
            return $value;
122✔
816
        }
×
817
        if ($this->exeContext->promiseAdapter->isThenable($value)) {
240✔
818
            $promise = $this->exeContext->promiseAdapter->convertThenable($value);
40✔
819
            if (! $promise instanceof Promise) {
40✔
820
                throw new InvariantViolation(sprintf(
×
821
                    '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s',
×
822
                    get_class($this->exeContext->promiseAdapter),
×
823
                    Utils::printSafe($promise)
×
824
                ));
×
825
            }
×
826

827
            return $promise;
40✔
828
        }
×
829

830
        return null;
232✔
831
    }
×
832

833
    /**
834
     * Similar to array_reduce(), however the reducing callback may return
835
     * a Promise, in which case reduction will continue after each promise resolves.
836
     *
837
     * If the callback does not return a Promise, then this function will also not
838
     * return a Promise.
839
     *
840
     * @param array<mixed>       $values
841
     * @param Promise|mixed|null $initialValue
842
     *
843
     * @return Promise|mixed|null
844
     */
845
    protected function promiseReduce(array $values, callable $callback, $initialValue)
846
    {
×
847
        return array_reduce(
6✔
848
            $values,
6✔
849
            function ($previous, $value) use ($callback) {
850
                $promise = $this->getPromise($previous);
6✔
851
                if ($promise !== null) {
6✔
852
                    return $promise->then(static function ($resolved) use ($callback, $value) {
853
                        return $callback($resolved, $value);
2✔
854
                    });
2✔
855
                }
×
856

857
                return $callback($previous, $value);
6✔
858
            },
×
859
            $initialValue
6✔
860
        );
×
861
    }
×
862

863
    /**
864
     * Complete a list value by completing each item in the list with the inner type.
865
     *
866
     * @param array<string|int>        $path
867
     * @param array<mixed>|Traversable $results
868
     *
869
     * @return array<mixed>|Promise|stdClass
870
     *
871
     * @throws Exception
872
     */
873
    protected function completeListValue(ListOfType $returnType, ArrayObject $fieldNodes, ResolveInfo $info, array $path, &$results)
874
    {
×
875
        $itemType = $returnType->getWrappedType();
104✔
876
        Utils::invariant(
104✔
877
            is_array($results) || $results instanceof Traversable,
104✔
878
            'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
104✔
879
        );
×
880
        $containsPromise = false;
104✔
881
        $i               = 0;
104✔
882
        $completedItems  = [];
104✔
883
        foreach ($results as $item) {
104✔
884
            $fieldPath     = $path;
102✔
885
            $fieldPath[]   = $i++;
102✔
886
            $info->path    = $fieldPath;
102✔
887
            $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
102✔
888
            if (! $containsPromise && $this->getPromise($completedItem) !== null) {
102✔
889
                $containsPromise = true;
14✔
890
            }
×
891
            $completedItems[] = $completedItem;
102✔
892
        }
×
893

894
        return $containsPromise
104✔
895
            ? $this->exeContext->promiseAdapter->all($completedItems)
14✔
896
            : $completedItems;
104✔
897
    }
×
898

899
    /**
900
     * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
901
     *
902
     * @param mixed $result
903
     *
904
     * @return mixed
905
     *
906
     * @throws Exception
907
     */
908
    protected function completeLeafValue(LeafType $returnType, &$result)
909
    {
×
910
        try {
×
911
            return $returnType->serialize($result);
205✔
912
        } catch (Throwable $error) {
3✔
913
            throw new InvariantViolation(
3✔
914
                'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result),
3✔
915
                0,
3✔
916
                $error
×
917
            );
×
918
        }
×
919
    }
×
920

921
    /**
922
     * Complete a value of an abstract type by determining the runtime object type
923
     * of that value, then complete the value for that type.
924
     *
925
     * @param array<string|int> $path
926
     * @param array<mixed>      $result
927
     *
928
     * @return array<mixed>|Promise|stdClass
929
     *
930
     * @throws Error
931
     */
932
    protected function completeAbstractValue(
933
        AbstractType $returnType,
×
934
        ArrayObject $fieldNodes,
×
935
        ResolveInfo $info,
×
936
        array $path,
×
937
        &$result
×
938
    ) {
×
939
        $exeContext    = $this->exeContext;
33✔
940
        $typeCandidate = $returnType->resolveType($result, $exeContext->contextValue, $info);
33✔
941

942
        if ($typeCandidate === null) {
33✔
943
            $runtimeType = static::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
11✔
944
        } elseif (is_callable($typeCandidate)) {
23✔
945
            $runtimeType = Schema::resolveType($typeCandidate);
×
946
        } else {
×
947
            $runtimeType = $typeCandidate;
23✔
948
        }
×
949
        $promise = $this->getPromise($runtimeType);
33✔
950
        if ($promise !== null) {
33✔
951
            return $promise->then(function ($resolvedRuntimeType) use (
952
                $returnType,
7✔
953
                $fieldNodes,
7✔
954
                $info,
7✔
955
                $path,
7✔
956
                &$result
7✔
957
            ) {
×
958
                return $this->completeObjectValue(
5✔
959
                    $this->ensureValidRuntimeType(
5✔
960
                        $resolvedRuntimeType,
5✔
961
                        $returnType,
×
962
                        $info,
×
963
                        $result
×
964
                    ),
×
965
                    $fieldNodes,
×
966
                    $info,
×
967
                    $path,
×
968
                    $result
×
969
                );
×
970
            });
7✔
971
        }
×
972

973
        return $this->completeObjectValue(
26✔
974
            $this->ensureValidRuntimeType(
26✔
975
                $runtimeType,
26✔
976
                $returnType,
×
977
                $info,
×
978
                $result
×
979
            ),
×
980
            $fieldNodes,
×
981
            $info,
×
982
            $path,
×
983
            $result
×
984
        );
×
985
    }
×
986

987
    /**
988
     * If a resolveType function is not given, then a default resolve behavior is
989
     * used which attempts two strategies:
990
     *
991
     * First, See if the provided value has a `__typename` field defined, if so, use
992
     * that value as name of the resolved type.
993
     *
994
     * Otherwise, test each possible type for the abstract type by calling
995
     * isTypeOf for the object being coerced, returning the first type that matches.
996
     *
997
     * @param mixed|null              $value
998
     * @param mixed|null              $contextValue
999
     * @param InterfaceType|UnionType $abstractType
1000
     *
1001
     * @return Promise|Type|string|null
1002
     */
1003
    protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
1004
    {
×
1005
        // First, look for `__typename`.
1006
        if ($value !== null &&
11✔
1007
            (is_array($value) || $value instanceof ArrayAccess) &&
11✔
1008
            isset($value['__typename']) &&
11✔
1009
            is_string($value['__typename'])
11✔
1010
        ) {
×
1011
            return $value['__typename'];
2✔
1012
        }
×
1013

1014
        if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader !== null) {
9✔
1015
            Warning::warnOnce(
1✔
1016
                sprintf(
1✔
1017
                    'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
×
1018
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
×
1019
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
×
1020
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
×
1021
                    $abstractType->name,
1✔
1022
                    Utils::printSafe($value)
1✔
1023
                ),
×
1024
                Warning::WARNING_FULL_SCHEMA_SCAN
1✔
1025
            );
×
1026
        }
×
1027
        // Otherwise, test each possible type.
1028
        $possibleTypes           = $info->schema->getPossibleTypes($abstractType);
9✔
1029
        $promisedIsTypeOfResults = [];
9✔
1030
        foreach ($possibleTypes as $index => $type) {
9✔
1031
            $isTypeOfResult = $type->isTypeOf($value, $contextValue, $info);
9✔
1032
            if ($isTypeOfResult === null) {
9✔
1033
                continue;
×
1034
            }
×
1035
            $promise = $this->getPromise($isTypeOfResult);
9✔
1036
            if ($promise !== null) {
9✔
1037
                $promisedIsTypeOfResults[$index] = $promise;
3✔
1038
            } elseif ($isTypeOfResult) {
6✔
1039
                return $type;
6✔
1040
            }
×
1041
        }
×
1042
        if (count($promisedIsTypeOfResults) > 0) {
3✔
1043
            return $this->exeContext->promiseAdapter->all($promisedIsTypeOfResults)
3✔
1044
                ->then(static function ($isTypeOfResults) use ($possibleTypes) : ?ObjectType {
1045
                    foreach ($isTypeOfResults as $index => $result) {
2✔
1046
                        if ($result) {
2✔
1047
                            return $possibleTypes[$index];
2✔
1048
                        }
×
1049
                    }
×
1050

1051
                    return null;
×
1052
                });
3✔
1053
        }
×
1054

1055
        return null;
×
1056
    }
×
1057

1058
    /**
1059
     * Complete an Object value by executing all sub-selections.
1060
     *
1061
     * @param array<string|int> $path
1062
     * @param mixed             $result
1063
     *
1064
     * @return array<mixed>|Promise|stdClass
1065
     *
1066
     * @throws Error
1067
     */
1068
    protected function completeObjectValue(
1069
        ObjectType $returnType,
×
1070
        ArrayObject $fieldNodes,
×
1071
        ResolveInfo $info,
×
1072
        array $path,
×
1073
        &$result
×
1074
    ) {
×
1075
        // If there is an isTypeOf predicate function, call it with the
1076
        // current result. If isTypeOf returns false, then raise an error rather
1077
        // than continuing execution.
1078
        $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info);
137✔
1079
        if ($isTypeOf !== null) {
137✔
1080
            $promise = $this->getPromise($isTypeOf);
11✔
1081
            if ($promise !== null) {
11✔
1082
                return $promise->then(function ($isTypeOfResult) use (
1083
                    $returnType,
2✔
1084
                    $fieldNodes,
2✔
1085
                    $path,
2✔
1086
                    &$result
2✔
1087
                ) {
×
1088
                    if (! $isTypeOfResult) {
2✔
1089
                        throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
×
1090
                    }
×
1091

1092
                    return $this->collectAndExecuteSubfields(
2✔
1093
                        $returnType,
2✔
1094
                        $fieldNodes,
×
1095
                        $path,
×
1096
                        $result
×
1097
                    );
×
1098
                });
2✔
1099
            }
×
1100
            if (! $isTypeOf) {
9✔
1101
                throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
1✔
1102
            }
×
1103
        }
×
1104

1105
        return $this->collectAndExecuteSubfields(
135✔
1106
            $returnType,
135✔
1107
            $fieldNodes,
×
1108
            $path,
×
1109
            $result
×
1110
        );
×
1111
    }
×
1112

1113
    /**
1114
     * @param array<mixed> $result
1115
     *
1116
     * @return Error
1117
     */
1118
    protected function invalidReturnTypeError(
1119
        ObjectType $returnType,
×
1120
        $result,
×
1121
        ArrayObject $fieldNodes
×
1122
    ) {
×
1123
        return new Error(
1✔
1124
            'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.',
1✔
1125
            $fieldNodes
×
1126
        );
×
1127
    }
×
1128

1129
    /**
1130
     * @param array<string|int> $path
1131
     * @param mixed             $result
1132
     *
1133
     * @return array<mixed>|Promise|stdClass
1134
     *
1135
     * @throws Error
1136
     */
1137
    protected function collectAndExecuteSubfields(
1138
        ObjectType $returnType,
×
1139
        ArrayObject $fieldNodes,
×
1140
        array $path,
×
1141
        &$result
×
1142
    ) {
×
1143
        $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
137✔
1144

1145
        return $this->executeFields($returnType, $result, $path, $subFieldNodes);
137✔
1146
    }
×
1147

1148
    /**
1149
     * A memoized collection of relevant subfields with regard to the return
1150
     * type. Memoizing ensures the subfields are not repeatedly calculated, which
1151
     * saves overhead when resolving lists of values.
1152
     */
1153
    protected function collectSubFields(ObjectType $returnType, ArrayObject $fieldNodes) : ArrayObject
1154
    {
×
1155
        if (! isset($this->subFieldCache[$returnType])) {
137✔
1156
            $this->subFieldCache[$returnType] = new SplObjectStorage();
137✔
1157
        }
×
1158
        if (! isset($this->subFieldCache[$returnType][$fieldNodes])) {
137✔
1159
            // Collect sub-fields to execute to complete this value.
1160
            $subFieldNodes        = new ArrayObject();
137✔
1161
            $visitedFragmentNames = new ArrayObject();
137✔
1162
            foreach ($fieldNodes as $fieldNode) {
137✔
1163
                if (! isset($fieldNode->selectionSet)) {
137✔
1164
                    continue;
×
1165
                }
×
1166
                $subFieldNodes = $this->collectFields(
137✔
1167
                    $returnType,
137✔
1168
                    $fieldNode->selectionSet,
137✔
1169
                    $subFieldNodes,
×
1170
                    $visitedFragmentNames
×
1171
                );
×
1172
            }
×
1173
            $this->subFieldCache[$returnType][$fieldNodes] = $subFieldNodes;
137✔
1174
        }
×
1175

1176
        return $this->subFieldCache[$returnType][$fieldNodes];
137✔
1177
    }
×
1178

1179
    /**
1180
     * Implements the "Evaluating selection sets" section of the spec
1181
     * for "read" mode.
1182
     *
1183
     * @param mixed             $rootValue
1184
     * @param array<string|int> $path
1185
     *
1186
     * @return Promise|stdClass|array<mixed>
1187
     */
1188
    protected function executeFields(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
1189
    {
×
1190
        $containsPromise = false;
257✔
1191
        $results         = [];
257✔
1192
        foreach ($fields as $responseName => $fieldNodes) {
257✔
1193
            $fieldPath   = $path;
257✔
1194
            $fieldPath[] = $responseName;
257✔
1195
            $result      = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
257✔
1196
            if ($result === static::$UNDEFINED) {
255✔
1197
                continue;
6✔
1198
            }
×
1199
            if (! $containsPromise && $this->isPromise($result)) {
252✔
1200
                $containsPromise = true;
39✔
1201
            }
×
1202
            $results[$responseName] = $result;
252✔
1203
        }
×
1204
        // If there are no promises, we can just return the object
1205
        if (! $containsPromise) {
255✔
1206
            return static::fixResultsIfEmptyArray($results);
226✔
1207
        }
×
1208

1209
        // Otherwise, results is a map from field name to the result of resolving that
1210
        // field, which is possibly a promise. Return a promise that will return this
1211
        // same map, but with any promises replaced with the values they resolved to.
1212
        return $this->promiseForAssocArray($results);
39✔
1213
    }
×
1214

1215
    /**
1216
     * Differentiate empty objects from empty lists.
1217
     *
1218
     * @see https://github.com/webonyx/graphql-php/issues/59
1219
     *
1220
     * @param array<mixed>|mixed $results
1221
     *
1222
     * @return array<mixed>|stdClass|mixed
1223
     */
1224
    protected static function fixResultsIfEmptyArray($results)
1225
    {
×
1226
        if ($results === []) {
257✔
1227
            return new stdClass();
5✔
1228
        }
×
1229

1230
        return $results;
253✔
1231
    }
×
1232

1233
    /**
1234
     * Transform an associative array with Promises to a Promise which resolves to an
1235
     * associative array where all Promises were resolved.
1236
     *
1237
     * @param array<string, Promise|mixed> $assoc
1238
     */
1239
    protected function promiseForAssocArray(array $assoc) : Promise
1240
    {
×
1241
        $keys              = array_keys($assoc);
39✔
1242
        $valuesAndPromises = array_values($assoc);
39✔
1243
        $promise           = $this->exeContext->promiseAdapter->all($valuesAndPromises);
39✔
1244

1245
        return $promise->then(static function ($values) use ($keys) {
1246
            $resolvedResults = [];
37✔
1247
            foreach ($values as $i => $value) {
37✔
1248
                $resolvedResults[$keys[$i]] = $value;
37✔
1249
            }
×
1250

1251
            return static::fixResultsIfEmptyArray($resolvedResults);
37✔
1252
        });
39✔
1253
    }
×
1254

1255
    /**
1256
     * @param string|ObjectType|null  $runtimeTypeOrName
1257
     * @param InterfaceType|UnionType $returnType
1258
     * @param mixed                   $result
1259
     */
1260
    protected function ensureValidRuntimeType(
1261
        $runtimeTypeOrName,
×
1262
        AbstractType $returnType,
×
1263
        ResolveInfo $info,
×
1264
        &$result
×
1265
    ) : ObjectType {
×
1266
        $runtimeType = is_string($runtimeTypeOrName)
31✔
1267
            ? $this->exeContext->schema->getType($runtimeTypeOrName)
4✔
1268
            : $runtimeTypeOrName;
31✔
1269
        if (! $runtimeType instanceof ObjectType) {
31✔
1270
            throw new InvariantViolation(
1✔
1271
                sprintf(
1✔
1272
                    'Abstract type %s must resolve to an Object type at ' .
×
1273
                    'runtime for field %s.%s with value "%s", received "%s". ' .
×
1274
                    'Either the %s type should provide a "resolveType" ' .
×
1275
                    'function or each possible type should provide an "isTypeOf" function.',
×
1276
                    $returnType,
1✔
1277
                    $info->parentType,
1✔
1278
                    $info->fieldName,
1✔
1279
                    Utils::printSafe($result),
1✔
1280
                    Utils::printSafe($runtimeType),
1✔
1281
                    $returnType
1✔
1282
                )
×
1283
            );
×
1284
        }
×
1285
        if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) {
30✔
1286
            throw new InvariantViolation(
4✔
1287
                sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
4✔
1288
            );
×
1289
        }
×
1290
        if ($runtimeType !== $this->exeContext->schema->getType($runtimeType->name)) {
30✔
1291
            throw new InvariantViolation(
1✔
1292
                sprintf(
1✔
1293
                    'Schema must contain unique named types but contains multiple types named "%s". ' .
×
1294
                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
×
1295
                    'type instance as referenced anywhere else within the schema ' .
×
1296
                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
×
1297
                    $runtimeType,
1✔
1298
                    $returnType
1✔
1299
                )
×
1300
            );
×
1301
        }
×
1302

1303
        return $runtimeType;
29✔
1304
    }
×
1305
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc