Passed
Pull Request — master (#417)
by
unknown
05:25
created

SchemaExtender::extendInterfaceType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 11
ccs 9
cts 9
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\AST\DirectiveDefinitionNode;
9
use GraphQL\Language\AST\DocumentNode;
10
use GraphQL\Language\AST\Node;
11
use GraphQL\Language\AST\NodeKind;
12
use GraphQL\Language\AST\ObjectTypeExtensionNode;
13
use GraphQL\Language\AST\SchemaDefinitionNode;
14
use GraphQL\Language\AST\SchemaTypeExtensionNode;
15
use GraphQL\Language\AST\TypeDefinitionNode;
16
use GraphQL\Language\AST\TypeExtensionNode;
17
use GraphQL\Type\Definition\CustomScalarType;
18
use GraphQL\Type\Definition\Directive;
19
use GraphQL\Type\Definition\EnumType;
20
use GraphQL\Type\Definition\EnumValueDefinition;
21
use GraphQL\Type\Definition\FieldArgument;
22
use GraphQL\Type\Definition\InputObjectType;
23
use GraphQL\Type\Definition\InterfaceType;
24
use GraphQL\Type\Definition\ListOfType;
25
use GraphQL\Type\Definition\NamedType;
26
use GraphQL\Type\Definition\NonNull;
27
use GraphQL\Type\Definition\ObjectType;
28
use GraphQL\Type\Definition\Type;
29
use GraphQL\Type\Definition\UnionType;
30
use GraphQL\Type\Introspection;
31
use GraphQL\Type\Schema;
32
use GraphQL\Validator\DocumentValidator;
33
use function array_keys;
34
use function array_map;
35
use function array_merge;
36
use function array_values;
37
use function count;
38
39
class SchemaExtender
40
{
41
    const SCHEMA_EXTENSION = 'SchemaExtension';
42
43
    /** @var Type[] */
44
    protected static $extendTypeCache;
45
46
    /** @var mixed[] */
47
    protected static $typeExtensionsMap;
48
49
    /** @var ASTDefinitionBuilder */
50
    protected static $astBuilder;
51
52
    /**
53
     * @return TypeExtensionNode[]|null
54
     */
55 49
    protected static function getExtensionASTNodes(NamedType $type) : ?array
56
    {
57 49
        if (! $type instanceof Type) {
58
            return null;
59
        }
60
61 49
        $name = $type->name;
62 49
        if ($type->extensionASTNodes !== null) {
63 49
            if (isset(static::$typeExtensionsMap[$name])) {
64 19
                return array_merge($type->extensionASTNodes, static::$typeExtensionsMap[$name]);
65
            }
66
67 47
            return $type->extensionASTNodes;
68
        }
69 43
        return static::$typeExtensionsMap[$name] ?? null;
70
    }
71
72
    /**
73
     * @throws Error
74
     */
75 29
    protected static function checkExtensionNode(Type $type, Node $node) : void
76
    {
77 29
        switch ($node->kind) {
78 29
            case NodeKind::OBJECT_TYPE_EXTENSION:
79 20
                if (! ($type instanceof ObjectType)) {
80 1
                    throw new Error(
81 1
                        'Cannot extend non-object type "' . $type->name . '".',
82 1
                        [$node]
83
                    );
84
                }
85 19
                break;
86 15
            case NodeKind::INTERFACE_TYPE_EXTENSION:
87 7
                if (! ($type instanceof InterfaceType)) {
88 1
                    throw new Error(
89 1
                        'Cannot extend non-interface type "' . $type->name . '".',
90 1
                        [$node]
91
                    );
92
                }
93 6
                break;
94 12
            case NodeKind::ENUM_TYPE_EXTENSION:
95 6
                if (! ($type instanceof EnumType)) {
96 1
                    throw new Error(
97 1
                        'Cannot extend non-enum type "' . $type->name . '".',
98 1
                        [$node]
99
                    );
100
                }
101 5
                break;
102 9
            case NodeKind::UNION_TYPE_EXTENSION:
103 6
                if (! ($type instanceof UnionType)) {
104 1
                    throw new Error(
105 1
                        'Cannot extend non-union type "' . $type->name . '".',
106 1
                        [$node]
107
                    );
108
                }
109 5
                break;
110 7
            case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
111 6
                if (! ($type instanceof InputObjectType)) {
112 1
                    throw new Error(
113 1
                        'Cannot extend non-input object type "' . $type->name . '".',
114 1
                        [$node]
115
                    );
116
                }
117 5
                break;
118
        }
119 28
    }
120
121 42
    protected static function extendCustomScalarType(CustomScalarType $type) : CustomScalarType
122
    {
123 42
        return new CustomScalarType([
124 42
            'name' => $type->name,
125 42
            'description' => $type->description,
126 42
            'astNode' => $type->astNode,
127 42
            'serialize' => $type->config['serialize'] ?? null,
128 42
            'parseValue' => $type->config['parseValue'] ?? null,
129 42
            'parseLiteral' => $type->config['parseLiteral'] ?? null,
130 42
            'extensionASTNodes' => static::getExtensionASTNodes($type),
131
        ]);
132
    }
133
134 42
    protected static function extendUnionType(UnionType $type) : UnionType
135
    {
136 42
        return new UnionType([
137 42
            'name' => $type->name,
138 42
            'description' => $type->description,
139
            'types' => static function () use ($type) {
140 41
                return static::extendPossibleTypes($type);
141 42
            },
142 42
            'astNode' => $type->astNode,
143 42
            'resolveType' => $type->config['resolveType'] ?? null,
144 42
            'extensionASTNodes' => static::getExtensionASTNodes($type),
145
        ]);
146
    }
147
148 42
    protected static function extendEnumType(EnumType $type) : EnumType
149
    {
150 42
        return new EnumType([
151 42
            'name' => $type->name,
152 42
            'description' => $type->description,
153 42
            'values' => static::extendValueMap($type),
154 41
            'astNode' => $type->astNode,
155 41
            'extensionASTNodes' => static::getExtensionASTNodes($type),
156
        ]);
157
    }
158
159 42
    protected static function extendInputObjectType(InputObjectType $type) : InputObjectType
160
    {
161 42
        return new InputObjectType([
162 42
            'name' => $type->name,
163 42
            'description' => $type->description,
164
            'fields' => static function () use ($type) {
165 42
                return static::extendInputFieldMap($type);
166 42
            },
167 42
            'astNode' => $type->astNode,
168 42
            'extensionASTNodes' => static::getExtensionASTNodes($type),
169
        ]);
170
    }
171
172
    /**
173
     * @return mixed[]
174
     */
175 42
    protected static function extendInputFieldMap(InputObjectType $type) : array
176
    {
177 42
        $newFieldMap = [];
178 42
        $oldFieldMap = $type->getFields();
179 42
        foreach ($oldFieldMap as $fieldName => $field) {
180 42
            $newFieldMap[$fieldName] = [
181 42
                'description' => $field->description,
182 42
                'type' => static::extendType($field->type),
183 42
                'astNode' => $field->astNode,
184
            ];
185
186 42
            if (! $field->defaultValueExists()) {
187 42
                continue;
188
            }
189
190
            $newFieldMap[$fieldName]['defaultValue'] = $field->defaultValue;
191
        }
192
193 42
        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
194 42
        if ($extensions !== null) {
195 5
            foreach ($extensions as $extension) {
196 5
                foreach ($extension->fields as $field) {
197 5
                    $fieldName = $field->name->value;
198 5
                    if (isset($oldFieldMap[$fieldName])) {
199 1
                        throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
200
                    }
201
202 4
                    $newFieldMap[$fieldName] = static::$astBuilder->buildInputField($field);
203
                }
204
            }
205
        }
206
207 42
        return $newFieldMap;
208
    }
209
210
    /**
211
     * @return mixed[]
212
     */
213 42
    protected static function extendValueMap(EnumType $type) : array
214
    {
215 42
        $newValueMap = [];
216
        /** @var EnumValueDefinition[] $oldValueMap */
217 42
        $oldValueMap = [];
218 42
        foreach ($type->getValues() as $value) {
219 42
            $oldValueMap[$value->name] = $value;
220
        }
221
222 42
        foreach ($oldValueMap as $key => $value) {
223 42
            $newValueMap[$key] = [
224 42
                'name' => $value->name,
225 42
                'description' => $value->description,
226 42
                'value' => $value->value,
227 42
                'deprecationReason' => $value->deprecationReason,
228 42
                'astNode' => $value->astNode,
229
            ];
230
        }
231
232 42
        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
233 42
        if ($extensions !== null) {
234 5
            foreach ($extensions as $extension) {
235 5
                foreach ($extension->values as $value) {
236 5
                    $valueName = $value->name->value;
237 5
                    if (isset($oldValueMap[$valueName])) {
238 1
                        throw new Error('Enum value "' . $type->name . '.' . $valueName . '" already exists in the schema. It cannot also be defined in this type extension.', [$value]);
239
                    }
240 4
                    $newValueMap[$valueName] = static::$astBuilder->buildEnumValue($value);
241
                }
242
            }
243
        }
244
245 41
        return $newValueMap;
246
    }
247
248
    /**
249
     * @return ObjectType[]
250
     */
251 41
    protected static function extendPossibleTypes(UnionType $type) : array
252
    {
253
        $possibleTypes = array_map(static function ($type) {
254 41
            return static::extendNamedType($type);
255 41
        }, $type->getTypes());
256
257 41
        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
258 41
        if ($extensions !== null) {
259 5
            foreach ($extensions as $extension) {
260 5
                foreach ($extension->types as $namedType) {
261 5
                    $possibleTypes[] = static::$astBuilder->buildType($namedType);
262
                }
263
            }
264
        }
265
266 41
        return $possibleTypes;
267
    }
268
269
    /**
270
     * @return InterfaceType[]
271
     */
272 45
    protected static function extendImplementedInterfaces(ObjectType $type) : array
273
    {
274
        $interfaces = array_map(static function (InterfaceType $interfaceType) {
275 42
            return static::extendNamedType($interfaceType);
276 45
        }, $type->getInterfaces());
277
278 45
        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
279 45
        if ($extensions !== null) {
280
            /** @var ObjectTypeExtensionNode $extension */
281 19
            foreach ($extensions as $extension) {
282 19
                foreach ($extension->interfaces as $namedType) {
283 19
                    $interfaces[] = static::$astBuilder->buildType($namedType);
284
                }
285
            }
286
        }
287 45
        return $interfaces;
288
    }
289
290 46
    protected static function extendType($typeDef)
291
    {
292 46
        if ($typeDef instanceof ListOfType) {
293 42
            return Type::listOf(static::extendType($typeDef->ofType));
294
        }
295
296 46
        if ($typeDef instanceof NonNull) {
297 45
            return Type::nonNull(static::extendType($typeDef->getWrappedType()));
298
        }
299
300 46
        return static::extendNamedType($typeDef);
301
    }
302
303
    /**
304
     * @param FieldArgument[] $args
305
     *
306
     * @return mixed[]
307
     */
308 45
    protected static function extendArgs(array $args) : array
309
    {
310 45
        return Utils::keyValMap(
311 45
            $args,
312
            static function (FieldArgument $arg) {
313 45
                return $arg->name;
314 45
            },
315
            static function (FieldArgument $arg) {
316
                $def = [
317 45
                    'type'        => static::extendType($arg->getType()),
318 45
                    'description' => $arg->description,
319 45
                    'astNode'     => $arg->astNode,
320
                ];
321
322 45
                if ($arg->defaultValueExists()) {
323 44
                    $def['defaultValue'] = $arg->defaultValue;
324
                }
325
326 45
                return $def;
327 45
            }
328
        );
329
    }
330
331
    /**
332
     * @param InterfaceType|ObjectType $type
333
     *
334
     * @return mixed[]
335
     *
336
     * @throws Error
337
     */
338 45
    protected static function extendFieldMap($type) : array
339
    {
340 45
        $newFieldMap = [];
341 45
        $oldFieldMap = $type->getFields();
342
343 45
        foreach (array_keys($oldFieldMap) as $fieldName) {
344 45
            $field = $oldFieldMap[$fieldName];
345
346 45
            $newFieldMap[$fieldName] = [
347 45
                'name' => $fieldName,
348 45
                'description' => $field->description,
349 45
                'deprecationReason' => $field->deprecationReason,
350 45
                'type' => static::extendType($field->getType()),
351 45
                'args' => static::extendArgs($field->args),
352 45
                'astNode' => $field->astNode,
353 45
                'resolve' => $field->resolveFn,
354
            ];
355
        }
356
357 45
        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
358 45
        if ($extensions !== null) {
359 21
            foreach ($extensions as $extension) {
360 21
                foreach ($extension->fields as $field) {
361 21
                    $fieldName = $field->name->value;
362 21
                    if (isset($oldFieldMap[$fieldName])) {
363 1
                        throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
364
                    }
365
366 20
                    $newFieldMap[$fieldName] = static::$astBuilder->buildField($field);
367
                }
368
            }
369
        }
370
371 45
        return $newFieldMap;
372
    }
373
374 49
    protected static function extendObjectType(ObjectType $type) : ObjectType
375
    {
376 49
        return new ObjectType([
377 49
            'name' => $type->name,
378 49
            'description' => $type->description,
379
            'interfaces' => static function () use ($type) {
380 45
                return static::extendImplementedInterfaces($type);
381 49
            },
382
            'fields' => static function () use ($type) {
383 45
                return static::extendFieldMap($type);
384 49
            },
385 49
            'astNode' => $type->astNode,
386 49
            'extensionASTNodes' => static::getExtensionASTNodes($type),
387 49
            'isTypeOf' => $type->config['isTypeOf'] ?? null,
388
        ]);
389
    }
390
391 43
    protected static function extendInterfaceType(InterfaceType $type) : InterfaceType
392
    {
393 43
        return new InterfaceType([
394 43
            'name' => $type->name,
395 43
            'description' => $type->description,
396
            'fields' => static function () use ($type) {
397 42
                return static::extendFieldMap($type);
398 43
            },
399 43
            'astNode' => $type->astNode,
400 43
            'extensionASTNodes' => static::getExtensionASTNodes($type),
401 43
            'resolveType' => $type->config['resolveType'] ?? null,
402
        ]);
403
    }
404
405 49
    protected static function isSpecifiedScalarType(Type $type) : bool
406
    {
407 49
        return $type instanceof NamedType &&
408
            (
409 49
                $type->name === Type::STRING ||
410 49
                $type->name === Type::INT ||
411 49
                $type->name === Type::FLOAT ||
412 49
                $type->name === Type::BOOLEAN ||
413 49
                $type->name === Type::ID
414
            );
415
    }
416
417 49
    protected static function extendNamedType(Type $type)
418
    {
419 49
        if (Introspection::isIntrospectionType($type) || static::isSpecifiedScalarType($type)) {
420 46
            return $type;
421
        }
422
423 49
        $name = $type->name;
424 49
        if (! isset(static::$extendTypeCache[$name])) {
425 49
            if ($type instanceof CustomScalarType) {
426 42
                static::$extendTypeCache[$name] = static::extendCustomScalarType($type);
427 49
            } elseif ($type instanceof ObjectType) {
428 49
                static::$extendTypeCache[$name] = static::extendObjectType($type);
429 43
            } elseif ($type instanceof InterfaceType) {
430 43
                static::$extendTypeCache[$name] = static::extendInterfaceType($type);
431 43
            } elseif ($type instanceof UnionType) {
432 42
                static::$extendTypeCache[$name] = static::extendUnionType($type);
433 43
            } elseif ($type instanceof EnumType) {
434 42
                static::$extendTypeCache[$name] = static::extendEnumType($type);
435 42
            } elseif ($type instanceof InputObjectType) {
436 42
                static::$extendTypeCache[$name] = static::extendInputObjectType($type);
437
            }
438
        }
439
440 49
        return static::$extendTypeCache[$name];
441
    }
442
443
    /**
444
     * @return mixed|null
445
     */
446 49
    protected static function extendMaybeNamedType(?NamedType $type = null)
447
    {
448 49
        if ($type !== null) {
449 48
            return static::extendNamedType($type);
450
        }
451
452 48
        return null;
453
    }
454
455
    /**
456
     * @param DirectiveDefinitionNode[] $directiveDefinitions
457
     *
458
     * @return Directive[]
459
     */
460 45
    protected static function getMergedDirectives(Schema $schema, array $directiveDefinitions) : array
461
    {
462
        $existingDirectives = array_map(static function (Directive $directive) {
463 45
            return static::extendDirective($directive);
464 45
        }, $schema->getDirectives());
465
466 45
        Utils::invariant(count($existingDirectives) > 0, 'schema must have default directives');
467
468 45
        return array_merge(
469 45
            $existingDirectives,
470
            array_map(static function (DirectiveDefinitionNode $directive) {
471 6
                return static::$astBuilder->buildDirective($directive);
472 45
            }, $directiveDefinitions)
473
        );
474
    }
475
476 45
    protected static function extendDirective(Directive $directive) : Directive
477
    {
478 45
        return new Directive([
479 45
            'name' => $directive->name,
480 45
            'description' => $directive->description,
481 45
            'locations' => $directive->locations,
482 45
            'args' => static::extendArgs($directive->args),
483 45
            'astNode' => $directive->astNode,
484
        ]);
485
    }
486
487
    /**
488
     * @param mixed[]|null $options
489
     */
490 55
    public static function extend(Schema $schema, DocumentNode $documentAST, ?array $options = null) : Schema
491
    {
492 55
        if ($options === null || ! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) {
493 54
            DocumentValidator::assertValidSDLExtension($documentAST, $schema);
494
        }
495
496 54
        $typeDefinitionMap         = [];
497 54
        static::$typeExtensionsMap = [];
498 54
        $directiveDefinitions      = [];
499
        /** @var SchemaDefinitionNode|null $schemaDef */
500 54
        $schemaDef = null;
501
        /** @var SchemaTypeExtensionNode[] $schemaExtensions */
502 54
        $schemaExtensions = [];
503
504 54
        $definitionsCount = count($documentAST->definitions);
505 54
        for ($i = 0; $i < $definitionsCount; $i++) {
506
507
            /** @var Node $def */
508 54
            $def = $documentAST->definitions[$i];
509
510 54
            if ($def instanceof SchemaDefinitionNode) {
511 1
                $schemaDef = $def;
512 53
            } elseif ($def instanceof SchemaTypeExtensionNode) {
513 8
                $schemaExtensions[] = $def;
514 52
            } elseif ($def instanceof TypeDefinitionNode) {
515 20
                $typeName = isset($def->name) ? $def->name->value : null;
516
517
                try {
518 20
                    $type = $schema->getType($typeName);
519 1
                } catch (Error $error) {
520 1
                    $type = null;
521
                }
522
523 20
                if ($type) {
524 1
                    throw new Error('Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.', [$def]);
525
                }
526 19
                $typeDefinitionMap[$typeName] = $def;
527 37
            } elseif ($def instanceof TypeExtensionNode) {
528 30
                $extendedTypeName = isset($def->name) ? $def->name->value : null;
529 30
                $existingType     = $schema->getType($extendedTypeName);
530 30
                if ($existingType === null) {
531 1
                    throw new Error('Cannot extend type "' . $extendedTypeName . '" because it does not exist in the existing schema.', [$def]);
532
                }
533
534 29
                static::checkExtensionNode($existingType, $def);
535
536 28
                $existingTypeExtensions                       = static::$typeExtensionsMap[$extendedTypeName] ?? null;
537 28
                static::$typeExtensionsMap[$extendedTypeName] = $existingTypeExtensions !== null ? array_merge($existingTypeExtensions, [$def]) : [$def];
538 8
            } elseif ($def instanceof DirectiveDefinitionNode) {
539 7
                $directiveName     = $def->name->value;
540 7
                $existingDirective = $schema->getDirective($directiveName);
541 7
                if ($existingDirective !== null) {
542 2
                    throw new Error('Directive "' . $directiveName . '" already exists in the schema. It cannot be redefined.', [$def]);
543
                }
544 6
                $directiveDefinitions[] = $def;
545
            }
546
        }
547
548 50
        if (count(static::$typeExtensionsMap) === 0 &&
549 50
            count($typeDefinitionMap) === 0 &&
550 50
            count($directiveDefinitions) === 0 &&
551 50
            count($schemaExtensions) === 0 &&
552 50
            $schemaDef === null
553
        ) {
554 1
            return $schema;
555
        }
556
557 49
        static::$astBuilder = new ASTDefinitionBuilder(
558 49
            $typeDefinitionMap,
559 49
            $options,
560
            static function (string $typeName) use ($schema) {
561
                /** @var NamedType $existingType */
562 10
                $existingType = $schema->getType($typeName);
563 10
                if ($existingType !== null) {
564 9
                    return static::extendNamedType($existingType);
565
                }
566
567 1
                throw new Error('Unknown type: "' . $typeName . '". Ensure that this type exists either in the original schema, or is added in a type definition.', [$typeName]);
568 49
            }
569
        );
570
571 49
        static::$extendTypeCache = [];
572
573
        $operationTypes = [
574 49
            'query' => static::extendMaybeNamedType($schema->getQueryType()),
575 49
            'mutation' => static::extendMaybeNamedType($schema->getMutationType()),
576 49
            'subscription' => static::extendMaybeNamedType($schema->getSubscriptionType()),
577
        ];
578
579 49
        if ($schemaDef) {
580 1
            foreach ($schemaDef->operationTypes as $operationType) {
581 1
                $operation = $operationType->operation;
582 1
                $type      = $operationType->type;
583
584 1
                if (isset($operationTypes[$operation])) {
585
                    throw new Error('Must provide only one ' . $operation . ' type in schema.');
586
                }
587
588 1
                $operationTypes[$operation] = static::$astBuilder->buildType($type);
589
            }
590
        }
591
592 49
        foreach ($schemaExtensions as $schemaExtension) {
593 8
            if (! $schemaExtension->operationTypes) {
594 2
                continue;
595
            }
596
597 7
            foreach ($schemaExtension->operationTypes as $operationType) {
598 7
                $operation = $operationType->operation;
599 7
                if (isset($operationTypes[$operation])) {
600 3
                    throw new Error('Must provide only one ' . $operation . ' type in schema.');
601
                }
602 6
                $operationTypes[$operation] = static::$astBuilder->buildType($operationType->type);
603
            }
604
        }
605
606 46
        $schemaExtensionASTNodes = count($schemaExtensions) > 0
607 5
            ? ($schema->extensionASTNodes ? array_merge($schema->extensionASTNodes, $schemaExtensions) : $schemaExtensions)
608 46
            : $schema->extensionASTNodes;
609
610 46
        $types = array_merge(
611
            array_map(static function ($type) {
612 46
                return static::extendType($type);
613 46
            }, array_values($schema->getTypeMap())),
614
            array_map(static function ($type) {
615 16
                return static::$astBuilder->buildType($type);
616 45
            }, array_values($typeDefinitionMap))
617
        );
618
619 45
        return new Schema([
620 45
            'query' => $operationTypes['query'],
621 45
            'mutation' => $operationTypes['mutation'],
622 45
            'subscription' => $operationTypes['subscription'],
623 45
            'types' => $types,
624 45
            'directives' => static::getMergedDirectives($schema, $directiveDefinitions),
625 45
            'astNode' => $schema->getAstNode(),
626 45
            'extensionASTNodes' => $schemaExtensionASTNodes,
627
        ]);
628
    }
629
}
630