Avoid infinite loop when using recursive types and interfaces (#16)

This commit is contained in:
vladar 2016-01-01 19:16:25 +06:00
parent 4591840ec7
commit 98e5835620
5 changed files with 92 additions and 46 deletions

View File

@ -34,10 +34,12 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType
}
/**
* @return Type
* @param bool $recurse
* @return mixed
*/
public function getWrappedType()
public function getWrappedType($recurse = false)
{
return Type::resolve($this->ofType);
$type = Type::resolve($this->ofType);
return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type;
}
}

View File

@ -29,9 +29,11 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
}
/**
* @return Type|callable
* @param bool $recurse
* @return Type
* @throws \Exception
*/
public function getWrappedType()
public function getWrappedType($recurse = false)
{
$type = Type::resolve($this->ofType);
@ -40,7 +42,7 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
'Cannot nest NonNull inside NonNull'
);
return $type;
return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type;
}
/**

View File

@ -64,8 +64,6 @@ class ObjectType extends Type implements OutputType, CompositeType
*/
private $_config;
private $_initialized = false;
/**
* @var callable
*/
@ -75,36 +73,9 @@ class ObjectType extends Type implements OutputType, CompositeType
{
Utils::invariant(!empty($config['name']), 'Every type is expected to have name');
$this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null;
$this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null;
$this->_config = $config;
if (isset($config['interfaces'])) {
InterfaceType::addImplementationToInterfaces($this);
}
}
/**
* Late instance initialization
*/
private function initialize()
{
if ($this->_initialized) {
return ;
}
$config = $this->_config;
if (isset($config['fields']) && is_callable($config['fields'])) {
$config['fields'] = call_user_func($config['fields']);
}
if (isset($config['interfaces']) && is_callable($config['interfaces'])) {
$config['interfaces'] = call_user_func($config['interfaces']);
}
// Note: this validation is disabled by default, because it is resource-consuming
// TODO: add bin/validate script to check if schema is valid during development
Config::validate($this->_config, [
Config::validate($config, [
'name' => Config::STRING | Config::REQUIRED,
'fields' => Config::arrayOf(
FieldDefinition::getDefinition(),
@ -118,10 +89,15 @@ class ObjectType extends Type implements OutputType, CompositeType
'resolveField' => Config::CALLBACK
]);
$this->_fields = FieldDefinition::createMap($config['fields']);
$this->_interfaces = isset($config['interfaces']) ? $config['interfaces'] : [];
$this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null;
$this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null;
$this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null;
$this->_initialized = true;
$this->_config = $config;
if (isset($config['interfaces'])) {
InterfaceType::addImplementationToInterfaces($this);
}
}
/**
@ -129,8 +105,10 @@ class ObjectType extends Type implements OutputType, CompositeType
*/
public function getFields()
{
if (false === $this->_initialized) {
$this->initialize();
if (null === $this->_fields) {
$fields = isset($this->_config['fields']) ? $this->_config['fields'] : [];
$fields = is_callable($fields) ? call_user_func($fields) : $fields;
$this->_fields = FieldDefinition::createMap($fields);
}
return $this->_fields;
}
@ -142,8 +120,8 @@ class ObjectType extends Type implements OutputType, CompositeType
*/
public function getField($name)
{
if (false === $this->_initialized) {
$this->initialize();
if (null === $this->_fields) {
$this->getFields();
}
Utils::invariant(isset($this->_fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
return $this->_fields[$name];
@ -154,8 +132,10 @@ class ObjectType extends Type implements OutputType, CompositeType
*/
public function getInterfaces()
{
if (false === $this->_initialized) {
$this->initialize();
if (null === $this->_interfaces) {
$interfaces = isset($this->_config['interfaces']) ? $this->_config['interfaces'] : [];
$interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces;
$this->_interfaces = $interfaces;
}
return $this->_interfaces;
}

View File

@ -8,5 +8,5 @@ interface WrappingType
NonNullType
ListOfType
*/
public function getWrappedType();
public function getWrappedType($recurse = false);
}

View File

@ -342,4 +342,66 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
}
Config::disableValidation();
}
public function testAllowsRecursiveDefinitions()
{
// See https://github.com/webonyx/graphql-php/issues/16
$node = new InterfaceType([
'name' => 'Node',
'fields' => [
'id' => ['type' => Type::nonNull(Type::id())]
]
]);
$blog = null;
$called = false;
$user = new ObjectType([
'name' => 'User',
'fields' => function() use (&$blog, &$called) {
$this->assertNotNull($blog, 'Blog type is expected to be defined at this point, but it is null');
$called = true;
return [
'id' => ['type' => Type::nonNull(Type::id())],
'blogs' => ['type' => Type::nonNull(Type::listOf(Type::nonNull($blog)))]
];
},
'interfaces' => function() use ($node) {
return [$node];
}
]);
$blog = new ObjectType([
'name' => 'Blog',
'fields' => function() use ($user) {
return [
'id' => ['type' => Type::nonNull(Type::id())],
'owner' => ['type' => Type::nonNull($user)]
];
},
'interfaces' => function() use ($node) {
return [$node];
}
]);
$schema = new Schema(new ObjectType([
'name' => 'Query',
'fields' => [
'node' => ['type' => $node]
]
]));
$this->assertTrue($called);
$this->assertEquals([$node], $blog->getInterfaces());
$this->assertEquals([$node], $user->getInterfaces());
$this->assertNotNull($user->getField('blogs'));
$this->assertSame($blog, $user->getField('blogs')->getType()->getWrappedType(true));
$this->assertNotNull($blog->getField('owner'));
$this->assertSame($user, $blog->getField('owner')->getType()->getWrappedType(true));
}
}