From 3e067cc60fb720703453904f1351f3acbfb4b453 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Tue, 6 Mar 2018 12:53:28 +0100 Subject: [PATCH] Readd type decorator and fix lazy type loading --- src/Type/Schema.php | 2 +- src/Utils/ASTDefinitionBuilder.php | 67 ++++++++++++++- src/Utils/BuildSchema.php | 25 +++--- tests/Utils/BuildSchemaTest.php | 130 ++++++++++++++++++++++++++++- 4 files changed, 208 insertions(+), 16 deletions(-) diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 3e8b16b..5bda972 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -134,7 +134,7 @@ class Schema if ($config->subscription) { $this->resolvedTypes[$config->subscription->name] = $config->subscription; } - if ($this->config->types) { + if (is_array($this->config->types)) { foreach ($this->resolveAdditionalTypes() as $type) { if (isset($this->resolvedTypes[$type->name])) { Utils::invariant( diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index be90907..36c8ec8 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -38,6 +38,11 @@ class ASTDefinitionBuilder */ private $typeDefintionsMap; + /** + * @var callable + */ + private $typeConfigDecorator; + /** * @var array */ @@ -53,9 +58,10 @@ class ASTDefinitionBuilder */ private $cache; - public function __construct(array $typeDefintionsMap, $options, callable $resolveType) + public function __construct(array $typeDefintionsMap, $options, callable $resolveType, callable $typeConfigDecorator = null) { $this->typeDefintionsMap = $typeDefintionsMap; + $this->typeConfigDecorator = $typeConfigDecorator; $this->options = $options; $this->resolveType = $resolveType; @@ -101,7 +107,41 @@ class ASTDefinitionBuilder private function internalBuildType($typeName, $typeNode = null) { if (!isset($this->cache[$typeName])) { if (isset($this->typeDefintionsMap[$typeName])) { - $this->cache[$typeName] = $this->makeSchemaDef($this->typeDefintionsMap[$typeName]); + $type = $this->makeSchemaDef($this->typeDefintionsMap[$typeName]); + if ($this->typeConfigDecorator) { + $fn = $this->typeConfigDecorator; + try { + $config = $fn($type->config, $this->typeDefintionsMap[$typeName], $this->typeDefintionsMap); + } catch (\Exception $e) { + throw new Error( + "Type config decorator passed to " . (static::class) . " threw an error " . + "when building $typeName type: {$e->getMessage()}", + null, + null, + null, + null, + $e + ); + } catch (\Throwable $e) { + throw new Error( + "Type config decorator passed to " . (static::class) . " threw an error " . + "when building $typeName type: {$e->getMessage()}", + null, + null, + null, + null, + $e + ); + } + if (!is_array($config) || isset($config[0])) { + throw new Error( + "Type config decorator passed to " . (static::class) . " is expected to return an array, but got " . + Utils::getVariableType($config) + ); + } + $type = $this->makeSchemaDefFromConfig($this->typeDefintionsMap[$typeName], $config); + } + $this->cache[$typeName] = $type; } else { $fn = $this->resolveType; $this->cache[$typeName] = $fn($typeName, $typeNode); @@ -186,6 +226,29 @@ class ASTDefinitionBuilder } } + private function makeSchemaDefFromConfig($def, array $config) + { + if (!$def) { + throw new Error('def must be defined.'); + } + switch ($def->kind) { + case NodeKind::OBJECT_TYPE_DEFINITION: + return new ObjectType($config); + case NodeKind::INTERFACE_TYPE_DEFINITION: + return new InterfaceType($config); + case NodeKind::ENUM_TYPE_DEFINITION: + return new EnumType($config); + case NodeKind::UNION_TYPE_DEFINITION: + return new UnionType($config); + case NodeKind::SCALAR_TYPE_DEFINITION: + return new CustomScalarType($config); + case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: + return new InputObjectType($config); + default: + throw new Error("Type kind of {$def->kind} not supported."); + } + } + private function makeTypeDef(ObjectTypeDefinitionNode $def) { $typeName = $def->name->value; diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 0b8ae31..18e169d 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -26,7 +26,7 @@ class BuildSchema * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema * has no resolve methods, so execution will use default resolvers. * - * Accepts options as a second argument: + * Accepts options as a third argument: * * - commentDescriptions: * Provide true to use preceding comments as the description. @@ -34,25 +34,26 @@ class BuildSchema * * @api * @param DocumentNode $ast + * @param callable $typeConfigDecorator * @param array $options * @return Schema * @throws Error */ - public static function buildAST(DocumentNode $ast, array $options = []) + public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) { - $builder = new self($ast, $options); + $builder = new self($ast, $typeConfigDecorator, $options); return $builder->buildSchema(); } private $ast; private $nodeMap; - private $loadedTypeDefs; + private $typeConfigDecorator; private $options; - public function __construct(DocumentNode $ast, array $options = []) + public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) { $this->ast = $ast; - $this->loadedTypeDefs = []; + $this->typeConfigDecorator = $typeConfigDecorator; $this->options = $options; } @@ -101,7 +102,8 @@ class BuildSchema $defintionBuilder = new ASTDefinitionBuilder( $this->nodeMap, $this->options, - function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); } + function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); }, + $this->typeConfigDecorator ); $directives = array_map(function($def) use ($defintionBuilder) { @@ -152,9 +154,7 @@ class BuildSchema 'types' => function () use ($defintionBuilder) { $types = []; foreach ($this->nodeMap as $name => $def) { - if (!isset($this->loadedTypeDefs[$name])) { - $types[] = $defintionBuilder->buildType($def->name->value); - } + $types[] = $defintionBuilder->buildType($def->name->value); } return $types; } @@ -196,12 +196,13 @@ class BuildSchema * * @api * @param DocumentNode|Source|string $source + * @param callable $typeConfigDecorator * @param array $options * @return Schema */ - public static function build($source, array $options = []) + public static function build($source, callable $typeConfigDecorator = null, array $options = []) { $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); - return self::buildAST($doc, $options); + return self::buildAST($doc, $typeConfigDecorator, $options); } } diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index cfd21f7..916f377 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -20,7 +20,7 @@ class BuildSchemaTest extends \PHPUnit_Framework_TestCase private function cycleOutput($body, $options = []) { $ast = Parser::parse($body); - $schema = BuildSchema::buildAST($ast, $options); + $schema = BuildSchema::buildAST($ast, null, $options); return "\n" . SchemaPrinter::doPrint($schema, $options); } @@ -1140,4 +1140,132 @@ type Repeated { $this->setExpectedException('GraphQL\Error\Error', 'Type "Repeated" was defined more than once.'); BuildSchema::buildAST($doc); } + + public function testSupportsTypeConfigDecorator() + { + $body = ' +schema { + query: Query +} + +type Query { + str: String + color: Color + hello: Hello +} + +enum Color { + RED + GREEN + BLUE +} + +interface Hello { + world: String +} +'; + $doc = Parser::parse($body); + + $decorated = []; + $calls = []; + + $typeConfigDecorator = function($defaultConfig, $node, $allNodesMap) use (&$decorated, &$calls) { + $decorated[] = $defaultConfig['name']; + $calls[] = [$defaultConfig, $node, $allNodesMap]; + return ['description' => 'My description of ' . $node->name->value] + $defaultConfig; + }; + + $schema = BuildSchema::buildAST($doc, $typeConfigDecorator); + $schema->getTypeMap(); + $this->assertEquals(['Query', 'Color', 'Hello'], $decorated); + + list($defaultConfig, $node, $allNodesMap) = $calls[0]; + $this->assertInstanceOf(ObjectTypeDefinitionNode::class, $node); + $this->assertEquals('Query', $defaultConfig['name']); + $this->assertInstanceOf(\Closure::class, $defaultConfig['fields']); + $this->assertInstanceOf(\Closure::class, $defaultConfig['interfaces']); + $this->assertArrayHasKey('description', $defaultConfig); + $this->assertCount(5, $defaultConfig); + $this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); + $this->assertEquals('My description of Query', $schema->getType('Query')->description); + + + list($defaultConfig, $node, $allNodesMap) = $calls[1]; + $this->assertInstanceOf(EnumTypeDefinitionNode::class, $node); + $this->assertEquals('Color', $defaultConfig['name']); + $enumValue = [ + 'description' => '', + 'deprecationReason' => '' + ]; + $this->assertArraySubset([ + 'RED' => $enumValue, + 'GREEN' => $enumValue, + 'BLUE' => $enumValue, + ], $defaultConfig['values']); + $this->assertCount(4, $defaultConfig); // 3 + astNode + $this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); + $this->assertEquals('My description of Color', $schema->getType('Color')->description); + + list($defaultConfig, $node, $allNodesMap) = $calls[2]; + $this->assertInstanceOf(InterfaceTypeDefinitionNode::class, $node); + $this->assertEquals('Hello', $defaultConfig['name']); + $this->assertInstanceOf(\Closure::class, $defaultConfig['fields']); + $this->assertArrayHasKey('description', $defaultConfig); + $this->assertCount(4, $defaultConfig); + $this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); + $this->assertEquals('My description of Hello', $schema->getType('Hello')->description); + } + + public function testCreatesTypesLazily() + { + $body = ' +schema { + query: Query +} + +type Query { + str: String + color: Color + hello: Hello +} + +enum Color { + RED + GREEN + BLUE +} + +interface Hello { + world: String +} + +type World implements Hello { + world: String +} +'; + $doc = Parser::parse($body); + $created = []; + + $typeConfigDecorator = function($config, $node) use (&$created) { + $created[] = $node->name->value; + return $config; + }; + + $schema = BuildSchema::buildAST($doc, $typeConfigDecorator); + $this->assertEquals(['Query'], $created); + + $schema->getType('Color'); + $this->assertEquals(['Query', 'Color'], $created); + + $schema->getType('Hello'); + $this->assertEquals(['Query', 'Color', 'Hello'], $created); + + $types = $schema->getTypeMap(); + $this->assertEquals(['Query', 'Color', 'Hello', 'World'], $created); + $this->assertArrayHasKey('Query', $types); + $this->assertArrayHasKey('Color', $types); + $this->assertArrayHasKey('Hello', $types); + $this->assertArrayHasKey('World', $types); + } + }