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: 8 additions & 4 deletions src/Mappers/RecursiveTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Controllers;

use TheCodingMachine\GraphQLite\Annotations\Mutation;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types\Book;
use TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types\CreateProductInput;
use TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types\ProductInterface;
use TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types\ProductItems;

class ProductController
{
#[Query]
public function product(): ProductInterface
{
return new Book('The Trial');
}

#[Query]
public function products(): ProductItems
{
return new ProductItems([new Book('The Trial'), new Book('The Castle')]);
}

// The input type forces GraphQLite to materialise output types mid-way through input field
// building, which is the ordering that triggered the regression.
#[Mutation]
public function createProduct(CreateProductInput $input): ProductInterface
{
return new Book($input->name);
}
}
27 changes: 27 additions & 0 deletions tests/Fixtures/InterfaceAsOutputType/Types/Book.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types;

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

#[Type]
class Book implements ProductInterface
{
public function __construct(private string $name)
{
}

public function getName(): string
{
return $this->name;
}

#[Field]
public function getAuthor(): string
{
return 'Franz Kafka';
}
}
18 changes: 18 additions & 0 deletions tests/Fixtures/InterfaceAsOutputType/Types/CreateProductInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types;

use TheCodingMachine\GraphQLite\Annotations\Field;
use TheCodingMachine\GraphQLite\Annotations\Input;

#[Input]
class CreateProductInput
{
public function __construct(
#[Field]
public string $name,
) {
}
}
15 changes: 15 additions & 0 deletions tests/Fixtures/InterfaceAsOutputType/Types/ProductInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types;

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

#[Type(name: 'Product')]
interface ProductInterface
{
#[Field]
public function getName(): string;
}
25 changes: 25 additions & 0 deletions tests/Fixtures/InterfaceAsOutputType/Types/ProductItems.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\InterfaceAsOutputType\Types;

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

#[Type]
class ProductItems
{
/** @param list<Book> $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;
}
}
76 changes: 76 additions & 0 deletions tests/Integration/InterfaceAsOutputTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?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;

/**
* A #[Type] on a PHP interface (with a custom name and a single concrete implementer) that is used
* as an output type used to break schema building: getOutputTypes() called the object-only
* mapClassToType() on the interface class once it was already registered, throwing
* "Expected GraphQL type ... to be an MutableObjectType".
*/
class InterfaceAsOutputTypeTest 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\\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'],
);
}
}
Loading