diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index 12f6d8af43..4df79548a6 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -243,6 +243,14 @@ public function findInterfaces(string $className): array continue; } + // Map through RecursiveTypeMapper's pipeline to ensure the interface + // is registered in the TypeRegistry, extended, and frozen. + // Calling the underlying typeMapper directly would create an orphaned, + // unfrozen instance that is never registered in the TypeRegistry. + $this->mapClassToType($interface, null); + + // Now retrieve the frozen MutableInterfaceType. TypeGenerator returns + // the same instance from the TypeRegistry that mapClassToType registered. $interfaceType = $this->typeMapper->mapClassToType($interface, null); assert($interfaceType instanceof MutableInterfaceType); @@ -489,14 +497,14 @@ public function mapNameToType(string $typeName): Type&NamedType if ($cachedType !== $type) { throw new RuntimeException('Cached type in registry is not the type returned by type mapper.'); } - if ($cachedType instanceof MutableObjectType && $cachedType->getStatus() === MutableObjectType::STATUS_FROZEN) { + if ($cachedType instanceof MutableInterface && $cachedType->getStatus() === MutableInterface::STATUS_FROZEN) { return $type; } } $this->typeRegistry->registerType($type); - if ($type instanceof MutableObjectType) { + if ($type instanceof MutableObjectType || $type instanceof MutableInterfaceType) { if ($this->typeMapper->canExtendTypeForName($typeName, $type)) { $this->typeMapper->extendTypeForName($typeName, $type); } diff --git a/tests/Fixtures/MutationInterfaceFreeze/Controllers/MutationOnlyController.php b/tests/Fixtures/MutationInterfaceFreeze/Controllers/MutationOnlyController.php new file mode 100644 index 0000000000..91d2396b86 --- /dev/null +++ b/tests/Fixtures/MutationInterfaceFreeze/Controllers/MutationOnlyController.php @@ -0,0 +1,18 @@ +message; + } + + #[Field] + public function getExtra(): string + { + return 'extra'; + } +} diff --git a/tests/Fixtures/MutationInterfaceFreeze/Types/ResultInterface.php b/tests/Fixtures/MutationInterfaceFreeze/Types/ResultInterface.php new file mode 100644 index 0000000000..e6f38d8dce --- /dev/null +++ b/tests/Fixtures/MutationInterfaceFreeze/Types/ResultInterface.php @@ -0,0 +1,15 @@ +addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\MutationInterfaceFreeze'); + + $this->schema = $schemaFactory->createSchema(); + } + + /** + * Schema validation triggers getTypeMap() which traverses all types. + * Before the fix, this would throw: + * "You must freeze() a MutableObjectType before fetching its fields." + */ + public function testSchemaValidationDoesNotThrowForMutationOnlyInterface(): void + { + $this->schema->assertValid(); + $this->addToAssertionCount(1); + } + + /** + * Executing a mutation that returns an interface type should work + * without any query also returning that interface. + */ + public function testMutationReturningInterfaceCanBeExecuted(): void + { + $queryString = ' + mutation { + mutateResult { + message + } + } + '; + + $result = GraphQL::executeQuery( + $this->schema, + $queryString, + ); + + $this->assertSame( + ['mutateResult' => ['message' => 'success']], + $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)['data'], + ); + } + + /** + * Inline fragments on an interface type in a mutation should work. + * Before the fix, the OverlappingFieldsCanBeMerged validator would + * access the unfrozen MutableInterfaceType and crash. + */ + public function testMutationWithInlineFragmentOnInterface(): void + { + $queryString = ' + mutation { + mutateResult { + ... on ResultInterface { + message + } + ... on ConcreteResult { + message + extra + } + } + } + '; + + $result = GraphQL::executeQuery( + $this->schema, + $queryString, + ); + + $this->assertSame( + [ + 'mutateResult' => [ + 'message' => 'success', + 'extra' => 'extra', + ], + ], + $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)['data'], + ); + } + + /** + * Introspection must include the mutation return interface type + * and it must be resolvable without errors. + */ + public function testIntrospectionIncludesMutationInterfaceType(): void + { + $queryString = ' + { + __type(name: "ResultInterface") { + kind + name + fields { + name + } + } + } + '; + + $result = GraphQL::executeQuery( + $this->schema, + $queryString, + ); + + $data = $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)['data']; + $this->assertSame('INTERFACE', $data['__type']['kind']); + $this->assertSame('ResultInterface', $data['__type']['name']); + $this->assertContains( + ['name' => 'message'], + $data['__type']['fields'], + ); + } +}