Failed Conditions
Push — master ( 7ff3e9...e7de06 )
by Vladimir
04:33 queued 01:54
created

SchemaExtender::isSpecifiedScalarType()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
nop 1
crap 6
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 47
    protected static function getExtensionASTNodes(NamedType $type) : ?array
56
    {
57 47
        if (! $type instanceof Type) {
58
            return null;
59
        }
60
61 47
        $name = $type->name;
62 47
        if ($type->extensionASTNodes !== null) {
63 47
            if (isset(static::$typeExtensionsMap[$name])) {
64 18
                return array_merge($type->extensionASTNodes, static::$typeExtensionsMap[$name]);
65
            }
66
67 46
            return $type->extensionASTNodes;
68
        }
69 43
        return static::$typeExtensionsMap[$name] ?? null;
70
    }
71
72
    /**
73
     * @throws Error
74
     */
75 28
    protected static function checkExtensionNode(Type $type, Node $node) : void
76
    {
77 28
        switch ($node->kind ?? null) {
78 28
            case NodeKind::OBJECT_TYPE_EXTENSION:
79 19
                if (! ($type instanceof ObjectType)) {
80 1
                    throw new Error(
81 1
                        'Cannot extend non-object type "' . $type->name . '".',
82 1
                        [$node]
83
                    );
84
                }
85 18
                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 27
    }
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 43
    protected static function extendImplementedInterfaces(ObjectType $type) : array
273
    {
274
        $interfaces = array_map(static function (InterfaceType $interfaceType) {
275 42
            return static::extendNamedType($interfaceType);
276 43
        }, $type->getInterfaces());
277
278 43
        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
279 43
        if ($extensions !== null) {
280
            /** @var ObjectTypeExtensionNode $extension */
281 18
            foreach ($extensions as $extension) {
282 18
                foreach ($extension->interfaces as $namedType) {
283 18
                    $interfaces[] = static::$astBuilder->buildType($namedType);
284
                }
285
            }
286
        }
287 43
        return $interfaces;
288
    }
289
290 44
    protected static function extendType($typeDef)
291
    {
292 44
        if ($typeDef instanceof ListOfType) {
293 42
            return Type::listOf(static::extendType($typeDef->ofType));
294
        }
295
296 44
        if ($typeDef instanceof NonNull) {
297 43
            return Type::nonNull(static::extendType($typeDef->getWrappedType()));
298
        }
299
300 44
        return static::extendNamedType($typeDef);
301
    }
302
303
    /**
304
     * @param FieldArgument[] $args
305
     *
306
     * @return mixed[]
307
     */
308 43
    protected static function extendArgs(array $args) : array
309
    {
310 43
        return Utils::keyValMap(
311 43
            $args,
312
            static function (FieldArgument $arg) {
313 43
                return $arg->name;
314 43
            },
315
            static function (FieldArgument $arg) {
316
                $def = [
317 43
                    'type'        => static::extendType($arg->getType()),
318 43
                    'description' => $arg->description,
319 43
                    'astNode'     => $arg->astNode,
320
                ];
321
322 43
                if ($arg->defaultValueExists()) {
323 42
                    $def['defaultValue'] = $arg->defaultValue;
324
                }
325
326 43
                return $def;
327 43
            }
328
        );
329
    }
330
331
    /**
332
     * @param InterfaceType|ObjectType $type
333
     *
334
     * @return mixed[]
335
     *
336
     * @throws Error
337
     */
338 43
    protected static function extendFieldMap($type) : array
339
    {
340 43
        $newFieldMap = [];
341 43
        $oldFieldMap = $type->getFields();
342
343 43
        foreach (array_keys($oldFieldMap) as $fieldName) {
344 43
            $field = $oldFieldMap[$fieldName];
345
346 43
            $newFieldMap[$fieldName] = [
347 43
                'name' => $fieldName,
348 43
                'description' => $field->description,
349 43
                'deprecationReason' => $field->deprecationReason,
350 43
                'type' => static::extendType($field->getType()),
351 43
                'args' => static::extendArgs($field->args),
352 43
                'astNode' => $field->astNode,
353 43
                'resolveFn' => $field->resolveFn,
354
            ];
355
        }
356
357 43
        $extensions = static::$typeExtensionsMap[$type->name] ?? null;
358 43
        if ($extensions !== null) {
359 20
            foreach ($extensions as $extension) {
360 20
                foreach ($extension->fields as $field) {
361 20
                    $fieldName = $field->name->value;
362 20
                    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 19
                    $newFieldMap[$fieldName] = static::$astBuilder->buildField($field);
367
                }
368
            }
369
        }
370
371 43
        return $newFieldMap;
372
    }
373
374 47
    protected static function extendObjectType(ObjectType $type) : ObjectType
375
    {
376 47
        return new ObjectType([
377 47
            'name' => $type->name,
378 47
            'description' => $type->description,
379
            'interfaces' => static function () use ($type) {
380 43
                return static::extendImplementedInterfaces($type);
381 47
            },
382
            'fields' => static function () use ($type) {
383 43
                return static::extendFieldMap($type);
384 47
            },
385 47
            'astNode' => $type->astNode,
386 47
            'extensionASTNodes' => static::getExtensionASTNodes($type),
387 47
            '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 47
    protected static function isSpecifiedScalarType(Type $type) : bool
406
    {
407 47
        return $type instanceof NamedType &&
408
            (
409 47
                $type->name === Type::STRING ||
410 47
                $type->name === Type::INT ||
411 47
                $type->name === Type::FLOAT ||
412 47
                $type->name === Type::BOOLEAN ||
413 47
                $type->name === Type::ID
414
            );
415
    }
416
417 47
    protected static function extendNamedType(Type $type)
418
    {
419 47
        if (Introspection::isIntrospectionType($type) || static::isSpecifiedScalarType($type)) {
420 44
            return $type;
421
        }
422
423 47
        $name = $type->name;
424 47
        if (! isset(static::$extendTypeCache[$name])) {
425 47
            if ($type instanceof CustomScalarType) {
426 42
                static::$extendTypeCache[$name] = static::extendCustomScalarType($type);
427 47
            } elseif ($type instanceof ObjectType) {
428 47
                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 47
        return static::$extendTypeCache[$name];
441
    }
442
443
    /**
444
     * @return mixed|null
445
     */
446 47
    protected static function extendMaybeNamedType(?NamedType $type = null)
447
    {
448 47
        if ($type !== null) {
449 46
            return static::extendNamedType($type);
450
        }
451
452 46
        return null;
453
    }
454
455
    /**
456
     * @param DirectiveDefinitionNode[] $directiveDefinitions
457
     *
458
     * @return Directive[]
459
     */
460 43
    protected static function getMergedDirectives(Schema $schema, array $directiveDefinitions) : array
461
    {
462
        $existingDirectives = array_map(static function (Directive $directive) {
463 43
            return static::extendDirective($directive);
464 43
        }, $schema->getDirectives());
465
466 43
        Utils::invariant(count($existingDirectives) > 0, 'schema must have default directives');
467
468 43
        return array_merge(
469 43
            $existingDirectives,
470
            array_map(static function (DirectiveDefinitionNode $directive) {
471 6
                return static::$astBuilder->buildDirective($directive);
472 43
            }, $directiveDefinitions)
473
        );
474
    }
475
476 43
    protected static function extendDirective(Directive $directive) : Directive
477
    {
478 43
        return new Directive([
479 43
            'name' => $directive->name,
480 43
            'description' => $directive->description,
481 43
            'locations' => $directive->locations,
482 43
            'args' => static::extendArgs($directive->args),
483 43
            'astNode' => $directive->astNode,
484
        ]);
485
    }
486
487
    /**
488
     * @param mixed[]|null $options
489
     */
490 53
    public static function extend(Schema $schema, DocumentNode $documentAST, ?array $options = null) : Schema
491
    {
492 53
        if ($options === null || ! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) {
493 52
            DocumentValidator::assertValidSDLExtension($documentAST, $schema);
494
        }
495
496 52
        $typeDefinitionMap         = [];
497 52
        static::$typeExtensionsMap = [];
498 52
        $directiveDefinitions      = [];
499
        /** @var SchemaDefinitionNode|null $schemaDef */
500 52
        $schemaDef = null;
501
        /** @var SchemaTypeExtensionNode[] $schemaExtensions */
502 52
        $schemaExtensions = [];
503
504 52
        $definitionsCount = count($documentAST->definitions);
505 52
        for ($i = 0; $i < $definitionsCount; $i++) {
506
507
            /** @var Node $def */
508 52
            $def = $documentAST->definitions[$i];
509
510 52
            if ($def instanceof SchemaDefinitionNode) {
511 1
                $schemaDef = $def;
512 51
            } elseif ($def instanceof SchemaTypeExtensionNode) {
513 8
                $schemaExtensions[] = $def;
514 50
            } elseif ($def instanceof TypeDefinitionNode) {
515 19
                $typeName = isset($def->name) ? $def->name->value : null;
516 19
                if ($schema->getType($typeName)) {
517 1
                    throw new Error('Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.', [$def]);
518
                }
519 18
                $typeDefinitionMap[$typeName] = $def;
520 36
            } elseif ($def instanceof TypeExtensionNode) {
521 29
                $extendedTypeName = isset($def->name) ? $def->name->value : null;
522 29
                $existingType     = $schema->getType($extendedTypeName);
523 29
                if ($existingType === null) {
524 1
                    throw new Error('Cannot extend type "' . $extendedTypeName . '" because it does not exist in the existing schema.', [$def]);
525
                }
526
527 28
                static::checkExtensionNode($existingType, $def);
528
529 27
                $existingTypeExtensions                       = static::$typeExtensionsMap[$extendedTypeName] ?? null;
530 27
                static::$typeExtensionsMap[$extendedTypeName] = $existingTypeExtensions !== null ? array_merge($existingTypeExtensions, [$def]) : [$def];
531 8
            } elseif ($def instanceof DirectiveDefinitionNode) {
532 7
                $directiveName     = $def->name->value;
533 7
                $existingDirective = $schema->getDirective($directiveName);
534 7
                if ($existingDirective !== null) {
535 2
                    throw new Error('Directive "' . $directiveName . '" already exists in the schema. It cannot be redefined.', [$def]);
536
                }
537 6
                $directiveDefinitions[] = $def;
538
            }
539
        }
540
541 48
        if (count(static::$typeExtensionsMap) === 0 &&
542 48
            count($typeDefinitionMap) === 0 &&
543 48
            count($directiveDefinitions) === 0 &&
544 48
            count($schemaExtensions) === 0 &&
545 48
            $schemaDef === null
546
        ) {
547 1
            return $schema;
548
        }
549
550 47
        static::$astBuilder = new ASTDefinitionBuilder(
551 47
            $typeDefinitionMap,
552 47
            $options,
553
            static function (string $typeName) use ($schema) {
554
                /** @var NamedType $existingType */
555 9
                $existingType = $schema->getType($typeName);
556 9
                if ($existingType !== null) {
557 8
                    return static::extendNamedType($existingType);
558
                }
559
560 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]);
561 47
            }
562
        );
563
564 47
        static::$extendTypeCache = [];
565
566
        $operationTypes = [
567 47
            'query' => static::extendMaybeNamedType($schema->getQueryType()),
568 47
            'mutation' => static::extendMaybeNamedType($schema->getMutationType()),
569 47
            'subscription' => static::extendMaybeNamedType($schema->getSubscriptionType()),
570
        ];
571
572 47
        if ($schemaDef) {
573 1
            foreach ($schemaDef->operationTypes as $operationType) {
574 1
                $operation = $operationType->operation;
575 1
                $type      = $operationType->type;
576
577 1
                if (isset($operationTypes[$operation])) {
578
                    throw new Error('Must provide only one ' . $operation . ' type in schema.');
579
                }
580
581 1
                $operationTypes[$operation] = static::$astBuilder->buildType($type);
582
            }
583
        }
584
585 47
        foreach ($schemaExtensions as $schemaExtension) {
586 8
            if (! $schemaExtension->operationTypes) {
587 2
                continue;
588
            }
589
590 7
            foreach ($schemaExtension->operationTypes as $operationType) {
591 7
                $operation = $operationType->operation;
592 7
                if ($operationTypes[$operation]) {
593 3
                    throw new Error('Must provide only one ' . $operation . ' type in schema.');
594
                }
595 6
                $operationTypes[$operation] = static::$astBuilder->buildType($operationType->type);
596
            }
597
        }
598
599 44
        $schemaExtensionASTNodes = count($schemaExtensions) > 0
600 5
            ? ($schema->extensionASTNodes ? array_merge($schema->extensionASTNodes, $schemaExtensions) : $schemaExtensions)
601 44
            : $schema->extensionASTNodes;
602
603 44
        $types = array_merge(
604
            array_map(static function ($type) {
605 44
                return static::extendType($type);
606 44
            }, array_values($schema->getTypeMap())),
607
            array_map(static function ($type) {
608 15
                return static::$astBuilder->buildType($type);
609 43
            }, array_values($typeDefinitionMap))
610
        );
611
612 43
        return new Schema([
613 43
            'query' => $operationTypes['query'],
614 43
            'mutation' => $operationTypes['mutation'],
615 43
            'subscription' => $operationTypes['subscription'],
616 43
            'types' => $types,
617 43
            'directives' => static::getMergedDirectives($schema, $directiveDefinitions),
618 43
            'astNode' => $schema->getAstNode(),
619 43
            'extensionASTNodes' => $schemaExtensionASTNodes,
620
        ]);
621
    }
622
}
623