Skip to content

GraphQL: Support Enum collections #5955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 24, 2023
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
25 changes: 25 additions & 0 deletions features/graphql/mutation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,31 @@ Feature: GraphQL mutation support
And the JSON node "data.createPerson.person.name" should be equal to "Mob"
And the JSON node "data.createPerson.person.genderType" should be equal to "FEMALE"

@!mongodb
Scenario: Create an item with an enum collection
When I send the following GraphQL request:
"""
mutation {
createPerson(input: {name: "Harry", academicGrades: [BACHELOR, MASTER]}) {
person {
id
name
genderType
academicGrades
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.createPerson.person.id" should be equal to "/people/2"
And the JSON node "data.createPerson.person.name" should be equal to "Harry"
And the JSON node "data.createPerson.person.genderType" should be equal to "MALE"
And the JSON node "data.createPerson.person.academicGrades" should have 2 elements
And the JSON node "data.createPerson.person.academicGrades[0]" should be equal to "BACHELOR"
And the JSON node "data.createPerson.person.academicGrades[1]" should be equal to "MASTER"

Scenario: Create an item with an enum as a resource
When I send the following GraphQL request:
"""
Expand Down
4 changes: 4 additions & 0 deletions src/GraphQl/Resolver/Factory/CollectionResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
return null;
}

if (is_a($resourceClass, \BackedEnum::class, true) && $source && \array_key_exists($info->fieldName, $source)) {
return $source[$info->fieldName];
}

$resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false];

$collection = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use GraphQL\Type\Definition\ResolveInfo;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -93,6 +94,20 @@ public function testResolve(): void
$this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info));
}

public function testResolveEnumFieldFromSource(): void
{
$resourceClass = GenderTypeEnum::class;
$rootClass = 'rootClass';
$operationName = 'collection_query';
$operation = (new QueryCollection())->withName($operationName);
$source = ['genders' => [GenderTypeEnum::MALE, GenderTypeEnum::FEMALE]];
$args = ['args'];
$info = $this->prophesize(ResolveInfo::class)->reveal();
$info->fieldName = 'genders';

$this->assertSame([GenderTypeEnum::MALE, GenderTypeEnum::FEMALE], ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info));
}

public function testResolveFieldNotInSource(): void
{
$resourceClass = \stdClass::class;
Expand Down
10 changes: 5 additions & 5 deletions src/GraphQl/Tests/Type/TypeBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -568,15 +568,15 @@ public function testGetEnumType(): void
{
$enumClass = GamePlayMode::class;
$enumName = 'GamePlayMode';
$enumDescription = 'GamePlayModeEnum description';
$enumDescription = 'GamePlayMode description';
/** @var Operation $operation */
$operation = (new Operation())
->withClass($enumClass)
->withShortName($enumName)
->withDescription('GamePlayModeEnum description');
->withDescription('GamePlayMode description');

$this->typesContainerProphecy->has('GamePlayModeEnum')->shouldBeCalled()->willReturn(false);
$this->typesContainerProphecy->set('GamePlayModeEnum', Argument::type(EnumType::class))->shouldBeCalled();
$this->typesContainerProphecy->has('GamePlayMode')->shouldBeCalled()->willReturn(false);
$this->typesContainerProphecy->set('GamePlayMode', Argument::type(EnumType::class))->shouldBeCalled();
$fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class);
$enumValues = [
GamePlayMode::CO_OP->name => ['value' => GamePlayMode::CO_OP->value],
Expand All @@ -587,7 +587,7 @@ public function testGetEnumType(): void
$this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->willReturn($fieldsBuilderProphecy->reveal());

self::assertEquals(new EnumType([
'name' => 'GamePlayModeEnum',
'name' => 'GamePlayMode',
'description' => $enumDescription,
'values' => $enumValues,
]), $this->typeBuilder->getEnumType($operation));
Expand Down
16 changes: 15 additions & 1 deletion src/GraphQl/Tests/Type/TypeConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ protected function setUp(): void
public function testConvertType(Type $type, bool $input, int $depth, GraphQLType|string|null $expectedGraphqlType): void
{
$this->typeBuilderProphecy->isCollection($type)->willReturn(false);
$this->resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willThrow(new ResourceClassNotFoundException());
$this->resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willReturn(new ResourceMetadataCollection('resourceClass'));
$this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType);

/** @var Operation $operation */
Expand Down Expand Up @@ -195,6 +195,20 @@ public static function convertTypeResourceProvider(): array
];
}

