diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index 2ce242f80..d3985a964 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -44,6 +44,10 @@ public function getInnermostType(): NamedType $type = $type->getWrappedType(); } + if ($type instanceof TypeReference) { + $type = Schema::resolveType($type); + } + assert($type instanceof NamedType, 'known because we unwrapped all the way down'); return $type; diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index ad742d7df..588289b1e 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -50,7 +50,7 @@ * ]); * * @phpstan-import-type FieldResolver from Executor - * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType + * @phpstan-type InterfaceTypeReference TypeReference|InterfaceType|callable(): TypeReference|InterfaceType * @phpstan-type ObjectConfig array{ * name?: string|null, * description?: string|null, diff --git a/src/Type/Definition/TypeReference.php b/src/Type/Definition/TypeReference.php new file mode 100644 index 000000000..374f1120f --- /dev/null +++ b/src/Type/Definition/TypeReference.php @@ -0,0 +1,32 @@ +name = $name; + } + + public function assertValid() : void + { + + } + + public function isBuiltInType() : bool + { + + } + + public function toString() : string + { + return $this->name; + } +} diff --git a/src/Type/Schema.php b/src/Type/Schema.php index bbe64bdc0..da99b1712 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -15,6 +15,7 @@ use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\TypeReference; use GraphQL\Type\Definition\UnionType; use GraphQL\Utils\InterfaceImplementations; use GraphQL\Utils\TypeInfo; @@ -210,6 +211,20 @@ private function collectAllTypes(): array return $typeMap; } + /** + * @param array $typeMap + */ + private function resolveTypeReference(Type $type, array &$typeMap): Type + { + if (!$type instanceof TypeReference) { + return $type; + } + + assert(isset($typeMap[$type->name]), sprintf('Type reference %s not found in type map', $type->name)); + + return $this->resolveTypeReference($typeMap[$type->name], $typeMap); + } + /** * Returns a list of directives supported by this schema. * @@ -312,6 +327,13 @@ public function getType(string $name): ?Type return $this->resolvedTypes[$name] = self::resolveType($type); } + if ($this->resolvedTypes[$name] instanceof TypeReference) { + unset($this->resolvedTypes[$name]); + + return $this->getType($name); + } + + return $this->resolvedTypes[$name]; } diff --git a/tests/Type/TypeReferenceTypeLoaderTest.php b/tests/Type/TypeReferenceTypeLoaderTest.php new file mode 100644 index 000000000..f7b8ab13c --- /dev/null +++ b/tests/Type/TypeReferenceTypeLoaderTest.php @@ -0,0 +1,117 @@ +types = [ + 'Node' => new InterfaceType([ + 'name' => 'Node', + 'fields' => [ + 'id' => Type::string(), + ], + 'resolveType' => static fn() : ?ObjectType => null, + ]), + 'Content' => new InterfaceType([ + 'name' => 'Content', + 'fields' => [ + 'title' => Type::string(), + 'body' => Type::string(), + ], + 'resolveType' => static fn() : ?ObjectType => null, + ]), + 'BlogStory' => new ObjectType([ + 'name' => 'BlogStory', + 'interfaces' => [ + new TypeReference('Node'), + new TypeReference('Content'), + ], + 'fields' => [ + 'id' => Type::string(), + 'title' => Type::string(), + 'body' => Type::string(), + ], + ]), + 'Query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'latestContent' => new TypeReference('Content'), + 'node' => new TypeReference('Node'), + ], + ]), + 'Mutation' => new ObjectType([ + 'name' => 'Mutation', + 'fields' => [ + 'postStory' => [ + 'type' => new TypeReference('PostStoryMutation'), + 'args' => [ + 'input' => Type::nonNull(new TypeReference('PostStoryMutationInput')), + 'clientRequestId' => Type::string(), + ], + ], + ], + ]), + 'PostStoryMutation' => new ObjectType([ + 'name' => 'PostStoryMutation', + 'fields' => [ + 'story' => new TypeReference('BlogStory'), + ], + ]), + 'PostStoryMutationInput' => new InputObjectType([ + 'name' => 'PostStoryMutationInput', + 'fields' => [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => Type::id(), + 'category' => Type::id(), + ], + ]), + ]; + } + + public function testWorksWithTypeLoader(): void + { + $schema = new Schema([ + 'query' => $this->types['Query'], + 'mutation' => $this->types['Mutation'], + 'typeLoader' => fn (string $name) => $this->types[$name] ?? null, + ]); + + $node = $schema->getType('Node'); + self::assertSame($this->types['Node'], $node); + + $content = $schema->getType('Content'); + self::assertSame($this->types['Content'], $content); + + $blogStory = $schema->getType('BlogStory'); + self::assertSame($this->types['BlogStory'], $blogStory); + self::assertInstanceOf(ObjectType::class, $blogStory); + self::assertCount(2, $blogStory->getInterfaces()); + + $postStoryMutation = $schema->getType('PostStoryMutation'); + self::assertSame($this->types['PostStoryMutation'], $postStoryMutation); + self::assertInstanceOf(ObjectType::class, $postStoryMutation); + $field = $postStoryMutation->getField('story'); + self::assertSame($blogStory, $field->getType()); + + $input = $schema->getType('PostStoryMutationInput'); + self::assertSame($this->types['PostStoryMutationInput'], $input); + + $result = $schema->isSubType($node, $schema->getType('BlogStory')); + self::assertTrue($result); + } +}