Readd type decorator and fix lazy type loading

This commit is contained in:
Daniel Tschinder 2018-03-06 12:53:28 +01:00
parent f9a366e69a
commit 3e067cc60f
4 changed files with 208 additions and 16 deletions

View File

@ -134,7 +134,7 @@ class Schema
if ($config->subscription) { if ($config->subscription) {
$this->resolvedTypes[$config->subscription->name] = $config->subscription; $this->resolvedTypes[$config->subscription->name] = $config->subscription;
} }
if ($this->config->types) { if (is_array($this->config->types)) {
foreach ($this->resolveAdditionalTypes() as $type) { foreach ($this->resolveAdditionalTypes() as $type) {
if (isset($this->resolvedTypes[$type->name])) { if (isset($this->resolvedTypes[$type->name])) {
Utils::invariant( Utils::invariant(

View File

@ -38,6 +38,11 @@ class ASTDefinitionBuilder
*/ */
private $typeDefintionsMap; private $typeDefintionsMap;
/**
* @var callable
*/
private $typeConfigDecorator;
/** /**
* @var array * @var array
*/ */
@ -53,9 +58,10 @@ class ASTDefinitionBuilder
*/ */
private $cache; 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->typeDefintionsMap = $typeDefintionsMap;
$this->typeConfigDecorator = $typeConfigDecorator;
$this->options = $options; $this->options = $options;
$this->resolveType = $resolveType; $this->resolveType = $resolveType;
@ -101,7 +107,41 @@ class ASTDefinitionBuilder
private function internalBuildType($typeName, $typeNode = null) { private function internalBuildType($typeName, $typeNode = null) {
if (!isset($this->cache[$typeName])) { if (!isset($this->cache[$typeName])) {
if (isset($this->typeDefintionsMap[$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 { } else {
$fn = $this->resolveType; $fn = $this->resolveType;
$this->cache[$typeName] = $fn($typeName, $typeNode); $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) private function makeTypeDef(ObjectTypeDefinitionNode $def)
{ {
$typeName = $def->name->value; $typeName = $def->name->value;

View File

@ -26,7 +26,7 @@ class BuildSchema
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
* has no resolve methods, so execution will use default resolvers. * has no resolve methods, so execution will use default resolvers.
* *
* Accepts options as a second argument: * Accepts options as a third argument:
* *
* - commentDescriptions: * - commentDescriptions:
* Provide true to use preceding comments as the description. * Provide true to use preceding comments as the description.
@ -34,25 +34,26 @@ class BuildSchema
* *
* @api * @api
* @param DocumentNode $ast * @param DocumentNode $ast
* @param callable $typeConfigDecorator
* @param array $options * @param array $options
* @return Schema * @return Schema
* @throws Error * @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(); return $builder->buildSchema();
} }
private $ast; private $ast;
private $nodeMap; private $nodeMap;
private $loadedTypeDefs; private $typeConfigDecorator;
private $options; private $options;
public function __construct(DocumentNode $ast, array $options = []) public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = [])
{ {
$this->ast = $ast; $this->ast = $ast;
$this->loadedTypeDefs = []; $this->typeConfigDecorator = $typeConfigDecorator;
$this->options = $options; $this->options = $options;
} }
@ -101,7 +102,8 @@ class BuildSchema
$defintionBuilder = new ASTDefinitionBuilder( $defintionBuilder = new ASTDefinitionBuilder(
$this->nodeMap, $this->nodeMap,
$this->options, $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) { $directives = array_map(function($def) use ($defintionBuilder) {
@ -152,10 +154,8 @@ class BuildSchema
'types' => function () use ($defintionBuilder) { 'types' => function () use ($defintionBuilder) {
$types = []; $types = [];
foreach ($this->nodeMap as $name => $def) { 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; return $types;
} }
]); ]);
@ -196,12 +196,13 @@ class BuildSchema
* *
* @api * @api
* @param DocumentNode|Source|string $source * @param DocumentNode|Source|string $source
* @param callable $typeConfigDecorator
* @param array $options * @param array $options
* @return Schema * @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); $doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
return self::buildAST($doc, $options); return self::buildAST($doc, $typeConfigDecorator, $options);
} }
} }

View File

@ -20,7 +20,7 @@ class BuildSchemaTest extends \PHPUnit_Framework_TestCase
private function cycleOutput($body, $options = []) private function cycleOutput($body, $options = [])
{ {
$ast = Parser::parse($body); $ast = Parser::parse($body);
$schema = BuildSchema::buildAST($ast, $options); $schema = BuildSchema::buildAST($ast, null, $options);
return "\n" . SchemaPrinter::doPrint($schema, $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.'); $this->setExpectedException('GraphQL\Error\Error', 'Type "Repeated" was defined more than once.');
BuildSchema::buildAST($doc); 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);
}
} }