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); $type = Type::resolve($this->ofType);
@ -40,7 +42,7 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
'Cannot nest NonNull inside NonNull' '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 $_config;
private $_initialized = false;
/** /**
* @var callable * @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'); 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 // 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 // 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, 'name' => Config::STRING | Config::REQUIRED,
'fields' => Config::arrayOf( 'fields' => Config::arrayOf(
FieldDefinition::getDefinition(), FieldDefinition::getDefinition(),
@ -118,10 +89,15 @@ class ObjectType extends Type implements OutputType, CompositeType
'resolveField' => Config::CALLBACK 'resolveField' => Config::CALLBACK
]); ]);
$this->_fields = FieldDefinition::createMap($config['fields']); $this->name = $config['name'];
$this->_interfaces = isset($config['interfaces']) ? $config['interfaces'] : []; $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->_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() public function getFields()
{ {
if (false === $this->_initialized) { if (null === $this->_fields) {
$this->initialize(); $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; return $this->_fields;
} }
@ -142,8 +120,8 @@ class ObjectType extends Type implements OutputType, CompositeType
*/ */
public function getField($name) public function getField($name)
{ {
if (false === $this->_initialized) { if (null === $this->_fields) {
$this->initialize(); $this->getFields();
} }
Utils::invariant(isset($this->_fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); Utils::invariant(isset($this->_fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
return $this->_fields[$name]; return $this->_fields[$name];
@ -154,8 +132,10 @@ class ObjectType extends Type implements OutputType, CompositeType
*/ */
public function getInterfaces() public function getInterfaces()
{ {
if (false === $this->_initialized) { if (null === $this->_interfaces) {
$this->initialize(); $interfaces = isset($this->_config['interfaces']) ? $this->_config['interfaces'] : [];
$interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces;
$this->_interfaces = $interfaces;
} }
return $this->_interfaces; return $this->_interfaces;
} }

View File

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

View File

@ -342,4 +342,66 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
} }
Config::disableValidation(); 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));
}
} }