Passed
Push — master ( 178671...98caf9 )
by Christoffer
04:16 queued 01:39
created

ExtensionContext::extendFieldType()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 1
1
<?php
2
3
namespace Digia\GraphQL\SchemaExtension;
4
5
use Digia\GraphQL\Error\ExtensionException;
6
use Digia\GraphQL\Error\InvalidTypeException;
7
use Digia\GraphQL\Error\InvariantException;
8
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
9
use Digia\GraphQL\Language\Node\DocumentNode;
10
use Digia\GraphQL\Language\Node\EnumTypeExtensionNode;
11
use Digia\GraphQL\Language\Node\InputObjectTypeExtensionNode;
12
use Digia\GraphQL\Language\Node\InterfaceTypeExtensionNode;
13
use Digia\GraphQL\Language\Node\NamedTypeNode;
14
use Digia\GraphQL\Language\Node\NodeInterface;
15
use Digia\GraphQL\Language\Node\ObjectTypeExtensionNode;
16
use Digia\GraphQL\Language\Node\ScalarTypeExtensionNode;
17
use Digia\GraphQL\Language\Node\TypeDefinitionNodeInterface;
18
use Digia\GraphQL\Language\Node\UnionTypeExtensionNode;
19
use Digia\GraphQL\SchemaBuilder\DefinitionBuilderCreatorInterface;
20
use Digia\GraphQL\SchemaBuilder\DefinitionBuilderInterface;
21
use Digia\GraphQL\Type\Definition\Argument;
22
use Digia\GraphQL\Type\Definition\Directive;
23
use Digia\GraphQL\Type\Definition\InterfaceType;
24
use Digia\GraphQL\Type\Definition\ListType;
25
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
26
use Digia\GraphQL\Type\Definition\NonNullType;
27
use Digia\GraphQL\Type\Definition\ObjectType;
28
use Digia\GraphQL\Type\Definition\TypeInterface;
29
use Digia\GraphQL\Type\Definition\UnionType;
30
use Digia\GraphQL\Type\SchemaInterface;
31
use Psr\SimpleCache\InvalidArgumentException;
32
use function Digia\GraphQL\Type\GraphQLInterfaceType;
33
use function Digia\GraphQL\Type\GraphQLList;
34
use function Digia\GraphQL\Type\GraphQLNonNull;
35
use function Digia\GraphQL\Type\GraphQLObjectType;
36
use function Digia\GraphQL\Type\GraphQLUnionType;
37
use function Digia\GraphQL\Type\isIntrospectionType;
38
use function Digia\GraphQL\Util\invariant;
39
use function Digia\GraphQL\Util\keyMap;
40
use function Digia\GraphQL\Util\toString;
41
42
class ExtensionContext implements ExtensionContextInterface
43
{
44
    /**
45
     * @var SchemaInterface
46
     */
47
    protected $schema;
48
49
    /**
50
     * @var DocumentNode
51
     */
52
    protected $document;
53
54
    /**
55
     * @var DefinitionBuilderCreatorInterface
56
     */
57
    protected $definitionBuilderCreator;
58
59
    /**
60
     * @var DefinitionBuilderInterface
61
     */
62
    protected $definitionBuilder;
63
64
    /**
65
     * @var TypeDefinitionNodeInterface[]
66
     */
67
    protected $typeDefinitionMap;
68
69
    /**
70
     * @var ObjectTypeExtensionNode[][]|InterfaceTypeExtensionNode[][]
71
     */
72
    protected $typeExtensionsMap;
73
74
    /**
75
     * @var DirectiveDefinitionNode[]
76
     */
77
    protected $directiveDefinitions;
78
79
    /**
80
     * @var NamedTypeInterface[]
81
     */
82
    protected $extendTypeCache;
83
84
    /**
85
     * ExtensionContext constructor.
86
     * @param SchemaInterface                   $schema
87
     * @param DocumentNode                      $document
88
     * @param DefinitionBuilderCreatorInterface $definitionBuilderCreator
89
     * @throws ExtensionException
90
     */
91
    public function __construct(
92
        SchemaInterface $schema,
93
        DocumentNode $document,
94
        DefinitionBuilderCreatorInterface $definitionBuilderCreator
95
    ) {
96
        $this->schema                   = $schema;
97
        $this->document                 = $document;
98
        $this->definitionBuilderCreator = $definitionBuilderCreator;
99
        $this->typeExtensionsMap        = [];
100
        $this->typeDefinitionMap        = [];
101
        $this->directiveDefinitions     = [];
102
        $this->extendTypeCache          = [];
103
104
        $typeDefinitionMap = $this->getExtendedDefinitions();
105
106
        $this->definitionBuilder = $this->createDefinitionBuilder($typeDefinitionMap);
107
108
    }
109
110
    /**
111
     * @throws ExtensionException
112
     */
113
    public function getExtendedDefinitions(): array
114
    {
115
        foreach ($this->document->getDefinitions() as $definition) {
116
            if ($definition instanceof TypeDefinitionNodeInterface) {
117
                // Sanity check that none of the defined types conflict with the schema's existing types.
118
                $typeName     = $definition->getNameValue();
119
                $existingType = $this->schema->getType($typeName);
120
121
                if (null !== $existingType) {
122
                    throw new ExtensionException(
123
                        \sprintf(
124
                            'Type "%s" already exists in the schema. It cannot also ' .
125
                            'be defined in this type definition.',
126
                            $typeName
127
                        ),
128
                        [$definition]
129
                    );
130
                }
131
132
                $this->typeDefinitionMap[$typeName] = $definition;
133
134
                continue;
135
            }
136
137
            if ($definition instanceof ObjectTypeExtensionNode || $definition instanceof InterfaceTypeExtensionNode) {
138
                // Sanity check that this type extension exists within the schema's existing types.
139
                $extendedTypeName = $definition->getNameValue();
140
                $existingType     = $this->schema->getType($extendedTypeName);
141
142
                if (null === $existingType) {
143
                    throw new ExtensionException(
144
                        \sprintf(
145
                            'Cannot extend type "%s" because it does not exist in the existing schema.',
146
                            $extendedTypeName
147
                        ),
148
                        [$definition]
149
                    );
150
                }
151
152
                $this->checkExtensionNode($existingType, $definition);
153
154
                $existingTypeExtensions = $this->typeExtensionsMap[$extendedTypeName] ?? [];
155
156
                $this->typeExtensionsMap[$extendedTypeName] = \array_merge($existingTypeExtensions, [$definition]);
157
158
                continue;
159
            }
160
161
            if ($definition instanceof DirectiveDefinitionNode) {
162
                $directiveName     = $definition->getNameValue();
163
                $existingDirective = $this->schema->getDirective($directiveName);
164
165
                if (null !== $existingDirective) {
166
                    throw new ExtensionException(
167
                        \sprintf(
168
                            'Directive "%s" already exists in the schema. It cannot be redefined.',
169
                            $directiveName
170
                        ),
171
                        [$definition]
172
                    );
173
                }
174
175
                $this->directiveDefinitions[] = $definition;
176
177
                continue;
178
            }
179
180
            if ($definition instanceof ScalarTypeExtensionNode ||
181
                $definition instanceof UnionTypeExtensionNode ||
182
                $definition instanceof EnumTypeExtensionNode ||
183
                $definition instanceof InputObjectTypeExtensionNode) {
184
                throw new ExtensionException(
185
                    \sprintf('The %s kind is not yet supported by extendSchema().', $definition->getKind())
186
                );
187
            }
188
        }
189
190
        return $this->typeDefinitionMap;
191
    }
192
193
    /**
194
     * @return bool
195
     */
196
    public function isSchemaExtended(): bool
197
    {
198
        return
199
            !empty(\array_keys($this->typeExtensionsMap)) ||
200
            !empty(\array_keys($this->typeDefinitionMap)) ||
201
            !empty($this->directiveDefinitions);
202
    }
203
204
    /**
205
     * @return TypeInterface|null
206
     * @throws InvalidArgumentException
207
     * @throws InvariantException
208
     */
209
    public function getExtendedQueryType(): ?TypeInterface
210
    {
211
        $existingQueryType = $this->schema->getQueryType();
212
213
        return null !== $existingQueryType
214
            ? $this->getExtendedType($existingQueryType)
215
            : null;
216
    }
217
218
    /**
219
     * @return TypeInterface|null
220
     * @throws InvalidArgumentException
221
     * @throws InvariantException
222
     */
223
    public function getExtendedMutationType(): ?TypeInterface
224
    {
225
        $existingMutationType = $this->schema->getMutationType();
226
227
        return null !== $existingMutationType
228
            ? $this->getExtendedType($existingMutationType)
229
            : null;
230
    }
231
232
    /**
233
     * @return TypeInterface|null
234
     * @throws InvalidArgumentException
235
     * @throws InvariantException
236
     */
237
    public function getExtendedSubscriptionType(): ?TypeInterface
238
    {
239
        $existingSubscriptionType = $this->schema->getSubscriptionType();
240
241
        return null !== $existingSubscriptionType
242
            ? $this->getExtendedType($existingSubscriptionType)
243
            : null;
244
    }
245
246
    /**
247
     * @return TypeInterface[]
248
     */
249
    public function getExtendedTypes(): array
250
    {
251
        return \array_merge(
252
            \array_map(function ($type) {
253
                return $this->getExtendedType($type);
254
            }, \array_values($this->schema->getTypeMap())),
255
            $this->definitionBuilder->buildTypes(\array_values($this->typeDefinitionMap))
256
        );
257
    }
258
259
    /**
260
     * @return Directive[]
261
     * @throws InvariantException
262
     */
263
    public function getExtendedDirectives(): array
264
    {
265
        $existingDirectives = $this->schema->getDirectives();
266
267
        invariant(!empty($existingDirectives), 'schema must have default directives');
268
269
        return \array_merge(
270
            $existingDirectives,
271
            \array_map(function (DirectiveDefinitionNode $node) {
272
                return $this->definitionBuilder->buildDirective($node);
273
            }, $this->directiveDefinitions)
274
        );
275
    }
276
277
    /**
278
     * @param NamedTypeNode $node
279
     * @return TypeInterface|null
280
     * @throws ExtensionException
281
     * @throws InvalidArgumentException
282
     * @throws InvariantException
283
     */
284
    public function resolveType(NamedTypeNode $node): ?TypeInterface
285
    {
286
        $typeName     = $node->getNameValue();
287
        $existingType = $this->schema->getType($typeName);
288
289
        if (null !== $existingType) {
290
            return $this->getExtendedType($existingType);
291
        }
292
293
        throw new ExtensionException(
294
            \sprintf(
295
                'Unknown type: "%s". Ensure that this type exists ' .
296
                'either in the original schema, or is added in a type definition.',
297
                $typeName
298
            ),
299
            [$node]
300
        );
301
    }
302
303
    /**
304
     * @param array $typeDefinitionMap
305
     * @return DefinitionBuilderInterface
306
     */
307
    protected function createDefinitionBuilder(array $typeDefinitionMap): DefinitionBuilderInterface
308
    {
309
        return $this->definitionBuilderCreator->create($typeDefinitionMap, [$this, 'resolveType']);
310
    }
311
312
    /**
313
     * @param TypeInterface $type
314
     * @param NodeInterface $node
315
     * @throws ExtensionException
316
     */
317
    protected function checkExtensionNode(TypeInterface $type, NodeInterface $node): void
318
    {
319
        if ($node instanceof ObjectTypeExtensionNode && !($type instanceof ObjectType)) {
320
            throw new ExtensionException(
321
                \sprintf('Cannot extend non-object type "%s".', toString($type)),
322
                [$node]
323
            );
324
        }
325
326
        if ($node instanceof InterfaceTypeExtensionNode && !($type instanceof InterfaceType)) {
327
            throw new ExtensionException(
328
                \sprintf('Cannot extend non-interface type "%s".', toString($type)),
329
                [$node]
330
            );
331
        }
332
    }
333
334
    /**
335
     * @param TypeInterface $type
336
     * @return TypeInterface
337
     * @throws InvalidArgumentException
338
     * @throws InvariantException
339
     */
340
    protected function getExtendedType(TypeInterface $type): TypeInterface
341
    {
342
        $typeName = $type->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

342
        /** @scrutinizer ignore-call */ 
343
        $typeName = $type->getName();
Loading history...
343
344
        if (!isset($this->extendTypeCache[$typeName])) {
345
            $this->extendTypeCache[$typeName] = $this->extendType($type);
346
        }
347
348
        return $this->extendTypeCache[$typeName];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->extendTypeCache[$typeName] returns the type Digia\GraphQL\Type\Definition\NamedTypeInterface which is incompatible with the type-hinted return Digia\GraphQL\Type\Definition\TypeInterface.
Loading history...
349
    }
350
351
    /**
352
     * @param TypeInterface $type
353
     * @return TypeInterface
354
     * @throws InvariantException
355
     */
356
    protected function extendType(TypeInterface $type): TypeInterface
357
    {
358
        /** @noinspection PhpParamsInspection */
359
        if (isIntrospectionType($type)) {
360
            // Introspection types are not extended.
361
            return $type;
362
        }
363
364
        if ($type instanceof ObjectType) {
365
            return $this->extendObjectType($type);
366
        }
367
368
        if ($type instanceof InterfaceType) {
369
            return $this->extendInterfaceType($type);
370
        }
371
372
        if ($type instanceof UnionType) {
373
            return $this->extendUnionType($type);
374
        }
375
376
        // This type is not yet extendable.
377
        return $type;
378
    }
379
380
    /**
381
     * @param ObjectType $type
382
     * @return ObjectType
383
     */
384
    protected function extendObjectType(ObjectType $type): ObjectType
385
    {
386
        $typeName          = $type->getName();
387
        $extensionASTNodes = $type->getExtensionAstNodes();
388
389
        if (isset($this->typeExtensionsMap[$typeName])) {
390
            $extensionASTNodes = !empty($extensionASTNodes)
391
                ? \array_merge($this->typeExtensionsMap[$typeName], $extensionASTNodes)
392
                : $this->typeExtensionsMap[$typeName];
393
        }
394
395
        return GraphQLObjectType([
396
            'name'              => $typeName,
397
            'description'       => $type->getDescription(),
398
            'interfaces'        => function () use ($type) {
399
                return $this->extendImplementedInterfaces($type);
400
            },
401
            'fields'            => function () use ($type) {
402
                return $this->extendFieldMap($type);
403
            },
404
            'astNode'           => $type->getAstNode(),
405
            'extensionASTNodes' => $extensionASTNodes,
406
            'isTypeOf'          => $type->getIsTypeOf(),
407
        ]);
408
    }
409
410
    /**
411
     * @param InterfaceType $type
412
     * @return InterfaceType
413
     */
414
    protected function extendInterfaceType(InterfaceType $type): InterfaceType
415
    {
416
        $typeName          = $type->getName();
417
        $extensionASTNodes = $this->typeExtensionsMap[$typeName] ?? [];
418
419
        if (isset($this->typeExtensionsMap[$typeName])) {
420
            $extensionASTNodes = !empty($extensionASTNodes)
421
                ? \array_merge($this->typeExtensionsMap[$typeName], $extensionASTNodes)
422
                : $this->typeExtensionsMap[$typeName];
423
        }
424
425
        return GraphQLInterfaceType([
426
            'name'              => $typeName,
427
            'description'       => $type->getDescription(),
428
            'fields'            => function () use ($type) {
429
                return $this->extendFieldMap($type);
430
            },
431
            'astNode'           => $type->getAstNode(),
432
            'extensionASTNodes' => $extensionASTNodes,
433
            'resolveType'       => $type->getResolveType(),
434
        ]);
435
    }
436
437
    /**
438
     * @param UnionType $type
439
     * @return UnionType
440
     * @throws InvariantException
441
     */
442
    protected function extendUnionType(UnionType $type): UnionType
443
    {
444
        return GraphQLUnionType([
445
            'name'        => $type->getName(),
446
            'description' => $type->getDescription(),
447
            'types'       => \array_map(function ($unionType) {
448
                return $this->getExtendedType($unionType);
449
            }, $type->getTypes()),
450
            'astNode'     => $type->getAstNode(),
451
            'resolveType' => $type->getResolveType(),
452
        ]);
453
    }
454
455
    /**
456
     * @param ObjectType $type
457
     * @return array
458
     * @throws InvariantException
459
     */
460
    protected function extendImplementedInterfaces(ObjectType $type): array
461
    {
462
        $interfaces = \array_map(function (InterfaceType $interface) {
463
            return $this->getExtendedType($interface);
464
        }, $type->getInterfaces());
465
466
        // If there are any extensions to the interfaces, apply those here.
467
        $extensions = $this->typeExtensionsMap[$type->getName()] ?? null;
468
469
        if (null !== $extensions) {
470
            foreach ($extensions as $extension) {
471
                foreach ($extension->getInterfaces() as $namedType) {
472
                    // Note: While this could make early assertions to get the correctly
473
                    // typed values, that would throw immediately while type system
474
                    // validation with validateSchema() will produce more actionable results.
475
                    $interfaces[] = $this->definitionBuilder->buildType($namedType);
476
                }
477
            }
478
        }
479
480
        return $interfaces;
481
    }
482
483
    /**
484
     * @param TypeInterface|ObjectType|InterfaceType $type
485
     * @return array
486
     * @throws InvalidTypeException
487
     * @throws InvariantException
488
     * @throws ExtensionException
489
     * @throws InvalidArgumentException
490
     */
491
    protected function extendFieldMap(TypeInterface $type): array
492
    {
493
        $typeName    = $type->getName();
494
        $newFieldMap = [];
495
        $oldFieldMap = $type->getFields();
0 ignored issues
show
Bug introduced by
The method getFields() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of Digia\GraphQL\Type\Definition\TypeInterface such as Digia\GraphQL\Type\Definition\InputObjectType or Digia\GraphQL\Type\Definition\ObjectType or Digia\GraphQL\Type\Definition\InterfaceType. ( Ignorable by Annotation )

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

495
        /** @scrutinizer ignore-call */ 
496
        $oldFieldMap = $type->getFields();
Loading history...
496
497
        foreach (\array_keys($oldFieldMap) as $fieldName) {
498
            $field = $oldFieldMap[$fieldName];
499
500
            $newFieldMap[$fieldName] = [
501
                'description'       => $field->getDescription(),
502
                'deprecationReason' => $field->getDeprecationReason(),
503
                'type'              => $this->extendFieldType($field->getType()),
504
                'args'              => keyMap($field->getArguments(), function (Argument $argument) {
505
                    return $argument->getName();
506
                }),
507
                'astNode'           => $field->getAstNode(),
508
                'resolve'           => $field->getResolve(),
509
            ];
510
        }
511
512
        // If there are any extensions to the fields, apply those here.
513
        /** @var ObjectTypeExtensionNode|InterfaceTypeExtensionNode[] $extensions */
514
        $extensions = $this->typeExtensionsMap[$typeName] ?? null;
515
516
        if (null !== $extensions) {
0 ignored issues
show
introduced by
The condition null !== $extensions is always true.
Loading history...
517
            foreach ($extensions as $extension) {
518
                foreach ($extension->getFields() as $field) {
519
                    $fieldName = $field->getNameValue();
520
521
                    if (isset($oldFieldMap[$fieldName])) {
522
                        throw new ExtensionException(
523
                            \sprintf(
524
                                'Field "%s.%s" already exists in the schema. ' .
525
                                'It cannot also be defined in this type extension.',
526
                                $typeName, $fieldName
527
                            ),
528
                            [$field]
529
                        );
530
                    }
531
532
                    $newFieldMap[$fieldName] = $this->definitionBuilder->buildField($field);
533
                }
534
            }
535
        }
536
537
        return $newFieldMap;
538
    }
539
540
    /**
541
     * @param TypeInterface $typeDefinition
542
     * @return TypeInterface
543
     * @throws InvalidArgumentException
544
     * @throws InvalidTypeException
545
     * @throws InvariantException
546
     */
547
    protected function extendFieldType(TypeInterface $typeDefinition): TypeInterface
548
    {
549
        if ($typeDefinition instanceof ListType) {
550
            return GraphQLList($this->extendFieldType($typeDefinition->getOfType()));
551
        }
552
553
        if ($typeDefinition instanceof NonNullType) {
554
            return GraphQLNonNull($this->extendFieldType($typeDefinition->getOfType()));
555
        }
556
557
        return $this->getExtendedType($typeDefinition);
558
    }
559
}
560