Added ability to decorate type configs in BuildSchema + made type creation lazy

This commit is contained in:
Vladimir Razuvaev 2017-07-28 17:55:25 +07:00
parent 3a8301f6c6
commit 09070485c1
3 changed files with 290 additions and 59 deletions

View File

@ -76,22 +76,27 @@ class BuildSchema
* has no resolve methods, so execution will use default resolvers. * has no resolve methods, so execution will use default resolvers.
* *
* @param DocumentNode $ast * @param DocumentNode $ast
* @param callable $typeConfigDecorator
* @return Schema * @return Schema
* @throws Error * @throws Error
*/ */
public static function buildAST(DocumentNode $ast) public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null)
{ {
$builder = new self($ast); $builder = new self($ast, $typeConfigDecorator);
return $builder->buildSchema(); return $builder->buildSchema();
} }
private $ast; private $ast;
private $innerTypeMap; private $innerTypeMap;
private $nodeMap; private $nodeMap;
private $typeConfigDecorator;
private $loadedTypeDefs;
public function __construct(DocumentNode $ast) public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null)
{ {
$this->ast = $ast; $this->ast = $ast;
$this->typeConfigDecorator = $typeConfigDecorator;
$this->loadedTypeDefs = [];
} }
public function buildSchema() public function buildSchema()
@ -199,10 +204,6 @@ class BuildSchema
'__TypeKind' => Introspection::_typeKind(), '__TypeKind' => Introspection::_typeKind(),
]; ];
$types = array_map(function($def) {
return $this->typeDefNamed($def->name->value);
}, $typeDefs);
$directives = array_map([$this, 'getDirective'], $directiveDefs); $directives = array_map([$this, 'getDirective'], $directiveDefs);
// If specified directives were not explicitly declared, add them. // If specified directives were not explicitly declared, add them.
@ -235,14 +236,21 @@ class BuildSchema
'subscription' => $subscriptionTypeName ? 'subscription' => $subscriptionTypeName ?
$this->getObjectType($this->nodeMap[$subscriptionTypeName]) : $this->getObjectType($this->nodeMap[$subscriptionTypeName]) :
null, null,
'types' => $types, 'typeLoader' => function($name) {
return $this->typeDefNamed($name);
},
'directives' => $directives, 'directives' => $directives,
'types' => function() {
$types = [];
foreach ($this->nodeMap as $name => $def) {
if (!isset($this->loadedTypeDefs[$name])) {
$types[] = $this->typeDefNamed($def->name->value);
}
}
return $types;
}
]); ]);
// Types in schema are loaded lazily, but we need full scan to ensure that schema is consistent
// Following statement will force Schema to scan all types and fields:
// TODO: replace this call with schema validator once it's ready
$schema->getTypeMap();
return $schema; return $schema;
} }
@ -251,9 +259,9 @@ class BuildSchema
return new Directive([ return new Directive([
'name' => $directiveNode->name->value, 'name' => $directiveNode->name->value,
'description' => $this->getDescription($directiveNode), 'description' => $this->getDescription($directiveNode),
'locations' => array_map(function($node) { 'locations' => Utils::map($directiveNode->locations, function($node) {
return $node->value; return $node->value;
}, $directiveNode->locations), }),
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
]); ]);
} }
@ -313,7 +321,45 @@ class BuildSchema
throw new Error('Type "' . $typeName . '" not found in document.'); throw new Error('Type "' . $typeName . '" not found in document.');
} }
$innerTypeDef = $this->makeSchemaDef($this->nodeMap[$typeName]); $this->loadedTypeDefs[$typeName] = true;
$config = $this->makeSchemaDefConfig($this->nodeMap[$typeName]);
if ($this->typeConfigDecorator) {
$fn = $this->typeConfigDecorator;
try {
$config = $fn($this->nodeMap[$typeName], $config, $this->nodeMap);
} 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)
);
}
}
$innerTypeDef = $this->makeSchemaDef($this->nodeMap[$typeName], $config);
if (!$innerTypeDef) { if (!$innerTypeDef) {
throw new Error("Nothing constructed for $typeName."); throw new Error("Nothing constructed for $typeName.");
} }
@ -321,38 +367,68 @@ class BuildSchema
return $innerTypeDef; return $innerTypeDef;
} }
private function makeSchemaDef($def) private function makeSchemaDefConfig($def)
{ {
if (!$def) { if (!$def) {
throw new Error('def must be defined.'); throw new Error('def must be defined.');
} }
switch ($def->kind) { switch ($def->kind) {
case NodeKind::OBJECT_TYPE_DEFINITION: case NodeKind::OBJECT_TYPE_DEFINITION:
return $this->makeTypeDef($def); return $this->makeTypeDefConfig($def);
case NodeKind::INTERFACE_TYPE_DEFINITION: case NodeKind::INTERFACE_TYPE_DEFINITION:
return $this->makeInterfaceDef($def); return $this->makeInterfaceDefConfig($def);
case NodeKind::ENUM_TYPE_DEFINITION: case NodeKind::ENUM_TYPE_DEFINITION:
return $this->makeEnumDef($def); return $this->makeEnumDefConfig($def);
case NodeKind::UNION_TYPE_DEFINITION: case NodeKind::UNION_TYPE_DEFINITION:
return $this->makeUnionDef($def); return $this->makeUnionDefConfig($def);
case NodeKind::SCALAR_TYPE_DEFINITION: case NodeKind::SCALAR_TYPE_DEFINITION:
return $this->makeScalarDef($def); return $this->makeScalarDefConfig($def);
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
return $this->makeInputObjectDef($def); return $this->makeInputObjectDefConfig($def);
default: default:
throw new Error("Type kind of {$def->kind} not supported."); throw new Error("Type kind of {$def->kind} not supported.");
} }
} }
private function makeTypeDef(ObjectTypeDefinitionNode $def) private function makeSchemaDef($def, array $config = null)
{
if (!$def) {
throw new Error('def must be defined.');
}
$config = $config ?: $this->makeSchemaDefConfig($def);
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 makeTypeDefConfig(ObjectTypeDefinitionNode $def)
{ {
$typeName = $def->name->value; $typeName = $def->name->value;
return new ObjectType([ return [
'name' => $typeName, 'name' => $typeName,
'description' => $this->getDescription($def), 'description' => $this->getDescription($def),
'fields' => function() use ($def) { return $this->makeFieldDefMap($def); }, 'fields' => function() use ($def) {
'interfaces' => function() use ($def) { return $this->makeImplementedInterfaces($def); } return $this->makeFieldDefMap($def);
]); },
'interfaces' => function() use ($def) {
return $this->makeImplementedInterfaces($def);
}
];
} }
private function makeFieldDefMap($def) private function makeFieldDefMap($def)
@ -375,7 +451,12 @@ class BuildSchema
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
{ {
return isset($def->interfaces) ? array_map([$this, 'produceInterfaceType'], $def->interfaces) : null; if (isset($def->interfaces)) {
return Utils::map($def->interfaces, function ($iface) {
return $this->produceInterfaceType($iface);
});
}
return null;
} }
private function makeInputValues($values) private function makeInputValues($values)
@ -400,20 +481,24 @@ class BuildSchema
); );
} }
private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) private function makeInterfaceDefConfig(InterfaceTypeDefinitionNode $def)
{ {
$typeName = $def->name->value; $typeName = $def->name->value;
return new InterfaceType([ return [
'name' => $typeName, 'name' => $typeName,
'description' => $this->getDescription($def), 'description' => $this->getDescription($def),
'fields' => function() use ($def) { return $this->makeFieldDefMap($def); }, 'fields' => function() use ($def) {
'resolveType' => [$this, 'cannotExecuteSchema'] return $this->makeFieldDefMap($def);
]); },
'resolveType' => function() {
$this->cannotExecuteSchema();
}
];
} }
private function makeEnumDef(EnumTypeDefinitionNode $def) private function makeEnumDefConfig(EnumTypeDefinitionNode $def)
{ {
return new EnumType([ return [
'name' => $def->name->value, 'name' => $def->name->value,
'description' => $this->getDescription($def), 'description' => $this->getDescription($def),
'values' => Utils::keyValMap( 'values' => Utils::keyValMap(
@ -428,41 +513,49 @@ class BuildSchema
]; ];
} }
) )
]); ];
} }
private function makeUnionDef(UnionTypeDefinitionNode $def) private function makeUnionDefConfig(UnionTypeDefinitionNode $def)
{ {
return new UnionType([ return [
'name' => $def->name->value, 'name' => $def->name->value,
'description' => $this->getDescription($def), 'description' => $this->getDescription($def),
'types' => array_map([$this, 'produceObjectType'], $def->types), 'types' => Utils::map($def->types, function($typeNode) {
return $this->produceObjectType($typeNode);
}),
'resolveType' => [$this, 'cannotExecuteSchema'] 'resolveType' => [$this, 'cannotExecuteSchema']
]); ];
} }
private function makeScalarDef(ScalarTypeDefinitionNode $def) private function makeScalarDefConfig(ScalarTypeDefinitionNode $def)
{ {
return new CustomScalarType([ return [
'name' => $def->name->value, 'name' => $def->name->value,
'description' => $this->getDescription($def), 'description' => $this->getDescription($def),
'serialize' => function() { return false; }, 'serialize' => function() {
return false;
},
// Note: validation calls the parse functions to determine if a // Note: validation calls the parse functions to determine if a
// literal value is correct. Returning null would cause use of custom // literal value is correct. Returning null would cause use of custom
// scalars to always fail validation. Returning false causes them to // scalars to always fail validation. Returning false causes them to
// always pass validation. // always pass validation.
'parseValue' => function() { return false; }, 'parseValue' => function() {
'parseLiteral' => function() { return false; } return false;
]); },
'parseLiteral' => function() {
return false;
}
];
} }
private function makeInputObjectDef(InputObjectTypeDefinitionNode $def) private function makeInputObjectDefConfig(InputObjectTypeDefinitionNode $def)
{ {
return new InputObjectType([ return [
'name' => $def->name->value, 'name' => $def->name->value,
'description' => $this->getDescription($def), 'description' => $this->getDescription($def),
'fields' => function() use ($def) { return $this->makeInputValues($def->fields); } 'fields' => function() use ($def) { return $this->makeInputValues($def->fields); }
]); ];
} }
/** /**
@ -485,8 +578,8 @@ class BuildSchema
public function getDescription($node) public function getDescription($node)
{ {
$loc = $node->loc; $loc = $node->loc;
if (!$loc) { if (!$loc || !$loc->startToken) {
return; return ;
} }
$comments = []; $comments = [];
$minSpaces = null; $minSpaces = null;
@ -516,11 +609,12 @@ class BuildSchema
* document. * document.
* *
* @param Source|string $source * @param Source|string $source
* @param callable $typeConfigDecorator
* @return Schema * @return Schema
*/ */
public static function build($source) public static function build($source, callable $typeConfigDecorator = null)
{ {
return self::buildAST(Parser::parse($source)); return self::buildAST(Parser::parse($source), $typeConfigDecorator);
} }
// Count the number of spaces on the starting side of a string. // Count the number of spaces on the starting side of a string.
@ -529,7 +623,8 @@ class BuildSchema
return strlen($str) - strlen(ltrim($str)); return strlen($str) - strlen(ltrim($str));
} }
public function cannotExecuteSchema() { public function cannotExecuteSchema()
{
throw new Error( throw new Error(
'Generated Schema cannot use Interface or Union types for execution.' 'Generated Schema cannot use Interface or Union types for execution.'
); );

View File

@ -211,7 +211,7 @@ class TypeInfo
} }
if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) {
foreach ((array) $type->getFields() as $fieldName => $field) { foreach ((array) $type->getFields() as $fieldName => $field) {
if (isset($field->args)) { if (!empty($field->args)) {
$fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args);
$nestedTypes = array_merge($nestedTypes, $fieldArgTypes); $nestedTypes = array_merge($nestedTypes, $fieldArgTypes);
} }

View File

@ -2,6 +2,11 @@
namespace GraphQL\Tests\Utils; namespace GraphQL\Tests\Utils;
use GraphQL\GraphQL; use GraphQL\GraphQL;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Utils\BuildSchema; use GraphQL\Utils\BuildSchema;
use GraphQL\Utils\SchemaPrinter; use GraphQL\Utils\SchemaPrinter;
@ -134,7 +139,7 @@ type Hello {
} }
'; ';
$output = $this->cycleOutput($body); $output = $this->cycleOutput($body);
$this->assertEquals($output, $body); $this->assertEquals($body, $output);
} }
/** /**
@ -761,7 +766,8 @@ type Hello {
} }
'; ';
$doc = Parser::parse($body); $doc = Parser::parse($body);
BuildSchema::buildAST($doc); $schema = BuildSchema::buildAST($doc);
$schema->getTypeMap();
} }
/** /**
@ -778,7 +784,8 @@ schema {
type Hello implements Bar { } type Hello implements Bar { }
'; ';
$doc = Parser::parse($body); $doc = Parser::parse($body);
BuildSchema::buildAST($doc); $schema = BuildSchema::buildAST($doc);
$schema->getTypeMap();
} }
/** /**
@ -796,7 +803,8 @@ union TestUnion = Bar
type Hello { testUnion: TestUnion } type Hello { testUnion: TestUnion }
'; ';
$doc = Parser::parse($body); $doc = Parser::parse($body);
BuildSchema::buildAST($doc); $schema = BuildSchema::buildAST($doc);
$schema->getTypeMap();
} }
/** /**
@ -920,4 +928,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($node, $defaultConfig, $allNodesMap) use (&$decorated, &$calls) {
$decorated[] = $node->name->value;
$calls[] = [$node, $defaultConfig, $allNodesMap];
return ['description' => 'My description of ' . $node->name->value] + $defaultConfig;
};
$schema = BuildSchema::buildAST($doc, $typeConfigDecorator);
$schema->getTypeMap();
$this->assertEquals(['Query', 'Color', 'Hello'], $decorated);
list($node, $defaultConfig, $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(4, $defaultConfig);
$this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']);
$this->assertEquals('My description of Query', $schema->getType('Query')->description);
list($node, $defaultConfig, $allNodesMap) = $calls[1];
$this->assertInstanceOf(EnumTypeDefinitionNode::class, $node);
$this->assertEquals('Color', $defaultConfig['name']);
$enumValue = [
'description' => '',
'deprecationReason' => ''
];
$this->assertEquals([
'RED' => $enumValue,
'GREEN' => $enumValue,
'BLUE' => $enumValue,
], $defaultConfig['values']);
$this->assertCount(3, $defaultConfig);
$this->assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']);
$this->assertEquals('My description of Color', $schema->getType('Color')->description);
list($node, $defaultConfig, $allNodesMap) = $calls[2];
$this->assertInstanceOf(InterfaceTypeDefinitionNode::class, $node);
$this->assertEquals('Hello', $defaultConfig['name']);
$this->assertInstanceOf(\Closure::class, $defaultConfig['fields']);
$this->assertInstanceOf(\Closure::class, $defaultConfig['resolveType']);
$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($node, $config) 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);
}
} }