From 486bf572edf67599dcfdcb9c362ec0ed230b1a28 Mon Sep 17 00:00:00 2001 From: Michael Georgiadis <38563912+michael-georgiadis@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:23:10 +0200 Subject: [PATCH 1/2] Reproduce bug --- .../Controllers/ProductController.php | 35 +++++++++ .../InterfaceAsOutputType/Types/Book.php | 27 +++++++ .../Types/CreateProductInput.php | 18 +++++ .../Types/ProductInterface.php | 15 ++++ .../Types/ProductItems.php | 25 ++++++ .../Integration/InterfaceAsOutputTypeTest.php | 76 +++++++++++++++++++ 6 files changed, 196 insertions(+) create mode 100644 tests/Fixtures/InterfaceAsOutputType/Controllers/ProductController.php create mode 100644 tests/Fixtures/InterfaceAsOutputType/Types/Book.php create mode 100644 tests/Fixtures/InterfaceAsOutputType/Types/CreateProductInput.php create mode 100644 tests/Fixtures/InterfaceAsOutputType/Types/ProductInterface.php create mode 100644 tests/Fixtures/InterfaceAsOutputType/Types/ProductItems.php create mode 100644 tests/Integration/InterfaceAsOutputTypeTest.php diff --git a/tests/Fixtures/InterfaceAsOutputType/Controllers/ProductController.php b/tests/Fixtures/InterfaceAsOutputType/Controllers/ProductController.php new file mode 100644 index 0000000000..49ccc6d724 --- /dev/null +++ b/tests/Fixtures/InterfaceAsOutputType/Controllers/ProductController.php @@ -0,0 +1,35 @@ +name); + } +} diff --git a/tests/Fixtures/InterfaceAsOutputType/Types/Book.php b/tests/Fixtures/InterfaceAsOutputType/Types/Book.php new file mode 100644 index 0000000000..575da154d4 --- /dev/null +++ b/tests/Fixtures/InterfaceAsOutputType/Types/Book.php @@ -0,0 +1,27 @@ +name; + } + + #[Field] + public function getAuthor(): string + { + return 'Franz Kafka'; + } +} diff --git a/tests/Fixtures/InterfaceAsOutputType/Types/CreateProductInput.php b/tests/Fixtures/InterfaceAsOutputType/Types/CreateProductInput.php new file mode 100644 index 0000000000..389e0d2d55 --- /dev/null +++ b/tests/Fixtures/InterfaceAsOutputType/Types/CreateProductInput.php @@ -0,0 +1,18 @@ + $products */ + public function __construct(private array $products) + { + } + + // Referencing the interface by name via outputType resolves it early, which is what left the + // interface "already registered" by the time getOutputTypes() reached its class. + #[Field(outputType: '[Product]')] + public function getItems(): array + { + return $this->products; + } +} diff --git a/tests/Integration/InterfaceAsOutputTypeTest.php b/tests/Integration/InterfaceAsOutputTypeTest.php new file mode 100644 index 0000000000..0a92d80d99 --- /dev/null +++ b/tests/Integration/InterfaceAsOutputTypeTest.php @@ -0,0 +1,76 @@ +addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\InterfaceAsOutputType'); + + $this->schema = $schemaFactory->createSchema(); + } + + public function testSchemaBuildsWhenInterfaceIsUsedAsOutputType(): void + { + // Resolve the interface by name first (as a name-based outputType reference does), so it is + // already registered by the time the full type map / getOutputTypes() runs. + $this->schema->getType('Product'); + + $this->schema->assertValid(); + + $this->assertNotNull($this->schema->getType('Product')); + $this->assertNotNull($this->schema->getType('Book')); + } + + public function testQueryReturningInterfaceResolvesConcreteType(): void + { + $result = GraphQL::executeQuery( + $this->schema, + ' + query { + product { + name + ... on Book { + author + } + } + } + ', + ); + + $this->assertSame( + [ + 'product' => [ + 'name' => 'The Trial', + 'author' => 'Franz Kafka', + ], + ], + $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)['data'] + ?? $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)['errors'], + ); + } +} From bf2afa4a5b7e1a7e5defea1e4a183bc2c4bdda02 Mon Sep 17 00:00:00 2001 From: Michael Georgiadis <38563912+michael-georgiadis@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:23:18 +0200 Subject: [PATCH 2/2] Apply fix --- src/Mappers/RecursiveTypeMapper.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index 4df79548a6..bb65537b30 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -308,15 +308,19 @@ public function mapClassToType(string $className, OutputType|null $subType): Mut // In the event this type was already part of cache, let's not extend it. if ($this->typeRegistry->hasType($type->name)) { + // Already-registered interface: return its concrete companion (as the non-cached path + // does below), else getMutableObjectType() throws on it. + if ($type instanceof MutableInterfaceType) { + return $this->classToTypeCache[$cacheKey] + ??= $this->getGeneratedObjectTypeFromInterfaceType($type); + } + $cachedType = $this->typeRegistry->getMutableObjectType($type->name); if ($cachedType !== $type) { throw new RuntimeException('Cached type in registry is not the type returned by type mapper.'); } - //if ($cachedType->getStatus() === MutableObjectType::STATUS_FROZEN) { - return $type; - - //} + return $type; } $this->typeRegistry->registerType($type);