mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 06:16:05 +03:00
Avoid infinite loop when using recursive types and interfaces (#16)
This commit is contained in:
parent
4591840ec7
commit
98e5835620
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -8,5 +8,5 @@ interface WrappingType
|
||||
NonNullType
|
||||
ListOfType
|
||||
*/
|
||||
public function getWrappedType();
|
||||
public function getWrappedType($recurse = false);
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user