public function testConvertTypeCollectionEnum(): void
{
$type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class));
$expectedGraphqlType = new EnumType(['name' => 'GenderTypeEnum', 'values' => []]);
$this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(true);
$this->resourceMetadataCollectionFactoryProphecy->create(GenderTypeEnum::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(GenderTypeEnum::class, []));
$this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType);

/** @var Operation $rootOperation */
$rootOperation = (new Query())->withName('test');
$graphqlType = $this->typeConverter->convertType($type, false, $rootOperation, 'resourceClass', 'rootClass', null, 0);
$this->assertSame($expectedGraphqlType, $graphqlType);
}

/**
* @dataProvider resolveTypeProvider
*/
Expand Down
9 changes: 7 additions & 2 deletions src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o
return $fields;
}

private function isEnumClass(string $resourceClass): bool
{
return is_a($resourceClass, \BackedEnum::class, true);
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -331,7 +336,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
$args = [];

if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) {
if ($this->pagination->isGraphQlEnabled($resourceOperation)) {
if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
$args = $this->getGraphQlPaginationArgs($resourceOperation);
}

Expand Down Expand Up @@ -544,7 +549,7 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati
}

if ($this->typeBuilder->isCollection($type)) {
if (!$input && $this->pagination->isGraphQlEnabled($resourceOperation)) {
if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
// Deprecated path, to remove in API Platform 4.
if ($this->typeBuilder instanceof TypeBuilderInterface) {
return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation);
Expand Down
3 changes: 0 additions & 3 deletions src/GraphQl/Type/TypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,6 @@ public function getPaginatedCollectionType(GraphQLType $resourceType, Operation
public function getEnumType(Operation $operation): GraphQLType
{
$enumName = $operation->getShortName();
if (!str_ends_with($enumName, 'Enum')) {
$enumName = sprintf('%sEnum', $enumName);
}

if ($this->typesContainer->has($enumName)) {
return $this->typesContainer->get($enumName);
Expand Down
42 changes: 20 additions & 22 deletions src/GraphQl/Type/TypeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,28 +70,7 @@ public function convertType(Type $type, bool $input, Operation $rootOperation, s
return GraphQLType::string();
}

$resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);

if (!$resourceType && is_a($type->getClassName(), \BackedEnum::class, true)) {
// Remove the condition in API Platform 4.
if ($this->typeBuilder instanceof TypeBuilderEnumInterface) {
$operation = null;
try {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($type->getClassName());
$operation = $resourceMetadataCollection->getOperation();
} catch (ResourceClassNotFoundException|OperationNotFoundException) {
}
/** @var Query $enumOperation */
$enumOperation = (new Query())
->withClass($type->getClassName())
->withShortName($operation?->getShortName() ?? (new \ReflectionClass($type->getClassName()))->getShortName())
->withDescription($operation?->getDescription());

return $this->typeBuilder->getEnumType($enumOperation);
}
}

return $resourceType;
return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);
default:
return null;
}
Expand Down Expand Up @@ -149,6 +128,25 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati
}

if (!$hasGraphQl) {
if (is_a($resourceClass, \BackedEnum::class, true)) {
// Remove the condition in API Platform 4.
if ($this->typeBuilder instanceof TypeBuilderEnumInterface) {
$operation = null;
try {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
$operation = $resourceMetadataCollection->getOperation();
} catch (ResourceClassNotFoundException|OperationNotFoundException) {
}
/** @var Query $enumOperation */
$enumOperation = (new Query())
->withClass($resourceClass)
->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName())
->withDescription($operation?->getDescription());

return $this->typeBuilder->getEnumType($enumOperation);
}
}

return null;
}

Expand Down
6 changes: 6 additions & 0 deletions tests/Fixtures/TestBundle/Entity/Person.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Tests\Fixtures\TestBundle\Enum\AcademicGrade;
use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
Expand Down Expand Up @@ -42,6 +43,11 @@ class Person
#[Groups(['people.pets'])]
public string $name;

/** @var array<AcademicGrade> */
#[ORM\Column(nullable: true)]
#[Groups(['people.pets'])]
public array $academicGrades = [];

#[ORM\OneToMany(targetEntity: PersonToPet::class, mappedBy: 'person')]
#[Groups(['people.pets'])]
public Collection|iterable $pets;
Expand Down
26 changes: 26 additions & 0 deletions tests/Fixtures/TestBundle/Enum/AcademicGrade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\Enum;

/**
* An enumeration of academic grades.
*/
enum AcademicGrade: string
{
case BACHELOR = 'BACHELOR';

case MASTER = 'MASTER';

case DOCTOR = 'DOCTOR';
}