mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-02-11 18:19:23 +03:00
Remove duplicated code from buildASTSchema and extendSchema
ref: graphql/graphql-js#1000 BREAKING CHANGE: SchemaBuilder::build() and buildAST() and constructor removed the typedecorator, as not needed anymore as library can now resolve union and interfaces from generated schemas.
This commit is contained in:
parent
48c33302a8
commit
2cbccb87db
437
src/Utils/ASTDefinitionBuilder.php
Normal file
437
src/Utils/ASTDefinitionBuilder.php
Normal file
@ -0,0 +1,437 @@
|
||||
<?php
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Executor\Values;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ListTypeNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NonNullTypeNode;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeNode;
|
||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||
use GraphQL\Language\Token;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
|
||||
class ASTDefinitionBuilder
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $typeDefintionsMap;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $resolveType;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
public function __construct(array $typeDefintionsMap, $options, callable $resolveType)
|
||||
{
|
||||
$this->typeDefintionsMap = $typeDefintionsMap;
|
||||
$this->options = $options;
|
||||
$this->resolveType = $resolveType;
|
||||
|
||||
$this->cache = [
|
||||
'String' => Type::string(),
|
||||
'Int' => Type::int(),
|
||||
'Float' => Type::float(),
|
||||
'Boolean' => Type::boolean(),
|
||||
'ID' => Type::id(),
|
||||
'__Schema' => Introspection::_schema(),
|
||||
'__Directive' => Introspection::_directive(),
|
||||
'__DirectiveLocation' => Introspection::_directiveLocation(),
|
||||
'__Type' => Introspection::_type(),
|
||||
'__Field' => Introspection::_field(),
|
||||
'__InputValue' => Introspection::_inputValue(),
|
||||
'__EnumValue' => Introspection::_enumValue(),
|
||||
'__TypeKind' => Introspection::_typeKind(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $innerType
|
||||
* @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||
* @return Type
|
||||
*/
|
||||
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
|
||||
{
|
||||
if ($inputTypeNode->kind == NodeKind::LIST_TYPE) {
|
||||
return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
|
||||
}
|
||||
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
|
||||
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
|
||||
Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.');
|
||||
return Type::nonNull($wrappedType);
|
||||
}
|
||||
return $innerType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode
|
||||
* @return TypeNode
|
||||
*/
|
||||
private function getNamedTypeNode(TypeNode $typeNode)
|
||||
{
|
||||
$namedType = $typeNode;
|
||||
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
|
||||
$namedType = $namedType->type;
|
||||
}
|
||||
return $namedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $typeName
|
||||
* @param NamedTypeNode|null $typeNode
|
||||
* @return Type
|
||||
* @throws Error
|
||||
*/
|
||||
private function internalBuildType($typeName, $typeNode = null) {
|
||||
if (!isset($this->cache[$typeName])) {
|
||||
if (isset($this->typeDefintionsMap[$typeName])) {
|
||||
$this->cache[$typeName] = $this->makeSchemaDef($this->typeDefintionsMap[$typeName]);
|
||||
} else {
|
||||
$fn = $this->resolveType;
|
||||
$this->cache[$typeName] = $fn($typeName, $typeNode);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cache[$typeName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|NamedTypeNode $ref
|
||||
* @return Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildType($ref)
|
||||
{
|
||||
if (is_string($ref)) {
|
||||
return $this->internalBuildType($ref);
|
||||
}
|
||||
|
||||
return $this->internalBuildType($ref->name->value, $ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode $typeNode
|
||||
* @return InputType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildInputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->internalBuildWrappedType($typeNode);
|
||||
Utils::invariant(Type::isInputType($type), 'Expected Input type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode $typeNode
|
||||
* @return OutputType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildOutputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->internalBuildWrappedType($typeNode);
|
||||
Utils::invariant(Type::isOutputType($type), 'Expected Output type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode|string $typeNode
|
||||
* @return ObjectType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildObjectType($typeNode)
|
||||
{
|
||||
$type = $this->buildType($typeNode);
|
||||
Utils::invariant($type instanceof ObjectType, 'Expected Object type.' . get_class($type));
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode|string $typeNode
|
||||
* @return InterfaceType|Type
|
||||
* @throws Error
|
||||
*/
|
||||
public function buildInterfaceType($typeNode)
|
||||
{
|
||||
$type = $this->buildType($typeNode);
|
||||
Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TypeNode $typeNode
|
||||
* @return Type
|
||||
* @throws Error
|
||||
*/
|
||||
private function internalBuildWrappedType(TypeNode $typeNode)
|
||||
{
|
||||
$typeDef = $this->buildType($this->getNamedTypeNode($typeNode));
|
||||
return $this->buildWrappedType($typeDef, $typeNode);
|
||||
}
|
||||
|
||||
public function buildDirective(DirectiveDefinitionNode $directiveNode)
|
||||
{
|
||||
return new Directive([
|
||||
'name' => $directiveNode->name->value,
|
||||
'description' => $this->getDescription($directiveNode),
|
||||
'locations' => Utils::map($directiveNode->locations, function ($node) {
|
||||
return $node->value;
|
||||
}),
|
||||
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
|
||||
'astNode' => $directiveNode,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildField(FieldDefinitionNode $field)
|
||||
{
|
||||
return [
|
||||
'type' => $this->buildOutputType($field->type),
|
||||
'description' => $this->getDescription($field),
|
||||
'args' => $this->makeInputValues($field->arguments),
|
||||
'deprecationReason' => $this->getDeprecationReason($field),
|
||||
'astNode' => $field
|
||||
];
|
||||
}
|
||||
|
||||
private function makeSchemaDef($def)
|
||||
{
|
||||
if (!$def) {
|
||||
throw new Error('def must be defined.');
|
||||
}
|
||||
switch ($def->kind) {
|
||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||
return $this->makeTypeDef($def);
|
||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||
return $this->makeInterfaceDef($def);
|
||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||
return $this->makeEnumDef($def);
|
||||
case NodeKind::UNION_TYPE_DEFINITION:
|
||||
return $this->makeUnionDef($def);
|
||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||
return $this->makeScalarDef($def);
|
||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||
return $this->makeInputObjectDef($def);
|
||||
default:
|
||||
throw new Error("Type kind of {$def->kind} not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private function makeTypeDef(ObjectTypeDefinitionNode $def)
|
||||
{
|
||||
$typeName = $def->name->value;
|
||||
return new ObjectType([
|
||||
'name' => $typeName,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'interfaces' => function () use ($def) {
|
||||
return $this->makeImplementedInterfaces($def);
|
||||
},
|
||||
'astNode' => $def
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeFieldDefMap($def)
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$def->fields,
|
||||
function ($field) {
|
||||
return $field->name->value;
|
||||
},
|
||||
function ($field) {
|
||||
return $this->buildField($field);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
||||
{
|
||||
if (isset($def->interfaces)) {
|
||||
return Utils::map($def->interfaces, function ($iface) {
|
||||
return $this->buildInterfaceType($iface);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function makeInputValues($values)
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$values,
|
||||
function ($value) {
|
||||
return $value->name->value;
|
||||
},
|
||||
function ($value) {
|
||||
$type = $this->buildInputType($value->type);
|
||||
$config = [
|
||||
'name' => $value->name->value,
|
||||
'type' => $type,
|
||||
'description' => $this->getDescription($value),
|
||||
'astNode' => $value
|
||||
];
|
||||
if (isset($value->defaultValue)) {
|
||||
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeInterfaceDef(InterfaceTypeDefinitionNode $def)
|
||||
{
|
||||
$typeName = $def->name->value;
|
||||
return new InterfaceType([
|
||||
'name' => $typeName,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'astNode' => $def
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeEnumDef(EnumTypeDefinitionNode $def)
|
||||
{
|
||||
return new EnumType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'astNode' => $def,
|
||||
'values' => Utils::keyValMap(
|
||||
$def->values,
|
||||
function ($enumValue) {
|
||||
return $enumValue->name->value;
|
||||
},
|
||||
function ($enumValue) {
|
||||
return [
|
||||
'description' => $this->getDescription($enumValue),
|
||||
'deprecationReason' => $this->getDeprecationReason($enumValue),
|
||||
'astNode' => $enumValue
|
||||
];
|
||||
}
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeUnionDef(UnionTypeDefinitionNode $def)
|
||||
{
|
||||
return new UnionType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'types' => Utils::map($def->types, function ($typeNode) {
|
||||
return $this->buildObjectType($typeNode);
|
||||
}),
|
||||
'astNode' => $def
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeScalarDef(ScalarTypeDefinitionNode $def)
|
||||
{
|
||||
return new CustomScalarType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'astNode' => $def,
|
||||
'serialize' => function($value) {
|
||||
return $value;
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeInputObjectDef(InputObjectTypeDefinitionNode $def)
|
||||
{
|
||||
return new InputObjectType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) {
|
||||
return $this->makeInputValues($def->fields);
|
||||
},
|
||||
'astNode' => $def,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a collection of directives, returns the string value for the
|
||||
* deprecation reason.
|
||||
*
|
||||
* @param EnumValueDefinitionNode | FieldDefinitionNode $node
|
||||
* @return string
|
||||
*/
|
||||
private function getDeprecationReason($node)
|
||||
{
|
||||
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
|
||||
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ast node, returns its string description.
|
||||
*/
|
||||
private function getDescription($node)
|
||||
{
|
||||
if ($node->description) {
|
||||
return $node->description->value;
|
||||
}
|
||||
if (isset($this->options['commentDescriptions'])) {
|
||||
$rawValue = $this->getLeadingCommentBlock($node);
|
||||
if ($rawValue !== null) {
|
||||
return BlockString::value("\n" . $rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getLeadingCommentBlock($node)
|
||||
{
|
||||
$loc = $node->loc;
|
||||
if (!$loc || !$loc->startToken) {
|
||||
return;
|
||||
}
|
||||
$comments = [];
|
||||
$token = $loc->startToken->prev;
|
||||
while (
|
||||
$token &&
|
||||
$token->kind === Token::COMMENT &&
|
||||
$token->next && $token->prev &&
|
||||
$token->line + 1 === $token->next->line &&
|
||||
$token->line !== $token->prev->line
|
||||
) {
|
||||
$value = $token->value;
|
||||
$comments[] = $value;
|
||||
$token = $token->prev;
|
||||
}
|
||||
|
||||
return implode("\n", array_reverse($comments));
|
||||
}
|
||||
}
|
@ -2,35 +2,13 @@
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Executor\Values;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeNode;
|
||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\Token;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
|
||||
/**
|
||||
* Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
|
||||
@ -38,33 +16,6 @@ use GraphQL\Type\Introspection;
|
||||
*/
|
||||
class BuildSchema
|
||||
{
|
||||
/**
|
||||
* @param Type $innerType
|
||||
* @param TypeNode $inputTypeNode
|
||||
* @return Type
|
||||
*/
|
||||
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
|
||||
{
|
||||
if ($inputTypeNode->kind == NodeKind::LIST_TYPE) {
|
||||
return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
|
||||
}
|
||||
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
|
||||
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
|
||||
Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.');
|
||||
return Type::nonNull($wrappedType);
|
||||
}
|
||||
return $innerType;
|
||||
}
|
||||
|
||||
private function getNamedTypeNode(TypeNode $typeNode)
|
||||
{
|
||||
$namedType = $typeNode;
|
||||
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
|
||||
$namedType = $namedType->type;
|
||||
}
|
||||
return $namedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* This takes the ast of a schema document produced by the parse function in
|
||||
* GraphQL\Language\Parser.
|
||||
@ -75,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 third argument:
|
||||
* Accepts options as a second argument:
|
||||
*
|
||||
* - commentDescriptions:
|
||||
* Provide true to use preceding comments as the description.
|
||||
@ -83,27 +34,24 @@ class BuildSchema
|
||||
*
|
||||
* @api
|
||||
* @param DocumentNode $ast
|
||||
* @param callable $typeConfigDecorator
|
||||
* @param array $options
|
||||
* @return Schema
|
||||
* @throws Error
|
||||
*/
|
||||
public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = [])
|
||||
public static function buildAST(DocumentNode $ast, array $options = [])
|
||||
{
|
||||
$builder = new self($ast, $typeConfigDecorator, $options);
|
||||
$builder = new self($ast, $options);
|
||||
return $builder->buildSchema();
|
||||
}
|
||||
|
||||
private $ast;
|
||||
private $innerTypeMap;
|
||||
private $nodeMap;
|
||||
private $typeConfigDecorator;
|
||||
private $loadedTypeDefs;
|
||||
private $options;
|
||||
|
||||
public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = [])
|
||||
public function __construct(DocumentNode $ast, array $options = [])
|
||||
{
|
||||
$this->ast = $ast;
|
||||
$this->typeConfigDecorator = $typeConfigDecorator;
|
||||
$this->loadedTypeDefs = [];
|
||||
$this->options = $options;
|
||||
}
|
||||
@ -150,23 +98,15 @@ class BuildSchema
|
||||
'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
|
||||
];
|
||||
|
||||
$this->innerTypeMap = [
|
||||
'String' => Type::string(),
|
||||
'Int' => Type::int(),
|
||||
'Float' => Type::float(),
|
||||
'Boolean' => Type::boolean(),
|
||||
'ID' => Type::id(),
|
||||
'__Schema' => Introspection::_schema(),
|
||||
'__Directive' => Introspection::_directive(),
|
||||
'__DirectiveLocation' => Introspection::_directiveLocation(),
|
||||
'__Type' => Introspection::_type(),
|
||||
'__Field' => Introspection::_field(),
|
||||
'__InputValue' => Introspection::_inputValue(),
|
||||
'__EnumValue' => Introspection::_enumValue(),
|
||||
'__TypeKind' => Introspection::_typeKind(),
|
||||
];
|
||||
$defintionBuilder = new ASTDefinitionBuilder(
|
||||
$this->nodeMap,
|
||||
$this->options,
|
||||
function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); }
|
||||
);
|
||||
|
||||
$directives = array_map([$this, 'getDirective'], $directiveDefs);
|
||||
$directives = array_map(function($def) use ($defintionBuilder) {
|
||||
return $defintionBuilder->buildDirective($def);
|
||||
}, $directiveDefs);
|
||||
|
||||
// If specified directives were not explicitly declared, add them.
|
||||
$skip = array_reduce($directives, function ($hasSkip, $directive) {
|
||||
@ -197,23 +137,23 @@ class BuildSchema
|
||||
}
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $this->getObjectType($operationTypes['query']),
|
||||
'query' => $defintionBuilder->buildObjectType($operationTypes['query']),
|
||||
'mutation' => isset($operationTypes['mutation']) ?
|
||||
$this->getObjectType($operationTypes['mutation']) :
|
||||
$defintionBuilder->buildObjectType($operationTypes['mutation']) :
|
||||
null,
|
||||
'subscription' => isset($operationTypes['subscription']) ?
|
||||
$this->getObjectType($operationTypes['subscription']) :
|
||||
$defintionBuilder->buildObjectType($operationTypes['subscription']) :
|
||||
null,
|
||||
'typeLoader' => function ($name) {
|
||||
return $this->typeDefNamed($name);
|
||||
'typeLoader' => function ($name) use ($defintionBuilder) {
|
||||
return $defintionBuilder->buildType($name);
|
||||
},
|
||||
'directives' => $directives,
|
||||
'astNode' => $schemaDef,
|
||||
'types' => function () {
|
||||
'types' => function () use ($defintionBuilder) {
|
||||
$types = [];
|
||||
foreach ($this->nodeMap as $name => $def) {
|
||||
if (!isset($this->loadedTypeDefs[$name])) {
|
||||
$types[] = $this->typeDefNamed($def->name->value);
|
||||
$types[] = $defintionBuilder->buildType($def->name->value);
|
||||
}
|
||||
}
|
||||
return $types;
|
||||
@ -250,377 +190,18 @@ class BuildSchema
|
||||
return $opTypes;
|
||||
}
|
||||
|
||||
private function getDirective(DirectiveDefinitionNode $directiveNode)
|
||||
{
|
||||
return new Directive([
|
||||
'name' => $directiveNode->name->value,
|
||||
'description' => $this->getDescription($directiveNode),
|
||||
'locations' => Utils::map($directiveNode->locations, function ($node) {
|
||||
return $node->value;
|
||||
}),
|
||||
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
|
||||
'astNode' => $directiveNode
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return CustomScalarType|EnumType|InputObjectType|UnionType
|
||||
* @throws Error
|
||||
*/
|
||||
private function getObjectType($name)
|
||||
{
|
||||
$type = $this->typeDefNamed($name);
|
||||
Utils::invariant(
|
||||
$type instanceof ObjectType,
|
||||
'AST must provide object type.'
|
||||
);
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceType(TypeNode $typeNode)
|
||||
{
|
||||
$typeName = $this->getNamedTypeNode($typeNode)->name->value;
|
||||
$typeDef = $this->typeDefNamed($typeName);
|
||||
return $this->buildWrappedType($typeDef, $typeNode);
|
||||
}
|
||||
|
||||
private function produceInputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant(Type::isInputType($type), 'Expected Input type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceOutputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant(Type::isOutputType($type), 'Expected Input type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceObjectType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant($type instanceof ObjectType, 'Expected Object type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceInterfaceType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function typeDefNamed($typeName)
|
||||
{
|
||||
if (isset($this->innerTypeMap[$typeName])) {
|
||||
return $this->innerTypeMap[$typeName];
|
||||
}
|
||||
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error('Type "' . $typeName . '" not found in document.');
|
||||
}
|
||||
|
||||
$this->loadedTypeDefs[$typeName] = true;
|
||||
|
||||
$config = $this->makeSchemaDefConfig($this->nodeMap[$typeName]);
|
||||
|
||||
if ($this->typeConfigDecorator) {
|
||||
$fn = $this->typeConfigDecorator;
|
||||
try {
|
||||
$config = $fn($config, $this->nodeMap[$typeName], $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) {
|
||||
throw new Error("Nothing constructed for $typeName.");
|
||||
}
|
||||
$this->innerTypeMap[$typeName] = $innerTypeDef;
|
||||
return $innerTypeDef;
|
||||
}
|
||||
|
||||
private function makeSchemaDefConfig($def)
|
||||
{
|
||||
if (!$def) {
|
||||
throw new Error('def must be defined.');
|
||||
}
|
||||
switch ($def->kind) {
|
||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||
return $this->makeTypeDefConfig($def);
|
||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||
return $this->makeInterfaceDefConfig($def);
|
||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||
return $this->makeEnumDefConfig($def);
|
||||
case NodeKind::UNION_TYPE_DEFINITION:
|
||||
return $this->makeUnionDefConfig($def);
|
||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||
return $this->makeScalarDefConfig($def);
|
||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||
return $this->makeInputObjectDefConfig($def);
|
||||
default:
|
||||
throw new Error("Type kind of {$def->kind} not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return [
|
||||
'name' => $typeName,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'interfaces' => function () use ($def) {
|
||||
return $this->makeImplementedInterfaces($def);
|
||||
},
|
||||
'astNode' => $def
|
||||
];
|
||||
}
|
||||
|
||||
private function makeFieldDefMap($def)
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$def->fields,
|
||||
function ($field) {
|
||||
return $field->name->value;
|
||||
},
|
||||
function ($field) {
|
||||
return [
|
||||
'type' => $this->produceOutputType($field->type),
|
||||
'description' => $this->getDescription($field),
|
||||
'args' => $this->makeInputValues($field->arguments),
|
||||
'deprecationReason' => $this->getDeprecationReason($field),
|
||||
'astNode' => $field
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
||||
{
|
||||
if (isset($def->interfaces)) {
|
||||
return Utils::map($def->interfaces, function ($iface) {
|
||||
return $this->produceInterfaceType($iface);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function makeInputValues($values)
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$values,
|
||||
function ($value) {
|
||||
return $value->name->value;
|
||||
},
|
||||
function ($value) {
|
||||
$type = $this->produceInputType($value->type);
|
||||
$config = [
|
||||
'name' => $value->name->value,
|
||||
'type' => $type,
|
||||
'description' => $this->getDescription($value),
|
||||
'astNode' => $value
|
||||
];
|
||||
if (isset($value->defaultValue)) {
|
||||
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeInterfaceDefConfig(InterfaceTypeDefinitionNode $def)
|
||||
{
|
||||
$typeName = $def->name->value;
|
||||
return [
|
||||
'name' => $typeName,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'astNode' => $def
|
||||
];
|
||||
}
|
||||
|
||||
private function makeEnumDefConfig(EnumTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'astNode' => $def,
|
||||
'values' => Utils::keyValMap(
|
||||
$def->values,
|
||||
function ($enumValue) {
|
||||
return $enumValue->name->value;
|
||||
},
|
||||
function ($enumValue) {
|
||||
return [
|
||||
'description' => $this->getDescription($enumValue),
|
||||
'deprecationReason' => $this->getDeprecationReason($enumValue),
|
||||
'astNode' => $enumValue
|
||||
];
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
private function makeUnionDefConfig(UnionTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'types' => Utils::map($def->types, function ($typeNode) {
|
||||
return $this->produceObjectType($typeNode);
|
||||
}),
|
||||
'astNode' => $def
|
||||
];
|
||||
}
|
||||
|
||||
private function makeScalarDefConfig(ScalarTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'astNode' => $def,
|
||||
'serialize' => function($value) {
|
||||
return $value;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private function makeInputObjectDefConfig(InputObjectTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) {
|
||||
return $this->makeInputValues($def->fields);
|
||||
},
|
||||
'astNode' => $def,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a collection of directives, returns the string value for the
|
||||
* deprecation reason.
|
||||
*
|
||||
* @param EnumValueDefinitionNode | FieldDefinitionNode $node
|
||||
* @return string
|
||||
*/
|
||||
private function getDeprecationReason($node)
|
||||
{
|
||||
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
|
||||
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ast node, returns its string description.
|
||||
*/
|
||||
public function getDescription($node)
|
||||
{
|
||||
if ($node->description) {
|
||||
return $node->description->value;
|
||||
}
|
||||
if (isset($this->options['commentDescriptions'])) {
|
||||
$rawValue = $this->getLeadingCommentBlock($node);
|
||||
if ($rawValue !== null) {
|
||||
return BlockString::value("\n" . $rawValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getLeadingCommentBlock($node)
|
||||
{
|
||||
$loc = $node->loc;
|
||||
if (!$loc || !$loc->startToken) {
|
||||
return;
|
||||
}
|
||||
$comments = [];
|
||||
$token = $loc->startToken->prev;
|
||||
while (
|
||||
$token &&
|
||||
$token->kind === Token::COMMENT &&
|
||||
$token->next && $token->prev &&
|
||||
$token->line + 1 === $token->next->line &&
|
||||
$token->line !== $token->prev->line
|
||||
) {
|
||||
$value = $token->value;
|
||||
$comments[] = $value;
|
||||
$token = $token->prev;
|
||||
}
|
||||
|
||||
return implode("\n", array_reverse($comments));
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to build a GraphQLSchema directly from a source
|
||||
* document.
|
||||
*
|
||||
* @api
|
||||
* @param DocumentNode|Source|string $source
|
||||
* @param callable $typeConfigDecorator
|
||||
* @param array $options
|
||||
* @return Schema
|
||||
*/
|
||||
public static function build($source, callable $typeConfigDecorator = null)
|
||||
public static function build($source, array $options = [])
|
||||
{
|
||||
$doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
|
||||
return self::buildAST($doc, $typeConfigDecorator);
|
||||
return self::buildAST($doc, $options);
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ class BuildSchemaTest extends \PHPUnit_Framework_TestCase
|
||||
private function cycleOutput($body, $options = [])
|
||||
{
|
||||
$ast = Parser::parse($body);
|
||||
$schema = BuildSchema::buildAST($ast, null, $options);
|
||||
$schema = BuildSchema::buildAST($ast, $options);
|
||||
return "\n" . SchemaPrinter::doPrint($schema, $options);
|
||||
}
|
||||
|
||||
@ -1172,131 +1172,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user