diff --git a/src/Type/Definition/Behavior/FieldsTrait.php b/src/Type/Definition/Behavior/FieldsTrait.php index be0a23eb..c3d148e9 100644 --- a/src/Type/Definition/Behavior/FieldsTrait.php +++ b/src/Type/Definition/Behavior/FieldsTrait.php @@ -2,10 +2,8 @@ namespace Digia\GraphQL\Type\Definition\Behavior; -use Digia\GraphQL\Type\Definition\Contract\NamedTypeInterface; -use Digia\GraphQL\Type\Definition\Contract\TypeInterface; use Digia\GraphQL\Type\Definition\Field; -use function Digia\GraphQL\Util\instantiateAssocFromArray; +use function Digia\GraphQL\Type\resolveThunk; use function Digia\GraphQL\Util\invariant; trait FieldsTrait @@ -72,16 +70,30 @@ protected function defineFields() * @return array * @throws \Exception */ -function defineFieldMap($type, $fields): array +function defineFieldMap($type, $fieldsThunk): array { - if (is_callable($fields)) { - $fields = $fields(); - } + $fields = resolveThunk($fieldsThunk) ?: []; invariant( is_array($fields), sprintf('%s fields must be an array or a callable which returns such an array.', $type->getName()) ); - return instantiateAssocFromArray(Field::class, $fields); + $fieldMap = []; + + foreach ($fields as $fieldName => $fieldConfig) { + invariant( + is_array($fieldConfig), + sprintf('%s.%s field config must be an object', $type->getName(), $fieldName) + ); + + invariant( + !isset($fieldConfig['isDeprecated']), + sprintf('%s.%s should provide "deprecationReason" instead of "isDeprecated".', $type->getName(), $fieldName) + ); + + $fieldMap[$fieldName] = new Field(array_merge($fieldConfig, ['name' => $fieldName])); + } + + return $fieldMap; } diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index f4496373..d9a2084a 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -12,16 +12,15 @@ use Digia\GraphQL\Type\Definition\Contract\CompositeTypeInterface; use Digia\GraphQL\Type\Definition\Contract\OutputTypeInterface; use Digia\GraphQL\Type\Definition\Contract\TypeInterface; +use function Digia\GraphQL\Type\resolveThunk; +use function Digia\GraphQL\Util\invariant; /** * Union Type Definition - * * When a field can return one of a heterogeneous set of types, a Union type * is used to describe what types are possible as well as providing a function * to determine which type is actually used when the field is resolved. - * * Example: - * * const PetType = new GraphQLUnionType({ * name: 'Pet', * types: [ DogType, CatType ], @@ -34,7 +33,6 @@ * } * } * }); - * */ /** @@ -53,10 +51,15 @@ class UnionType implements AbstractTypeInterface, CompositeTypeInterface, Output use ConfigTrait; /** - * @var TypeInterface[] + * @var array|callable */ private $types; + /** + * @var TypeInterface[] + */ + private $_types; + /** * @inheritdoc */ @@ -67,33 +70,51 @@ protected function beforeConfig(): void /** * @return TypeInterface[] + * @throws \Exception */ public function getTypes(): array { - return $this->types; + $this->defineTypes(); + + return $this->_types; } /** - * @param TypeInterface $type + * @param array|callable $types * @return $this */ - public function addType(TypeInterface $type) + protected function setTypes($types) { - $this->types[] = $type; + $this->types = $types; return $this; } /** - * @param TypeInterface[] $types - * @return $this + * @throws \Exception */ - protected function setTypes(array $types) + protected function defineTypes() { - foreach ($types as $type) { - $this->addType($type); + if ($this->_types === null) { + $this->_types = defineTypes($this, $this->types); } - - return $this; } } + +/** + * @param UnionType $type + * @param mixed $typesThunk + * @return array + * @throws \Exception + */ +function defineTypes(UnionType $type, $typesThunk): array +{ + $types = resolveThunk($typesThunk) ?: []; + + invariant( + is_array($types), + sprintf('Must provide Array of types or a function which returns such an array for Union %s.', $type->getName()) + ); + + return $types; +} diff --git a/src/Type/functions.php b/src/Type/functions.php index 19a1c30b..b0f66191 100644 --- a/src/Type/functions.php +++ b/src/Type/functions.php @@ -32,7 +32,14 @@ use function Digia\GraphQL\Util\invariant; use function Digia\GraphQL\Util\toString; - +/** + * @param $thunk + * @return array + */ +function resolveThunk($thunk): array +{ + return is_callable($thunk) ? $thunk() : $thunk; +} /** * @param $type diff --git a/src/Util/functions.php b/src/Util/functions.php index c2da67f4..4cfab312 100644 --- a/src/Util/functions.php +++ b/src/Util/functions.php @@ -38,10 +38,6 @@ function instantiateFromArray(string $className, array $array): array function instantiateAssocFromArray(string $className, array $array, string $keyName = 'name'): array { $objects = array_map(function ($item, $key) use ($className, $keyName) { - if ($item instanceof $className) { - return $item; - } - return new $className(array_merge($item, [$keyName => $key])); }, $array, array_keys($array)); diff --git a/tests/Functional/Type/DefinitionTest.php b/tests/Functional/Type/DefinitionTest.php index fb4d5312..eef98176 100644 --- a/tests/Functional/Type/DefinitionTest.php +++ b/tests/Functional/Type/DefinitionTest.php @@ -421,6 +421,7 @@ public function testIdentifiesInputTypes($type, $answer) public function identifiesInputTypesDataProvider(): array { + // We cannot use the class fields here because they do not get instantiated for data providers. return [ [GraphQLInt(), true], [GraphQLObjectType(), true], @@ -438,4 +439,83 @@ public function testProhibitsNestingNonNullInsideNonNull() { GraphQLNonNull(GraphQLNonNull(GraphQLInt())); } + + /** + * @throws \Exception + */ + public function testAllowsAThunkForUnionMemberTypes() + { + $union = GraphQLUnionType([ + 'name' => 'ThunkUnion', + 'types' => function () { + return [$this->objectType]; + } + ]); + + $types = $union->getTypes(); + + $this->assertEquals(1, count($types)); + $this->assertEquals($this->objectType, $types[0]); + } + + /** + * @throws \Exception + */ + public function testDoesNotMutatePassedFieldDefinitions() + { + $fields = [ + 'field1' => ['type' => GraphQLString()], + 'field2' => [ + 'type' => GraphQLString(), + 'args' => [ + 'id' => ['type' => GraphQLString()], + ], + ], + ]; + + $testObject1 = GraphQLObjectType([ + 'name' => 'Test1', + 'fields' => $fields, + ]); + + $testObject2 = GraphQLObjectType([ + 'name' => 'Test2', + 'fields' => $fields, + ]); + + $this->assertEquals($testObject2->getFields(), $testObject1->getFields()); + + $testInputObject1 = GraphQLInputObjectType([ + 'name' => 'Test1', + 'fields' => $fields, + ]); + + $testInputObject2 = GraphQLInputObjectType([ + 'name' => 'Test2', + 'fields' => $fields, + ]); + + $this->assertEquals($testInputObject2->getFields(), $testInputObject1->getFields()); + + $this->assertInstanceOf(StringType::class, $fields['field1']['type']); + $this->assertInstanceOf(StringType::class, $fields['field2']['type']); + $this->assertInstanceOf(StringType::class, $fields['field2']['args']['id']['type']); + } + + // TODO: Assess if we want to test 'accepts an Object type with a field function'. + + /** + * @expectedException \Exception + */ + public function testRejectsFieldWithNullConfig() + { + $objType = GraphQLObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'f' => null, + ], + ]); + + $objType->getFields(); + } }