Passed
Pull Request — master (#164)
by Christoffer
02:18
created

ExtensionContext::isSchemaExtended()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 4
nc 3
nop 0
1
<?php
2
3
namespace Digia\GraphQL\Schema\Extension;
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\Schema\DefinitionBuilderCreatorInterface;
20
use Digia\GraphQL\Schema\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\Schema\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->extendTypeCache          = [];
100
    }
101
102
    /**
103
     * @throws ExtensionException
104
     */
105
    public function boot(): void
106
    {
107
        $this->extendDefinitions();
108
109
        $this->definitionBuilder = $this->createDefinitionBuilder();
110
    }
111
112
    /**
113
     * @return bool
114
     */
115
    public function isSchemaExtended(): bool
116
    {
117
        return
118
            !empty(\array_keys($this->typeExtensionsMap)) ||
119
            !empty(\array_keys($this->typeDefinitionMap)) ||
120
            !empty($this->directiveDefinitions);
121
    }
122
123
    /**
124
     * @return TypeInterface|null
125
     * @throws InvalidArgumentException
126
     * @throws InvariantException
127
     */
128
    public function getExtendedQueryType(): ?TypeInterface
129
    {
130
        $existingQueryType = $this->schema->getQueryType();
131
132
        return null !== $existingQueryType
133
            ? $this->getExtendedType($existingQueryType)
134
            : null;
135
    }
136
137
    /**
138
     * @return TypeInterface|null
139
     * @throws InvalidArgumentException
140
     * @throws InvariantException
141
     */
142
    public function getExtendedMutationType(): ?TypeInterface
143
    {
144
        $existingMutationType = $this->schema->getMutationType();
145
146
        return null !== $existingMutationType
147
            ? $this->getExtendedType($existingMutationType)
148
            : null;
149
    }
150
151
    /**
152
     * @return TypeInterface|null
153
     * @throws InvalidArgumentException
154
     * @throws InvariantException
155
     */
156
    public function getExtendedSubscriptionType(): ?TypeInterface
157
    {
158
        $existingSubscriptionType = $this->schema->getSubscriptionType();
159
160
        return null !== $existingSubscriptionType
161
            ? $this->getExtendedType($existingSubscriptionType)
162
            : null;
163
    }
164
165
    /**
166
     * @return TypeInterface[]
167
     */
168
    public function getExtendedTypes(): array
169
    {
170
        return \array_merge(
171
            \array_map(function ($type) {
172
                return $this->getExtendedType($type);
173
            }, \array_values($this->schema->getTypeMap())),
174
            $this->definitionBuilder->buildTypes(\array_values($this->typeDefinitionMap))
175
        );
176
    }
177
178
    /**
179
     * @return Directive[]
180
     * @throws InvariantException
181
     */
182
    public function getExtendedDirectives(): array
183
    {
184
        $existingDirectives = $this->schema->getDirectives();
185
186
        invariant(!empty($existingDirectives), 'schema must have default directives');
187
188
        return \array_merge(
189
            $existingDirectives,
190
            \array_map(function (DirectiveDefinitionNode $node) {
191
                return $this->definitionBuilder->buildDirective($node);
192
            }, $this->directiveDefinitions)
193
        );
194
    }
195
196
    /**
197
     * @param NamedTypeNode $node
198
     * @return TypeInterface|null
199
     * @throws ExtensionException
200
     * @throws InvalidArgumentException
201
     * @throws InvariantException
202
     */
203
    public function resolveType(NamedTypeNode $node): ?TypeInterface
204
    {
205
        $typeName     = $node->getNameValue();
206
        $existingType = $this->schema->getType($typeName);
207
208
        if (null !== $existingType) {
209
            return $this->getExtendedType($existingType);
210
        }
211
212
        throw new ExtensionException(
213
            \sprintf(
214
                'Unknown type: "%s". Ensure that this type exists ' .
215
                'either in the original schema, or is added in a type definition.',
216
                $typeName
217
            ),
218
            [$node]
219
        );
220
    }
221
222
    /**
223
     * @throws ExtensionException
224
     */
225
    protected function extendDefinitions(): void
