From fa3cfe673503c820aa5b249346eefd474aca3d21 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Tue, 22 Nov 2022 16:06:13 +0100 Subject: [PATCH 1/3] fix(openapi): resource classes are no longer considered as enum --- .php-cs-fixer.dist.php | 1 + features/openapi/docs.feature | 8 +++ src/JsonSchema/SchemaFactory.php | 7 +-- src/JsonSchema/TypeFactory.php | 2 +- .../TestBundle/Document/VideoGame.php | 37 +++++++++++++ .../Fixtures/TestBundle/Entity/VideoGame.php | 39 ++++++++++++++ .../Fixtures/TestBundle/Enum/GamePlayMode.php | 53 +++++++++++++++++++ tests/Fixtures/app/config/config_common.yml | 1 + tests/JsonSchema/TypeFactoryTest.php | 35 ++++++++---- 9 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Document/VideoGame.php create mode 100644 tests/Fixtures/TestBundle/Entity/VideoGame.php create mode 100644 tests/Fixtures/TestBundle/Enum/GamePlayMode.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 9e5241ceca6..5ddb8587cd2 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -24,6 +24,7 @@ ->notPath('src/Annotation/ApiResource.php') // temporary ->notPath('src/Annotation/ApiSubresource.php') // temporary ->notPath('tests/Fixtures/TestBundle/Entity/DummyPhp8.php') // temporary + ->notPath('tests/Fixtures/TestBundle/Enum/GamePlayMode.php') // PHPDoc on enum cases ->append([ 'tests/Fixtures/app/console', ]); diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 2cbf3b33c45..3a413fa916f 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -73,6 +73,14 @@ Feature: Documentation support "nullable": true } """ + And the "playMode" property exists for the OpenAPI class "VideoGame" + And the "playMode" property for the OpenAPI class "VideoGame" should be equal to: + """ + { + "type": "string", + "format": "iri-reference" + } + """ # Enable these tests when SF 4.4 / PHP 7.1 support is dropped #And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean" #And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean" diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index aee436fb591..41a39e5e5e5 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -174,7 +174,10 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $propertySchema['externalDocs'] = ['url' => $iri]; } - if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault())) { + // TODO: 3.0 support multiple types + $type = $propertyMetadata->getBuiltinTypes()[0] ?? null; + + if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault()) && (null === $type?->getClassName() || !$this->isResourceClass($type->getClassName()))) { if ($default instanceof \BackedEnum) { $default = $default->value; } @@ -190,8 +193,6 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str } $valueSchema = []; - // TODO: 3.0 support multiple types - $type = $propertyMetadata->getBuiltinTypes()[0] ?? null; if (null !== $type) { if ($isCollection = $type->isCollection()) { $keyType = $type->getCollectionKeyTypes()[0] ?? null; diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php index e67d510b3ce..62c06e4cc83 100644 --- a/src/JsonSchema/TypeFactory.php +++ b/src/JsonSchema/TypeFactory.php @@ -116,7 +116,7 @@ private function getClassType(?string $className, bool $nullable, string $format 'format' => 'binary', ]; } - if (is_a($className, \BackedEnum::class, true)) { + if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) { $rEnum = new \ReflectionEnum($className); $enumCases = array_map(static fn (\ReflectionEnumBackedCase $rCase) => $rCase->getBackingValue(), $rEnum->getCases()); if ($nullable) { diff --git a/tests/Fixtures/TestBundle/Document/VideoGame.php b/tests/Fixtures/TestBundle/Document/VideoGame.php new file mode 100644 index 00000000000..60dc2a5abf4 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/VideoGame.php @@ -0,0 +1,37 @@ + + * + * 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\Document; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ApiResource] +#[ODM\Document] +class VideoGame +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + private ?int $id = null; + + #[ODM\Field(type: 'string')] + public string $name; + + #[ODM\Field(type: 'string', enumType: GamePlayMode::class)] + public GamePlayMode $playMode = GamePlayMode::SINGLE_PLAYER; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/VideoGame.php b/tests/Fixtures/TestBundle/Entity/VideoGame.php new file mode 100644 index 00000000000..34dd41b2f29 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/VideoGame.php @@ -0,0 +1,39 @@ + + * + * 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\Entity; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource] +#[ORM\Entity] +class VideoGame +{ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + #[ORM\Column(type: 'string')] + public string $name; + + #[ORM\Column(type: 'string', enumType: GamePlayMode::class)] + public GamePlayMode $playMode = GamePlayMode::SINGLE_PLAYER; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Enum/GamePlayMode.php b/tests/Fixtures/TestBundle/Enum/GamePlayMode.php new file mode 100644 index 00000000000..ec6c38a2fe7 --- /dev/null +++ b/tests/Fixtures/TestBundle/Enum/GamePlayMode.php @@ -0,0 +1,53 @@ + + * + * 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; + +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Tests\Fixtures\TestBundle\Metadata\Get; + +#[Get(description: 'Indicates whether this game is multi-player, co-op or single-player.', provider: self::class.'::getCase')] +#[GetCollection(provider: self::class.'::getCases')] +#[Query(provider: self::class.'::getCase')] +#[QueryCollection(provider: self::class.'::getCases', paginationEnabled: false)] +enum GamePlayMode: string +{ + /** Co-operative games, where you play on the same team with friends. */ + case CO_OP = 'CoOp'; + + /** Requiring or allowing multiple human players to play simultaneously. */ + case MULTI_PLAYER = 'MultiPlayer'; + + /** Which is played by a lone player. */ + case SINGLE_PLAYER = 'SinglePlayer'; + + public function getId(): string + { + return $this->name; + } + + public static function getCase(Operation $operation, array $uriVariables): GamePlayMode + { + $name = $uriVariables['id'] ?? null; + + return \constant(self::class."::$name"); + } + + public static function getCases(): array + { + return self::cases(); + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 98ff470df5e..f8055e315fd 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -78,6 +78,7 @@ api_platform: doctrine_mongodb_odm: false mapping: paths: + - '%kernel.project_dir%/../TestBundle/Enum' - '%kernel.project_dir%/../TestBundle/Model' parameters: diff --git a/tests/JsonSchema/TypeFactoryTest.php b/tests/JsonSchema/TypeFactoryTest.php index 418139459ef..996027a62f1 100644 --- a/tests/JsonSchema/TypeFactoryTest.php +++ b/tests/JsonSchema/TypeFactoryTest.php @@ -13,10 +13,12 @@ namespace ApiPlatform\Tests\JsonSchema; +use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -32,7 +34,10 @@ class TypeFactoryTest extends TestCase */ public function testGetType(array $schema, Type $type): void { - $typeFactory = new TypeFactory(); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); + $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); + $typeFactory = new TypeFactory($resourceClassResolver->reveal()); $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_OPENAPI))); } @@ -54,8 +59,10 @@ public function typeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['nullable' => true, 'type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; + yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['nullable' => true, 'type' => 'array', 'items' => ['type' => 'string']], @@ -155,7 +162,10 @@ public function typeProvider(): iterable */ public function testGetTypeWithJsonSchemaSyntax(array $schema, Type $type): void { - $typeFactory = new TypeFactory(); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); + $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); + $typeFactory = new TypeFactory($resourceClassResolver->reveal()); $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_JSON_SCHEMA))); } @@ -177,8 +187,10 @@ public function jsonSchemaTypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => ['string', 'null'], 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield [['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield ['nullable enum' => ['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; + yield ['nullable enum resource' => ['type' => ['string', 'null'], 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['type' => ['array', 'null'], 'items' => ['type' => 'string']], @@ -271,7 +283,10 @@ public function jsonSchemaTypeProvider(): iterable /** @dataProvider openAPIV2TypeProvider */ public function testGetTypeWithOpenAPIV2Syntax(array $schema, Type $type): void { - $typeFactory = new TypeFactory(); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); + $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); + $typeFactory = new TypeFactory($resourceClassResolver->reveal()); $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_SWAGGER))); } @@ -293,8 +308,10 @@ public function openAPIV2TypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield [['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; + yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; + yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable, but ignored in OpenAPI V2' => [ ['type' => 'array', 'items' => ['type' => 'string']], From 786ba0a8b5d222323d7177b2d0f45f8de8f6aef2 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Tue, 22 Nov 2022 16:23:54 +0100 Subject: [PATCH 2/3] feat: make property metadata factories work with enum cases --- .php-cs-fixer.dist.php | 1 + src/Metadata/ApiProperty.php | 2 +- .../AttributePropertyMetadataFactory.php | 21 +++++++++++++++++++ .../TestBundle/Enum/GenderTypeEnum.php | 8 +++++++ .../AttributePropertyMetadataFactoryTest.php | 4 ++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 5ddb8587cd2..2493ac92a54 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -25,6 +25,7 @@ ->notPath('src/Annotation/ApiSubresource.php') // temporary ->notPath('tests/Fixtures/TestBundle/Entity/DummyPhp8.php') // temporary ->notPath('tests/Fixtures/TestBundle/Enum/GamePlayMode.php') // PHPDoc on enum cases + ->notPath('tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php') // PHPDoc on enum cases ->append([ 'tests/Fixtures/app/console', ]); diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index b01bad439f2..8541dc210af 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -20,7 +20,7 @@ * * @author Kévin Dunglas */ -#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)] +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT)] final class ApiProperty { /** diff --git a/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php b/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php index 94fa423dad4..6100f784393 100644 --- a/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php @@ -42,9 +42,30 @@ public function create(string $resourceClass, string $property, array $options = } } + $reflectionClass = null; + $reflectionEnum = null; + try { $reflectionClass = new \ReflectionClass($resourceClass); } catch (\ReflectionException) { + } + try { + $reflectionEnum = new \ReflectionEnum($resourceClass); + } catch (\ReflectionException) { + } + + if (!$reflectionClass && !$reflectionEnum) { + return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property); + } + + if ($reflectionEnum) { + if ($reflectionEnum->hasCase($property)) { + $reflectionCase = $reflectionEnum->getCase($property); + if ($attributes = $reflectionCase->getAttributes(ApiProperty::class)) { + return $this->createMetadata($attributes[0]->newInstance(), $parentPropertyMetadata); + } + } + return $this->handleNotFound($parentPropertyMetadata, $resourceClass, $property); } diff --git a/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php index 12327a2e6df..7eb7acffc53 100644 --- a/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php +++ b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php @@ -13,8 +13,16 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Enum; +use ApiPlatform\Metadata\ApiProperty; + +/** + * An enumeration of genders. + */ enum GenderTypeEnum: string { + /** The male gender. */ case MALE = 'male'; + + #[ApiProperty(description: 'The female gender.')] case FEMALE = 'female'; } diff --git a/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php b/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php index 35ad053b952..21c38321e36 100644 --- a/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php +++ b/tests/Metadata/Property/Factory/AttributePropertyMetadataFactoryTest.php @@ -18,6 +18,7 @@ use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyPhp8ApiPropertyAttribute; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -38,6 +39,9 @@ public function testCreateAttribute(): void $metadata = $factory->create(DummyPhp8ApiPropertyAttribute::class, 'foo'); $this->assertSame('a foo', $metadata->getDescription()); + + $metadata = $factory->create(GenderTypeEnum::class, 'FEMALE'); + $this->assertSame('The female gender.', $metadata->getDescription()); } public function testClassNotFound(): void From a7ac31e3f8eb4f0016d75542852bde2b44ee0f14 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Tue, 22 Nov 2022 16:26:32 +0100 Subject: [PATCH 3/3] feat: add GraphQL enum support --- .php-cs-fixer.dist.php | 1 + features/graphql/introspection.feature | 55 ++++++++++++++++ features/graphql/mutation.feature | 63 ++++++++++++++++++ src/GraphQl/Type/FieldsBuilder.php | 41 +++++++++++- .../Type/FieldsBuilderEnumInterface.php | 64 +++++++++++++++++++ src/GraphQl/Type/FieldsBuilderInterface.php | 2 + src/GraphQl/Type/SchemaBuilder.php | 5 +- src/GraphQl/Type/TypeBuilder.php | 49 +++++++++++++- src/GraphQl/Type/TypeBuilderEnumInterface.php | 57 +++++++++++++++++ src/GraphQl/Type/TypeBuilderInterface.php | 4 ++ src/GraphQl/Type/TypeConverter.php | 28 +++++++- tests/Fixtures/TestBundle/Document/Person.php | 7 +- tests/Fixtures/TestBundle/Entity/Person.php | 1 + .../TestBundle/Enum/EnumWithDescriptions.php | 29 +++++++++ tests/GraphQl/Type/FieldsBuilderTest.php | 26 +++++++- tests/GraphQl/Type/SchemaBuilderTest.php | 9 +-- tests/GraphQl/Type/TypeBuilderTest.php | 62 +++++++++++++----- tests/GraphQl/Type/TypeConverterTest.php | 13 ++-- tests/JsonSchema/SchemaFactoryTest.php | 15 ++++- 19 files changed, 487 insertions(+), 44 deletions(-) create mode 100644 src/GraphQl/Type/FieldsBuilderEnumInterface.php create mode 100644 src/GraphQl/Type/TypeBuilderEnumInterface.php create mode 100644 tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 2493ac92a54..846e5a32281 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -24,6 +24,7 @@ ->notPath('src/Annotation/ApiResource.php') // temporary ->notPath('src/Annotation/ApiSubresource.php') // temporary ->notPath('tests/Fixtures/TestBundle/Entity/DummyPhp8.php') // temporary + ->notPath('tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php') // PHPDoc on enum cases ->notPath('tests/Fixtures/TestBundle/Enum/GamePlayMode.php') // PHPDoc on enum cases ->notPath('tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php') // PHPDoc on enum cases ->append([ diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature index 22934bbbb31..03be840718e 100644 --- a/features/graphql/introspection.feature +++ b/features/graphql/introspection.feature @@ -566,3 +566,58 @@ Feature: GraphQL introspection support And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionCursorConnection" is not present in the types container' And the JSON node "data.typeNotAvailable" should be null And the JSON node "data.typeOwner.fields[1].type.name" should be equal to "VoDummyInspectionCursorConnection" + + Scenario: Introspect an enum + When I send the following GraphQL request: + """ + { + person: __type(name: "Person") { + name + fields { + name + type { + name + description + enumValues { + name + description + } + } + } + } + } + """ + 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.person.fields[1].type.name" should be equal to "GenderTypeEnum" + #And the JSON node "data.person.fields[1].type.description" should be equal to "An enumeration of genders." + And the JSON node "data.person.fields[1].type.enumValues[0].name" should be equal to "MALE" + #And the JSON node "data.person.fields[1].type.enumValues[0].description" should be equal to "The male gender." + And the JSON node "data.person.fields[1].type.enumValues[1].name" should be equal to "FEMALE" + And the JSON node "data.person.fields[1].type.enumValues[1].description" should be equal to "The female gender." + + Scenario: Introspect an enum resource + When I send the following GraphQL request: + """ + { + videoGame: __type(name: "VideoGame") { + name + fields { + name + type { + name + kind + ofType { + name + kind + } + } + } + } + } + """ + 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.videoGame.fields[3].type.ofType.name" should be equal to "GamePlayMode" diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature index e67e55554b4..b0532bf4d80 100644 --- a/features/graphql/mutation.feature +++ b/features/graphql/mutation.feature @@ -485,6 +485,69 @@ Feature: GraphQL mutation support And the JSON node "data.createDummy.dummy.arrayData[1]" should be equal to baz And the JSON node "data.createDummy.clientMutationId" should be equal to "myId" + Scenario: Create an item with an enum + When I send the following GraphQL request: + """ + mutation { + createPerson(input: {name: "Mob", genderType: FEMALE}) { + person { + id + name + genderType + } + } + } + """ + 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/1" + 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" + + Scenario: Create an item with an enum as a resource + When I send the following GraphQL request: + """ + { + gamePlayModes { + id + name + } + gamePlayMode(id: "/game_play_modes/SINGLE_PLAYER") { + name + } + } + """ + 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.gamePlayModes" should have 3 elements + And the JSON node "data.gamePlayModes[2].id" should be equal to "/game_play_modes/SINGLE_PLAYER" + And the JSON node "data.gamePlayModes[2].name" should be equal to "SINGLE_PLAYER" + And the JSON node "data.gamePlayMode.name" should be equal to "SINGLE_PLAYER" + When I send the following GraphQL request: + """ + mutation { + createVideoGame(input: {name: "Baten Kaitos", playMode: "/game_play_modes/SINGLE_PLAYER"}) { + videoGame { + id + name + playMode { + id + name + } + } + } + } + """ + 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.createVideoGame.videoGame.id" should be equal to "/video_games/1" + And the JSON node "data.createVideoGame.videoGame.name" should be equal to "Baten Kaitos" + And the JSON node "data.createVideoGame.videoGame.playMode.id" should be equal to "/game_play_modes/SINGLE_PLAYER" + And the JSON node "data.createVideoGame.videoGame.playMode.name" should be equal to "SINGLE_PLAYER" + Scenario: Delete an item through a mutation When I send the following GraphQL request: """ diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 26a9c7d42c3..77bb27c9dbc 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -42,10 +42,16 @@ * * @author Alan Poulain */ -final class FieldsBuilder implements FieldsBuilderInterface +final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) + private readonly TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder; + + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) { + if ($typeBuilder instanceof TypeBuilderInterface) { + @trigger_error(sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); + } + $this->typeBuilder = $typeBuilder; } /** @@ -226,6 +232,26 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o return $fields; } + /** + * {@inheritdoc} + */ + public function getEnumFields(string $enumClass): array + { + $rEnum = new \ReflectionEnum($enumClass); + + $enumCases = []; + foreach ($rEnum->getCases() as $rCase) { + $enumCase = ['value' => $rCase->getBackingValue()]; + $propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName()); + if ($enumCaseDescription = $propertyMetadata->getDescription()) { + $enumCase['description'] = $enumCaseDescription; + } + $enumCases[$rCase->getName()] = $enumCase; + } + + return $enumCases; + } + /** * {@inheritdoc} */ @@ -481,7 +507,16 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation) : GraphQLType::listOf($graphqlType); + if (!$input && $this->pagination->isGraphQlEnabled($resourceOperation)) { + // Deprecated path, to remove in API Platform 4. + if ($this->typeBuilder instanceof TypeBuilderInterface) { + return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation); + } + + return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation); + } + + return GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/GraphQl/Type/FieldsBuilderEnumInterface.php b/src/GraphQl/Type/FieldsBuilderEnumInterface.php new file mode 100644 index 00000000000..0517796e71c --- /dev/null +++ b/src/GraphQl/Type/FieldsBuilderEnumInterface.php @@ -0,0 +1,64 @@ + + * + * 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\GraphQl\Type; + +use ApiPlatform\Metadata\GraphQl\Operation; + +/** + * Interface implemented to build GraphQL fields. + * + * @author Alan Poulain + */ +interface FieldsBuilderEnumInterface +{ + /** + * Gets the fields of a node for a query. + */ + public function getNodeQueryFields(): array; + + /** + * Gets the item query fields of the schema. + */ + public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array; + + /** + * Gets the collection query fields of the schema. + */ + public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array; + + /** + * Gets the mutation fields of the schema. + */ + public function getMutationFields(string $resourceClass, Operation $operation): array; + + /** + * Gets the subscription fields of the schema. + */ + public function getSubscriptionFields(string $resourceClass, Operation $operation): array; + + /** + * Gets the fields of the type of the given resource. + */ + public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array; + + /** + * Gets the fields (cases) of the enum. + */ + public function getEnumFields(string $enumClass): array; + + /** + * Resolve the args of a resource by resolving its types. + */ + public function resolveResourceArgs(array $args, Operation $operation): array; +} diff --git a/src/GraphQl/Type/FieldsBuilderInterface.php b/src/GraphQl/Type/FieldsBuilderInterface.php index afab39d8aaa..dc4bd57f003 100644 --- a/src/GraphQl/Type/FieldsBuilderInterface.php +++ b/src/GraphQl/Type/FieldsBuilderInterface.php @@ -19,6 +19,8 @@ * Interface implemented to build GraphQL fields. * * @author Alan Poulain + * + * @deprecated Since API Platform 3.1. Use @see FieldsBuilderEnumInterface instead. */ interface FieldsBuilderInterface { diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index 8be0411010d..c1e2d1ce942 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -32,8 +32,11 @@ */ final class SchemaBuilder implements SchemaBuilderInterface { - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderInterface $fieldsBuilder) + public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder) { + if ($this->fieldsBuilder instanceof FieldsBuilderInterface) { + @trigger_error(sprintf('$fieldsBuilder argument of SchemaBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); + } } public function getSchema(): Schema diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 480b102bb2c..4c9fec5e649 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\Pagination; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\NonNull; @@ -35,7 +36,7 @@ * * @author Alan Poulain */ -final class TypeBuilder implements TypeBuilderInterface +final class TypeBuilder implements TypeBuilderInterface, TypeBuilderEnumInterface { private $defaultFieldResolver; @@ -201,6 +202,16 @@ public function getNodeInterface(): InterfaceType * {@inheritdoc} */ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType + { + @trigger_error('Using getResourcePaginatedCollectionType method of TypeBuilder is deprecated since API Platform 3.1. Use getPaginatedCollectionType method instead.', \E_USER_DEPRECATED); + + return $this->getPaginatedCollectionType($resourceType, $operation); + } + + /** + * {@inheritdoc} + */ + public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType { $shortName = $resourceType->name; $paginationType = $this->pagination->getGraphQlPaginationType($operation); @@ -226,6 +237,42 @@ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, st return $resourcePaginatedCollectionType; } + public function getEnumType(Operation $operation): GraphQLType + { + $enumName = $operation->getShortName(); + $enumKey = $enumName; + if (!str_ends_with($enumName, 'Enum')) { + $enumKey = sprintf('%sEnum', $enumName); + } + + if ($this->typesContainer->has($enumKey)) { + return $this->typesContainer->get($enumKey); + } + + /** @var FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder */ + $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder'); + $enumCases = []; + // Remove the condition in API Platform 4. + if ($fieldsBuilder instanceof FieldsBuilderEnumInterface) { + $enumCases = $fieldsBuilder->getEnumFields($operation->getClass()); + } else { + @trigger_error(sprintf('api_platform.graphql.fields_builder service implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); + } + + $enumConfig = [ + 'name' => $enumName, + 'values' => $enumCases, + ]; + if ($enumDescription = $operation->getDescription()) { + $enumConfig['description'] = $enumDescription; + } + + $enumType = new EnumType($enumConfig); + $this->typesContainer->set($enumKey, $enumType); + + return $enumType; + } + /** * {@inheritdoc} */ diff --git a/src/GraphQl/Type/TypeBuilderEnumInterface.php b/src/GraphQl/Type/TypeBuilderEnumInterface.php new file mode 100644 index 00000000000..9bbaa5215b0 --- /dev/null +++ b/src/GraphQl/Type/TypeBuilderEnumInterface.php @@ -0,0 +1,57 @@ + + * + * 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\GraphQl\Type; + +use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use GraphQL\Type\Definition\InterfaceType; +use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type as GraphQLType; +use Symfony\Component\PropertyInfo\Type; + +/** + * Interface implemented to build a GraphQL type. + * + * @author Alan Poulain + */ +interface TypeBuilderEnumInterface +{ + /** + * Gets the object type of the given resource. + * + * @return ObjectType|NonNull the object type, possibly wrapped by NonNull + */ + public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType; + + /** + * Get the interface type of a node. + */ + public function getNodeInterface(): InterfaceType; + + /** + * Gets the type of a paginated collection of the given resource type. + */ + public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType; + + /** + * Gets the type corresponding to an enum. + */ + public function getEnumType(Operation $operation): GraphQLType; + + /** + * Returns true if a type is a collection. + */ + public function isCollection(Type $type): bool; +} diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php index 50bb0077893..8b782e32461 100644 --- a/src/GraphQl/Type/TypeBuilderInterface.php +++ b/src/GraphQl/Type/TypeBuilderInterface.php @@ -25,6 +25,8 @@ * Interface implemented to build a GraphQL type. * * @author Alan Poulain + * + * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface instead. */ interface TypeBuilderInterface { @@ -42,6 +44,8 @@ public function getNodeInterface(): InterfaceType; /** * Gets the type of a paginated collection of the given resource type. + * + * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface::getPaginatedCollectionType() method instead. */ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType; diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index 6ba6df9192e..f868962c6ae 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -37,8 +37,11 @@ */ final class TypeConverter implements TypeConverterInterface { - public function __construct(private readonly TypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) + public function __construct(private readonly TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) { + if ($typeBuilder instanceof TypeBuilderInterface) { + @trigger_error(sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); + } } /** @@ -67,7 +70,28 @@ public function convertType(Type $type, bool $input, Operation $rootOperation, s return GraphQLType::string(); } - return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth); + $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; default: return null; } diff --git a/tests/Fixtures/TestBundle/Document/Person.php b/tests/Fixtures/TestBundle/Document/Person.php index f7d11a309aa..21d287dc6ad 100644 --- a/tests/Fixtures/TestBundle/Document/Person.php +++ b/tests/Fixtures/TestBundle/Document/Person.php @@ -32,13 +32,14 @@ class Person #[ODM\Id(strategy: 'INCREMENT', type: 'int')] private ?int $id = null; + #[ODM\Field(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + #[Groups(['people.pets'])] + public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; + #[Groups(['people.pets'])] #[ODM\Field(type: 'string')] public string $name; - #[ODM\Field(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] - public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; - #[Groups(['people.pets'])] #[ODM\ReferenceMany(targetDocument: PersonToPet::class, mappedBy: 'person')] public Collection|iterable $pets; diff --git a/tests/Fixtures/TestBundle/Entity/Person.php b/tests/Fixtures/TestBundle/Entity/Person.php index 775969b2786..809da04a5b6 100644 --- a/tests/Fixtures/TestBundle/Entity/Person.php +++ b/tests/Fixtures/TestBundle/Entity/Person.php @@ -35,6 +35,7 @@ class Person private ?int $id = null; #[ORM\Column(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + #[Groups(['people.pets'])] public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; #[ORM\Column(type: 'string')] diff --git a/tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php b/tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php new file mode 100644 index 00000000000..301d159815a --- /dev/null +++ b/tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php @@ -0,0 +1,29 @@ + + * + * 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; + +enum EnumWithDescriptions +{ + /** + * A short description for case one. + */ + case ONE; + + /** + * A short description for case two. + * + * A long description for case two. + */ + case TWO; +} diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index 5313db3537d..853b962223f 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -17,7 +17,7 @@ use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\FieldsBuilder; -use ApiPlatform\GraphQl\Type\TypeBuilderInterface; +use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface; use ApiPlatform\GraphQl\Type\TypeConverterInterface; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiProperty; @@ -33,6 +33,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; @@ -78,7 +79,7 @@ protected function setUp(): void $this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); - $this->typeBuilderProphecy = $this->prophesize(TypeBuilderInterface::class); + $this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class); $this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class); $this->itemResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->collectionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); @@ -207,7 +208,7 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); + $this->typeBuilderProphecy->getPaginatedCollectionType($graphqlType, $operation)->willReturn($graphqlType); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -827,6 +828,25 @@ public function resourceObjectTypeFieldsProvider(): array ]; } + public function testGetEnumFields(): void + { + $enumClass = GenderTypeEnum::class; + + $this->propertyMetadataFactoryProphecy->create($enumClass, GenderTypeEnum::MALE->name)->willReturn(new ApiProperty( + description: 'Description of MALE case', + )); + $this->propertyMetadataFactoryProphecy->create($enumClass, GenderTypeEnum::FEMALE->name)->willReturn(new ApiProperty( + description: 'Description of FEMALE case', + )); + + $enumFields = $this->fieldsBuilder->getEnumFields($enumClass); + + $this->assertSame([ + GenderTypeEnum::MALE->name => ['value' => GenderTypeEnum::MALE->value, 'description' => 'Description of MALE case'], + GenderTypeEnum::FEMALE->name => ['value' => GenderTypeEnum::FEMALE->value, 'description' => 'Description of FEMALE case'], + ], $enumFields); + } + /** * @dataProvider resolveResourceArgsProvider */ diff --git a/tests/GraphQl/Type/SchemaBuilderTest.php b/tests/GraphQl/Type/SchemaBuilderTest.php index 294d5312c3d..1850951d402 100644 --- a/tests/GraphQl/Type/SchemaBuilderTest.php +++ b/tests/GraphQl/Type/SchemaBuilderTest.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Tests\GraphQl\Type; -use ApiPlatform\GraphQl\Type\FieldsBuilderInterface; +use ApiPlatform\GraphQl\Type\FieldsBuilderEnumInterface; use ApiPlatform\GraphQl\Type\SchemaBuilder; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\GraphQl\Type\TypesFactoryInterface; @@ -42,15 +42,10 @@ class SchemaBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $resourceNameCollectionFactoryProphecy; - private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $typesFactoryProphecy; - private ObjectProphecy $typesContainerProphecy; - private ObjectProphecy $fieldsBuilderProphecy; - private SchemaBuilder $schemaBuilder; /** @@ -62,7 +57,7 @@ protected function setUp(): void $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->typesFactoryProphecy = $this->prophesize(TypesFactoryInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); - $this->fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $this->fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $this->schemaBuilder = new SchemaBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->typesFactoryProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->fieldsBuilderProphecy->reveal()); } diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index ee6dc591d4e..0eed16b666f 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Tests\GraphQl\Type; use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Type\FieldsBuilderInterface; +use ApiPlatform\GraphQl\Type\FieldsBuilderEnumInterface; use ApiPlatform\GraphQl\Type\TypeBuilder; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiResource; @@ -26,6 +26,8 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\Pagination; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; @@ -48,12 +50,9 @@ class TypeBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $typesContainerProphecy; - /** @var callable */ private $defaultFieldResolver; - private ObjectProphecy $fieldsBuilderLocatorProphecy; - private TypeBuilder $typeBuilder; /** @@ -93,7 +92,7 @@ public function testGetResourceObjectType(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -119,7 +118,7 @@ public function testGetResourceObjectTypeOutputClass(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('outputClass', $operation, false, 0, ['class' => 'outputClass'])->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -188,7 +187,7 @@ public function testGetResourceObjectTypeInput(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -214,7 +213,7 @@ public function testGetResourceObjectTypeNestedInput(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -240,7 +239,7 @@ public function testGetResourceObjectTypeCustomMutationInputArgs(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null) ->shouldBeCalled()->willReturn(['clientMutationId' => GraphQLType::string()]); $fieldsBuilderProphecy->resolveResourceArgs([], $operation)->shouldBeCalled(); @@ -320,7 +319,7 @@ public function testGetResourceObjectTypeMutationWrappedType(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -344,7 +343,7 @@ public function testGetResourceObjectTypeMutationNested(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -425,7 +424,7 @@ public function testGetResourceObjectTypeSubscriptionWrappedType(): void $this->assertArrayHasKey('interfaces', $wrappedType->config); $this->assertArrayHasKey('fields', $wrappedType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); @@ -449,7 +448,7 @@ public function testGetResourceObjectTypeSubscriptionNested(): void $this->assertArrayHasKey('interfaces', $resourceObjectType->config); $this->assertArrayHasKey('fields', $resourceObjectType->config); - $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderInterface::class); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); @@ -477,7 +476,7 @@ public function testGetNodeInterface(): void $this->assertSame(GraphQLType::string(), $resolvedType); } - public function testCursorBasedGetResourcePaginatedCollectionType(): void + public function testCursorBasedGetPaginatedCollectionType(): void { /** @var Operation $operation */ $operation = (new Query())->withPaginationType('cursor'); @@ -487,7 +486,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getPaginatedCollectionType(GraphQLType::string(), $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -533,7 +532,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->assertSame(GraphQLType::int(), $totalCountType->getWrappedType()); } - public function testPageBasedGetResourcePaginatedCollectionType(): void + public function testPageBasedGetPaginatedCollectionType(): void { /** @var Operation $operation */ $operation = (new Query())->withPaginationType('page'); @@ -542,7 +541,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getPaginatedCollectionType(GraphQLType::string(), $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); @@ -568,6 +567,35 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->assertSame(GraphQLType::int(), $paginationInfoObjectTypeFields['totalCount']->getType()->getWrappedType()); } + public function testGetEnumType(): void + { + $enumClass = GamePlayMode::class; + $enumName = 'GamePlayMode'; + $enumDescription = 'GamePlayModeEnum description'; + /** @var Operation $operation */ + $operation = (new Operation()) + ->withClass($enumClass) + ->withShortName($enumName) + ->withDescription('GamePlayModeEnum description'); + + $this->typesContainerProphecy->has('GamePlayModeEnum')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('GamePlayModeEnum', Argument::type(EnumType::class))->shouldBeCalled(); + $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); + $enumValues = [ + GamePlayMode::CO_OP->name => ['value' => GamePlayMode::CO_OP->value], + GamePlayMode::MULTI_PLAYER->name => ['value' => GamePlayMode::MULTI_PLAYER->value], + GamePlayMode::SINGLE_PLAYER->name => ['value' => GamePlayMode::SINGLE_PLAYER->value, 'description' => 'Which is played by a lone player.'], + ]; + $fieldsBuilderProphecy->getEnumFields($enumClass)->willReturn($enumValues); + $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->willReturn($fieldsBuilderProphecy->reveal()); + + self::assertEquals(new EnumType([ + 'name' => $enumName, + 'description' => $enumDescription, + 'values' => $enumValues, + ]), $this->typeBuilder->getEnumType($operation)); + } + /** * @dataProvider typesProvider */ diff --git a/tests/GraphQl/Type/TypeConverterTest.php b/tests/GraphQl/Type/TypeConverterTest.php index 6e7634b7900..500fdd03921 100644 --- a/tests/GraphQl/Type/TypeConverterTest.php +++ b/tests/GraphQl/Type/TypeConverterTest.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Tests\GraphQl\Type; use ApiPlatform\Exception\ResourceClassNotFoundException; -use ApiPlatform\GraphQl\Type\TypeBuilderInterface; +use ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface; use ApiPlatform\GraphQl\Type\TypeConverter; use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiProperty; @@ -25,7 +25,9 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Type\Definition\DateTimeType; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type as GraphQLType; use PHPUnit\Framework\TestCase; @@ -42,13 +44,9 @@ class TypeConverterTest extends TestCase use ProphecyTrait; private ObjectProphecy $typeBuilderProphecy; - private ObjectProphecy $typesContainerProphecy; - private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $propertyMetadataFactoryProphecy; - private TypeConverter $typeConverter; /** @@ -56,7 +54,7 @@ class TypeConverterTest extends TestCase */ protected function setUp(): void { - $this->typeBuilderProphecy = $this->prophesize(TypeBuilderInterface::class); + $this->typeBuilderProphecy = $this->prophesize(TypeBuilderEnumInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); @@ -69,6 +67,8 @@ 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->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType); /** @var Operation $operation */ $operation = (new Query())->withName('test'); @@ -86,6 +86,7 @@ public function convertTypeProvider(): array [new Type(Type::BUILTIN_TYPE_ARRAY), false, 0, 'Iterable'], [new Type(Type::BUILTIN_TYPE_ITERABLE), false, 0, 'Iterable'], [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeInterface::class), false, 0, GraphQLType::string()], + [new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class), false, 0, new EnumType(['name' => 'GenderTypeEnum'])], [new Type(Type::BUILTIN_TYPE_OBJECT), false, 0, null], [new Type(Type::BUILTIN_TYPE_CALLABLE), false, 0, null], [new Type(Type::BUILTIN_TYPE_NULL), false, 0, null], diff --git a/tests/JsonSchema/SchemaFactoryTest.php b/tests/JsonSchema/SchemaFactoryTest.php index 062a16a8566..415cdbb6d6e 100644 --- a/tests/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonSchema/SchemaFactoryTest.php @@ -118,6 +118,12 @@ public function testBuildSchemaWithSerializerGroups(): void ), Argument::cetera())->willReturn([ 'type' => 'string', ]); + $typeFactoryProphecy->getType(Argument::allOf( + Argument::type(Type::class), + Argument::which('getBuiltinType', Type::BUILTIN_TYPE_OBJECT) + ), Argument::cetera())->willReturn([ + 'type' => 'object', + ]); $shortName = (new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); @@ -135,14 +141,16 @@ public function testBuildSchemaWithSerializerGroups(): void $serializerGroup = 'custom_operation_dummy'; $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(OverriddenOperationDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['alias', 'description'])); + $propertyNameCollectionFactoryProphecy->create(OverriddenOperationDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['alias', 'description', 'genderType'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'alias', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)); $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'description', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)); + $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'genderType', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)])->withReadable(true)->withDefault(GenderTypeEnum::MALE)); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(OverriddenOperationDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(GenderTypeEnum::class)->willReturn(true); $schemaFactory = new SchemaFactory($typeFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), null, $resourceClassResolverProphecy->reveal()); $resultSchema = $schemaFactory->buildSchema(OverriddenOperationDummy::class, 'json', Schema::TYPE_OUTPUT, null, null, ['groups' => $serializerGroup, AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false]); @@ -163,6 +171,11 @@ public function testBuildSchemaWithSerializerGroups(): void $this->assertArrayHasKey('description', $definitions[$rootDefinitionKey]['properties']); $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['description']); $this->assertSame('string', $definitions[$rootDefinitionKey]['properties']['description']['type']); + $this->assertArrayHasKey('genderType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayNotHasKey('default', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayNotHasKey('example', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']); } public function testBuildSchemaForAssociativeArray(): void