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); 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'], + ); + } +}