Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/Mappers/RecursiveTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\MutationInterfaceFreeze\Controllers;

use TheCodingMachine\GraphQLite\Annotations\Mutation;
use TheCodingMachine\GraphQLite\Fixtures\MutationInterfaceFreeze\Types\ConcreteResult;
use TheCodingMachine\GraphQLite\Fixtures\MutationInterfaceFreeze\Types\ResultInterface;

class MutationOnlyController
{
#[Mutation]
public function mutateResult(): ResultInterface
{
return new ConcreteResult('success');
}
}
28 changes: 28 additions & 0 deletions tests/Fixtures/MutationInterfaceFreeze/Types/ConcreteResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\MutationInterfaceFreeze\Types;

use TheCodingMachine\GraphQLite\Annotations\Field;
use TheCodingMachine\GraphQLite\Annotations\Type;

#[Type]
class ConcreteResult implements ResultInterface
{
public function __construct(private readonly string $message)
{
}

#[Field]
public function getMessage(): string
{
return $this->message;
}

#[Field]
public function getExtra(): string
{
return 'extra';
}
}
15 changes: 15 additions & 0 deletions tests/Fixtures/MutationInterfaceFreeze/Types/ResultInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\MutationInterfaceFreeze\Types;

use TheCodingMachine\GraphQLite\Annotations\Field;
use TheCodingMachine\GraphQLite\Annotations\Type;

#[Type]
interface ResultInterface
{
#[Field]
public function getMessage(): string;
}
141 changes: 141 additions & 0 deletions tests/Integration/MutationInterfaceFreezeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Integration;

use GraphQL\Error\DebugFlag;
use GraphQL\GraphQL;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Psr16Cache;
use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer;
use TheCodingMachine\GraphQLite\Containers\EmptyContainer;
use TheCodingMachine\GraphQLite\Schema;
use TheCodingMachine\GraphQLite\SchemaFactory;

/**
* Regression test for issue #308: MutableInterfaceType not frozen when used
* exclusively as a mutation return type.
*
* @see https://github.com/thecodingmachine/graphqlite/issues/308
*/
class MutationInterfaceFreezeTest extends TestCase
{
private Schema $schema;

public function setUp(): void
{
$container = new BasicAutoWiringContainer(new EmptyContainer());

$schemaFactory = new SchemaFactory(new Psr16Cache(new ArrayAdapter()), $container);
$schemaFactory->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'],
);
}
}
Loading