diff --git a/src/Type/Definition/ListOfType.php b/src/Type/Definition/ListOfType.php index 860ee58..f1ae5ed 100644 --- a/src/Type/Definition/ListOfType.php +++ b/src/Type/Definition/ListOfType.php @@ -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; } } diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index 75bd561..5f9b5b3 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -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; } /** diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index de19237..9275820 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -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; } diff --git a/src/Type/Definition/WrappingType.php b/src/Type/Definition/WrappingType.php index 0fd02e4..1f275bc 100644 --- a/src/Type/Definition/WrappingType.php +++ b/src/Type/Definition/WrappingType.php @@ -8,5 +8,5 @@ interface WrappingType NonNullType ListOfType */ - public function getWrappedType(); + public function getWrappedType($recurse = false); } diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index cbdc43c..2e6ea77 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -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)); + + } }