Skip to content

Commit 9083765

Browse files
jotweajosef.wagner
and
josef.wagner
authored
feat(graphql): support enum collection as property (#5955)
Co-authored-by: josef.wagner <[email protected]>
1 parent f776f11 commit 9083765

File tree

10 files changed

+123
-33
lines changed

10 files changed

+123
-33
lines changed

features/graphql/mutation.feature

+25
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,31 @@ Feature: GraphQL mutation support
505505
And the JSON node "data.createPerson.person.name" should be equal to "Mob"
506506
And the JSON node "data.createPerson.person.genderType" should be equal to "FEMALE"
507507

508+
@!mongodb
509+
Scenario: Create an item with an enum collection
510+
When I send the following GraphQL request:
511+
"""
512+
mutation {
513+
createPerson(input: {name: "Harry", academicGrades: [BACHELOR, MASTER]}) {
514+
person {
515+
id
516+
name
517+
genderType
518+
academicGrades
519+
}
520+
}
521+
}
522+
"""
523+
Then the response status code should be 200
524+
And the response should be in JSON
525+
And the header "Content-Type" should be equal to "application/json"
526+
And the JSON node "data.createPerson.person.id" should be equal to "/people/2"
527+
And the JSON node "data.createPerson.person.name" should be equal to "Harry"
528+
And the JSON node "data.createPerson.person.genderType" should be equal to "MALE"
529+
And the JSON node "data.createPerson.person.academicGrades" should have 2 elements
530+
And the JSON node "data.createPerson.person.academicGrades[0]" should be equal to "BACHELOR"
531+
And the JSON node "data.createPerson.person.academicGrades[1]" should be equal to "MASTER"
532+
508533
Scenario: Create an item with an enum as a resource
509534
When I send the following GraphQL request:
510535
"""

src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
4646
return null;
4747
}
4848

49+
if (is_a($resourceClass, \BackedEnum::class, true) && $source && \array_key_exists($info->fieldName, $source)) {
50+
return $source[$info->fieldName];
51+
}
52+
4953
$resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false];
5054

5155
$collection = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext);

src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php

+15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
1919
use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface;
2020
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
21+
use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum;
2122
use ApiPlatform\Metadata\GraphQl\QueryCollection;
2223
use GraphQL\Type\Definition\ResolveInfo;
2324
use PHPUnit\Framework\TestCase;
@@ -93,6 +94,20 @@ public function testResolve(): void
9394
$this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info));
9495
}
9596

97+
public function testResolveEnumFieldFromSource(): void
98+
{
99+
$resourceClass = GenderTypeEnum::class;
100+
$rootClass = 'rootClass';
101+
$operationName = 'collection_query';
102+
$operation = (new QueryCollection())->withName($operationName);
103+
$source = ['genders' => [GenderTypeEnum::MALE, GenderTypeEnum::FEMALE]];
104+
$args = ['args'];
105+
$info = $this->prophesize(ResolveInfo::class)->reveal();
106+
$info->fieldName = 'genders';
107+
108+
$this->assertSame([GenderTypeEnum::MALE, GenderTypeEnum::FEMALE], ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info));
109+
}
110+
96111
public function testResolveFieldNotInSource(): void
97112
{
98113
$resourceClass = \stdClass::class;

src/GraphQl/Tests/Type/TypeBuilderTest.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -568,15 +568,15 @@ public function testGetEnumType(): void
568568
{
569569
$enumClass = GamePlayMode::class;
570570
$enumName = 'GamePlayMode';
571-
$enumDescription = 'GamePlayModeEnum description';
571+
$enumDescription = 'GamePlayMode description';
572572
/** @var Operation $operation */
573573
$operation = (new Operation())
574574
->withClass($enumClass)
575575
->withShortName($enumName)
576-
->withDescription('GamePlayModeEnum description');
576+
->withDescription('GamePlayMode description');
577577

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

589589
self::assertEquals(new EnumType([
590-
'name' => 'GamePlayModeEnum',
590+
'name' => 'GamePlayMode',
591591
'description' => $enumDescription,
592592
'values' => $enumValues,
593593
]), $this->typeBuilder->getEnumType($operation));

src/GraphQl/Tests/Type/TypeConverterTest.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ protected function setUp(): void
6767
public function testConvertType(Type $type, bool $input, int $depth, GraphQLType|string|null $expectedGraphqlType): void
6868
{
6969
$this->typeBuilderProphecy->isCollection($type)->willReturn(false);
70-
$this->resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willThrow(new ResourceClassNotFoundException());
70+
$this->resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willReturn(new ResourceMetadataCollection('resourceClass'));
7171
$this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType);
7272

7373
/** @var Operation $operation */
@@ -195,6 +195,20 @@ public static function convertTypeResourceProvider(): array
195195
];
196196
}
197197

198+
public function testConvertTypeCollectionEnum(): void
199+
{
200+
$type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class));
201+
$expectedGraphqlType = new EnumType(['name' => 'GenderTypeEnum', 'values' => []]);
202+
$this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(true);
203+
$this->resourceMetadataCollectionFactoryProphecy->create(GenderTypeEnum::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(GenderTypeEnum::class, []));
204+
$this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType);
205+
206+
/** @var Operation $rootOperation */
207+
$rootOperation = (new Query())->withName('test');
208+
$graphqlType = $this->typeConverter->convertType($type, false, $rootOperation, 'resourceClass', 'rootClass', null, 0);
209+
$this->assertSame($expectedGraphqlType, $graphqlType);
210+
}
211+
198212
/**
199213
* @dataProvider resolveTypeProvider
200214
*/

src/GraphQl/Type/FieldsBuilder.php

+7-2
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o
243243
return $fields;
244244
}
245245

246+
private function isEnumClass(string $resourceClass): bool
247+
{
248+
return is_a($resourceClass, \BackedEnum::class, true);
249+
}
250+
246251
/**
247252
* {@inheritdoc}
248253
*/
@@ -331,7 +336,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
331336
$args = [];
332337

333338
if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) {
334-
if ($this->pagination->isGraphQlEnabled($resourceOperation)) {
339+
if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
335340
$args = $this->getGraphQlPaginationArgs($resourceOperation);
336341
}
337342

@@ -544,7 +549,7 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati
544549
}
545550

546551
if ($this->typeBuilder->isCollection($type)) {
547-
if (!$input && $this->pagination->isGraphQlEnabled($resourceOperation)) {
552+
if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
548553
// Deprecated path, to remove in API Platform 4.
549554
if ($this->typeBuilder instanceof TypeBuilderInterface) {
550555
return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation);

src/GraphQl/Type/TypeBuilder.php

-3
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,6 @@ public function getPaginatedCollectionType(GraphQLType $resourceType, Operation
245245
public function getEnumType(Operation $operation): GraphQLType
246246
{
247247
$enumName = $operation->getShortName();
248-
if (!str_ends_with($enumName, 'Enum')) {
249-
$enumName = sprintf('%sEnum', $enumName);
250-
}
251248

252249
if ($this->typesContainer->has($enumName)) {
253250
return $this->typesContainer->get($enumName);

src/GraphQl/Type/TypeConverter.php

+20-22
Original file line numberDiff line numberDiff line change
@@ -70,28 +70,7 @@ public function convertType(Type $type, bool $input, Operation $rootOperation, s
7070
return GraphQLType::string();
7171
}
7272

73-
$resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);
74-
75-
if (!$resourceType && is_a($type->getClassName(), \BackedEnum::class, true)) {
76-
// Remove the condition in API Platform 4.
77-
if ($this->typeBuilder instanceof TypeBuilderEnumInterface) {
78-
$operation = null;
79-
try {
80-
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($type->getClassName());
81-
$operation = $resourceMetadataCollection->getOperation();
82-
} catch (ResourceClassNotFoundException|OperationNotFoundException) {
83-
}
84-
/** @var Query $enumOperation */
85-
$enumOperation = (new Query())
86-
->withClass($type->getClassName())
87-
->withShortName($operation?->getShortName() ?? (new \ReflectionClass($type->getClassName()))->getShortName())
88-
->withDescription($operation?->getDescription());
89-
90-
return $this->typeBuilder->getEnumType($enumOperation);
91-
}
92-
}
93-
94-
return $resourceType;
73+
return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth);
9574
default:
9675
return null;
9776
}
@@ -149,6 +128,25 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati
149128
}
150129

151130
if (!$hasGraphQl) {
131+
if (is_a($resourceClass, \BackedEnum::class, true)) {
132+
// Remove the condition in API Platform 4.
133+
if ($this->typeBuilder instanceof TypeBuilderEnumInterface) {
134+
$operation = null;
135+
try {
136+
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
137+
$operation = $resourceMetadataCollection->getOperation();
138+
} catch (ResourceClassNotFoundException|OperationNotFoundException) {
139+
}
140+
/** @var Query $enumOperation */
141+
$enumOperation = (new Query())
142+
->withClass($resourceClass)
143+
->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName())
144+
->withDescription($operation?->getDescription());
145+
146+
return $this->typeBuilder->getEnumType($enumOperation);
147+
}
148+
}
149+
152150
return null;
153151
}
154152

tests/Fixtures/TestBundle/Entity/Person.php

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
1515

1616
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\Enum\AcademicGrade;
1718
use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum;
1819
use Doctrine\Common\Collections\ArrayCollection;
1920
use Doctrine\Common\Collections\Collection;
@@ -42,6 +43,11 @@ class Person
4243
#[Groups(['people.pets'])]
4344
public string $name;
4445

46+
/** @var array<AcademicGrade> */
47+
#[ORM\Column(nullable: true)]
48+
#[Groups(['people.pets'])]
49+
public array $academicGrades = [];
50+
4551
#[ORM\OneToMany(targetEntity: PersonToPet::class, mappedBy: 'person')]
4652
#[Groups(['people.pets'])]
4753
public Collection|iterable $pets;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Enum;
15+
16+
/**
17+
* An enumeration of academic grades.
18+
*/
19+
enum AcademicGrade: string
20+
{
21+
case BACHELOR = 'BACHELOR';
22+
23+
case MASTER = 'MASTER';
24+
25+
case DOCTOR = 'DOCTOR';
26+
}

0 commit comments

Comments
 (0)