mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-29 00:25:17 +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);
|
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,5 @@ interface WrappingType
|
|||||||
NonNullType
|
NonNullType
|
||||||
ListOfType
|
ListOfType
|
||||||
*/
|
*/
|
||||||
public function getWrappedType();
|
public function getWrappedType($recurse = false);
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user