226
    {
227
        $typeDefinitionMap    = [];
228
        $typeExtensionsMap    = [];
229
        $directiveDefinitions = [];
230
231
        foreach ($this->document->getDefinitions() as $definition) {
232
            if ($definition instanceof TypeDefinitionNodeInterface) {
233
                // Sanity check that none of the defined types conflict with the schema's existing types.
234
                $typeName     = $definition->getNameValue();
235
                $existingType = $this->schema->getType($typeName);
236
237
                if (null !== $existingType) {
238
                    throw new ExtensionException(
239
                        \sprintf(
240
                            'Type "%s" already exists in the schema. It cannot also ' .
241
                            'be defined in this type definition.',
242
                            $typeName
243
                        ),
244
                        [$definition]
245
                    );
246
                }
247
248
                $typeDefinitionMap[$typeName] = $definition;
249
250
                continue;
251
            }
252
253
            if ($definition instanceof ObjectTypeExtensionNode || $definition instanceof InterfaceTypeExtensionNode) {
254
                // Sanity check that this type extension exists within the schema's existing types.
255
                $extendedTypeName = $definition->getNameValue();
256
                $existingType     = $this->schema->getType($extendedTypeName);
257
258
                if (null === $existingType) {
259
                    throw new ExtensionException(
260
                        \sprintf(
261
                            'Cannot extend type "%s" because it does not exist in the existing schema.',
262
                            $extendedTypeName
263
                        ),
264
                        [$definition]
265
                    );
266
                }
267
268
                $this->checkExtensionNode($existingType, $definition);
269
270
                $typeExtensionsMap[$extendedTypeName] = \array_merge(
271
                    $typeExtensionsMap[$extendedTypeName] ?? [],
272
                    [$definition]
273
                );
274
275
                continue;
276
            }
277
278
            if ($definition instanceof DirectiveDefinitionNode) {
279
                $directiveName     = $definition->getNameValue();
280
                $existingDirective = $this->schema->getDirective($directiveName);
281
282
                if (null !== $existingDirective) {
283
                    throw new ExtensionException(
284
                        \sprintf(
285
                            'Directive "%s" already exists in the schema. It cannot be redefined.',
286
                            $directiveName
287
                        ),
288
                        [$definition]
289
                    );
290
                }
291
292
                $directiveDefinitions[] = $definition;
293
294
                continue;
295
            }
296
297
            if ($definition instanceof ScalarTypeExtensionNode ||
298
                $definition instanceof UnionTypeExtensionNode ||
299
                $definition instanceof EnumTypeExtensionNode ||
300
                $definition instanceof InputObjectTypeExtensionNode) {
301
                throw new ExtensionException(
302
                    \sprintf('The %s kind is not yet supported by extendSchema().', $definition->getKind())
303
                );
304
            }
305
        }
306
307
        $this->typeDefinitionMap    = $typeDefinitionMap;
308
        $this->typeExtensionsMap    = $typeExtensionsMap;
309
        $this->directiveDefinitions = $directiveDefinitions;
310
    }
311
312
    /**
313
     * @return DefinitionBuilderInterface
314
     */
315
    protected function createDefinitionBuilder(): DefinitionBuilderInterface
316
    {
317
        return $this->definitionBuilderCreator->create($this->typeDefinitionMap, [$this, 'resolveType']);
318
    }
319
320
    /**
321
     * @param TypeInterface $type
322
     * @param NodeInterface $node
323
     * @throws ExtensionException
324
     */
325
    protected function checkExtensionNode(TypeInterface $type, NodeInterface $node): void
326
    {
327
        if ($node instanceof ObjectTypeExtensionNode && !($type instanceof ObjectType)) {
328
            throw new ExtensionException(
329
                \sprintf('Cannot extend non-object type "%s".', toString($type)),
330
                [$node]
331
            );
332
        }
333
334
        if ($node instanceof InterfaceTypeExtensionNode && !($type instanceof InterfaceType)) {
335
            throw new ExtensionException(
336
                \sprintf('Cannot extend non-interface type "%s".', toString($type)),
337
                [$node]
338
            );
339
        }
340
    }
341
342
    /**
343
     * @param TypeInterface $type
344
     * @return TypeInterface
345
     * @throws InvalidArgumentException
346
     * @throws InvariantException
347
     */
348
    protected function getExtendedType(TypeInterface $type): TypeInterface
349
    {
350
        $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

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

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