diff --git a/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php b/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php
index 523168f10bd..33409aeb11d 100644
--- a/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php
+++ b/src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php
@@ -59,15 +59,11 @@ public function create(string $resourceClass, string $property, array $options =
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);
- }
+ if ($reflectionEnum && $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);
}
if ($reflectionClass->hasProperty($property)) {
@@ -79,11 +75,11 @@ public function create(string $resourceClass, string $property, array $options =
foreach (array_merge(Reflection::ACCESSOR_PREFIXES, Reflection::MUTATOR_PREFIXES) as $prefix) {
$methodName = $prefix.ucfirst($property);
- if (!$reflectionClass->hasMethod($methodName)) {
+ if (!$reflectionClass->hasMethod($methodName) && !$reflectionEnum?->hasMethod($methodName)) {
continue;
}
- $reflectionMethod = $reflectionClass->getMethod($methodName);
+ $reflectionMethod = $reflectionClass->hasMethod($methodName) ? $reflectionClass->getMethod($methodName) : $reflectionEnum?->getMethod($methodName);
if (!$reflectionMethod->isPublic()) {
continue;
}
diff --git a/src/Metadata/Resource/Factory/BackedEnumResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/BackedEnumResourceMetadataCollectionFactory.php
new file mode 100644
index 00000000000..9f7411b1c3b
--- /dev/null
+++ b/src/Metadata/Resource/Factory/BackedEnumResourceMetadataCollectionFactory.php
@@ -0,0 +1,67 @@
+
+ *
+ * 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\Metadata\Resource\Factory;
+
+use ApiPlatform\Metadata\Operations;
+use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
+
+/**
+ * Triggers resource deprecations.
+ *
+ * @internal
+ */
+final class BackedEnumResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
+{
+ public const PROVIDER = 'api_platform.state_provider.backed_enum';
+
+ public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated)
+ {
+ }
+
+ public function create(string $resourceClass): ResourceMetadataCollection
+ {
+ $resourceMetadataCollection = $this->decorated->create($resourceClass);
+ if (!is_a($resourceClass, \BackedEnum::class, true)) {
+ return $resourceMetadataCollection;
+ }
+
+ foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
+ $newOperations = [];
+ foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
+ $newOperations[$operationName] = $operation;
+
+ if (null !== $operation->getProvider()) {
+ continue;
+ }
+
+ $newOperations[$operationName] = $operation->withProvider(self::PROVIDER);
+ }
+
+ $newGraphQlOperations = [];
+ foreach ($resourceMetadata->getGraphQlOperations() as $operationName => $operation) {
+ $newGraphQlOperations[$operationName] = $operation;
+
+ if (null !== $operation->getProvider()) {
+ continue;
+ }
+
+ $newGraphQlOperations[$operationName] = $operation->withProvider(self::PROVIDER);
+ }
+
+ $resourceMetadataCollection[$i] = $resourceMetadata->withOperations(new Operations($newOperations))->withGraphQlOperations($newGraphQlOperations);
+ }
+
+ return $resourceMetadataCollection;
+ }
+}
diff --git a/src/Metadata/Resource/Factory/LinkFactory.php b/src/Metadata/Resource/Factory/LinkFactory.php
index 3420aa4647c..95fecdd95b2 100644
--- a/src/Metadata/Resource/Factory/LinkFactory.php
+++ b/src/Metadata/Resource/Factory/LinkFactory.php
@@ -59,6 +59,9 @@ public function createLinksFromIdentifiers(Metadata $operation): array
$link = (new Link())->withFromClass($resourceClass)->withIdentifiers($identifiers);
$parameterName = $identifiers[0];
+ if ('value' === $parameterName && enum_exists($resourceClass)) {
+ $parameterName = 'id';
+ }
if (1 < \count($identifiers)) {
$parameterName = 'id';
@@ -155,6 +158,10 @@ private function getIdentifiersFromResourceClass(string $resourceClass): array
return ['id'];
}
+ if (!$hasIdProperty && !$identifiers && enum_exists($resourceClass)) {
+ return ['value'];
+ }
+
return $identifiers;
}
diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php
index 8814c5b9214..c21f71ea81a 100644
--- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php
+++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php
@@ -88,6 +88,10 @@ private function getResourceWithDefaults(string $resourceClass, string $shortNam
private function getDefaultHttpOperations($resource): iterable
{
+ if (enum_exists($resource->getClass())) {
+ return new Operations([new GetCollection(paginationEnabled: false), new Get()]);
+ }
+
if (($defaultOperations = $this->defaults['operations'] ?? null) && null === $resource->getOperations()) {
$operations = [];
@@ -108,8 +112,9 @@ private function getDefaultHttpOperations($resource): iterable
private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource
{
+ $operations = enum_exists($resource->getClass()) ? [new QueryCollection(paginationEnabled: false), new Query()] : [new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')];
$graphQlOperations = [];
- foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) {
+ foreach ($operations as $operation) {
[$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
$graphQlOperations[$key] = $operation;
}
diff --git a/src/State/Provider/BackedEnumProvider.php b/src/State/Provider/BackedEnumProvider.php
new file mode 100644
index 00000000000..d5641fb2719
--- /dev/null
+++ b/src/State/Provider/BackedEnumProvider.php
@@ -0,0 +1,69 @@
+
+ *
+ * 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\State\Provider;
+
+use ApiPlatform\Metadata\CollectionOperationInterface;
+use ApiPlatform\Metadata\Exception\RuntimeException;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+final class BackedEnumProvider implements ProviderInterface
+{
+ public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
+ {
+ $resourceClass = $operation->getClass();
+ if (!$resourceClass || !is_a($resourceClass, \BackedEnum::class, true)) {
+ throw new RuntimeException('This resource is not an enum');
+ }
+
+ if ($operation instanceof CollectionOperationInterface) {
+ return $resourceClass::cases();
+ }
+
+ $id = $uriVariables['id'] ?? null;
+ if (null === $id) {
+ throw new NotFoundHttpException('Not Found');
+ }
+
+ if ($enum = $this->resolveEnum($resourceClass, $id)) {
+ return $enum;
+ }
+
+ throw new NotFoundHttpException('Not Found');
+ }
+
+ /**
+ * @param class-string $resourceClass
+ */
+ private function resolveEnum(string $resourceClass, string|int $id): ?\BackedEnum
+ {
+ $reflectEnum = new \ReflectionEnum($resourceClass);
+ $type = (string) $reflectEnum->getBackingType();
+
+ if ('int' === $type) {
+ if (!is_numeric($id)) {
+ return null;
+ }
+ $enum = $resourceClass::tryFrom((int) $id);
+ } else {
+ $enum = $resourceClass::tryFrom($id);
+ }
+
+ // @deprecated enums will be indexable only by value in 4.0
+ $enum ??= array_reduce($resourceClass::cases(), static fn ($c, \BackedEnum $case) => $id === $case->name ? $case : $c, null);
+
+ return $enum;
+ }
+}
diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml
index 04d8fa2016f..2fd6edfdca3 100644
--- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml
+++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml
@@ -29,6 +29,10 @@
+
+
+
+
diff --git a/src/Symfony/Bundle/Resources/config/state/provider.xml b/src/Symfony/Bundle/Resources/config/state/provider.xml
index 56a42f70f87..47f56f4aea1 100644
--- a/src/Symfony/Bundle/Resources/config/state/provider.xml
+++ b/src/Symfony/Bundle/Resources/config/state/provider.xml
@@ -25,6 +25,11 @@
+
+
+
+
+
api_platform.symfony.main_controller
diff --git a/tests/Fixtures/TestBundle/ApiResource/BackedEnumIntegerResource.php b/tests/Fixtures/TestBundle/ApiResource/BackedEnumIntegerResource.php
new file mode 100644
index 00000000000..7a1a15757ff
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/BackedEnumIntegerResource.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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\ApiResource;
+
+use ApiPlatform\Metadata\ApiResource;
+
+#[ApiResource]
+enum BackedEnumIntegerResource: int
+{
+ case Yes = 1;
+ case No = 2;
+ case Maybe = 3;
+
+ public function getDescription(): string
+ {
+ return match ($this) {
+ self::Yes => 'We say yes',
+ self::No => 'Computer says no',
+ self::Maybe => 'Let me think about it',
+ };
+ }
+}
diff --git a/tests/Fixtures/TestBundle/ApiResource/BackedEnumStringResource.php b/tests/Fixtures/TestBundle/ApiResource/BackedEnumStringResource.php
new file mode 100644
index 00000000000..2066187ed63
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/BackedEnumStringResource.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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\ApiResource;
+
+use ApiPlatform\Metadata\ApiResource;
+
+#[ApiResource]
+enum BackedEnumStringResource: string
+{
+ case Yes = 'yes';
+ case No = 'no';
+ case Maybe = 'maybe';
+
+ public function getDescription(): string
+ {
+ return match ($this) {
+ self::Yes => 'We say yes',
+ self::No => 'Computer says no',
+ self::Maybe => 'Let me think about it',
+ };
+ }
+}
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6264/Availability.php b/tests/Fixtures/TestBundle/ApiResource/Issue6264/Availability.php
index a714307a13c..fecf4cdabfe 100644
--- a/tests/Fixtures/TestBundle/ApiResource/Issue6264/Availability.php
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6264/Availability.php
@@ -16,10 +16,18 @@
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\GraphQl\Query;
-#[ApiResource(normalizationContext: ['groups' => ['get']])]
-#[GetCollection(provider: Availability::class.'::getCases')]
-#[Get(provider: Availability::class.'::getCase')]
+#[ApiResource(
+ normalizationContext: ['groups' => ['get']],
+ operations: [
+ new GetCollection(provider: Availability::class.'::getCases'),
+ new Get(provider: Availability::class.'::getCase'),
+ ],
+ graphQlOperations: [
+ new Query(provider: Availability::class.'getCase'),
+ ]
+)]
enum Availability: int
{
use BackedEnumTrait;
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6264/AvailabilityStatus.php b/tests/Fixtures/TestBundle/ApiResource/Issue6264/AvailabilityStatus.php
index a6825d72e8d..ef406b2a4ce 100644
--- a/tests/Fixtures/TestBundle/ApiResource/Issue6264/AvailabilityStatus.php
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6264/AvailabilityStatus.php
@@ -22,7 +22,7 @@
#[Get(provider: AvailabilityStatus::class.'::getCase')]
enum AvailabilityStatus: string
{
- use BackedEnumTrait;
+ use BackedEnumStringTrait;
case Pending = 'pending';
case Reviewed = 'reviewed';
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumStringTrait.php b/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumStringTrait.php
new file mode 100644
index 00000000000..f6f90be4cc7
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumStringTrait.php
@@ -0,0 +1,49 @@
+
+ *
+ * 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\ApiResource\Issue6264;
+
+use ApiPlatform\Metadata\Operation;
+use Symfony\Component\Serializer\Attribute\Groups;
+
+trait BackedEnumStringTrait
+{
+ public static function values(): array
+ {
+ return array_map(static fn (\BackedEnum $feature) => $feature->value, self::cases());
+ }
+
+ public function getId(): string
+ {
+ return $this->value;
+ }
+
+ #[Groups(['get'])]
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ public static function getCases(): array
+ {
+ return self::cases();
+ }
+
+ /**
+ * @param array $uriVariables
+ */
+ public static function getCase(Operation $operation, array $uriVariables): ?self
+ {
+ return array_reduce(self::cases(), static fn ($c, \BackedEnum $case) => $case->value == $uriVariables['id'] ? $case : $c, null);
+ }
+}
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumTrait.php b/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumTrait.php
index 20b2d60eee2..3bd48c3ebd5 100644
--- a/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumTrait.php
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumTrait.php
@@ -23,13 +23,13 @@ public static function values(): array
return array_map(static fn (\BackedEnum $feature) => $feature->value, self::cases());
}
- public function getId(): string|int
+ public function getId(): int
{
return $this->value;
}
#[Groups(['get'])]
- public function getValue(): string|int
+ public function getValue(): int
{
return $this->value;
}
@@ -39,6 +39,9 @@ public static function getCases(): array
return self::cases();
}
+ /**
+ * @param array $uriVariables
+ */
public static function getCase(Operation $operation, array $uriVariables): ?self
{
return array_reduce(self::cases(), static fn ($c, \BackedEnum $case) => $case->value == $uriVariables['id'] ? $case : $c, null);
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6317/Issue6317.php b/tests/Fixtures/TestBundle/ApiResource/Issue6317/Issue6317.php
new file mode 100644
index 00000000000..3b98bb283bc
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6317/Issue6317.php
@@ -0,0 +1,48 @@
+
+ *
+ * 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\ApiResource\Issue6317;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+
+#[ApiResource]
+enum Issue6317: int
+{
+ case First = 1;
+ case Second = 2;
+
+ #[ApiProperty(identifier: true, example: 'An example of an ID')]
+ public function getId(): int
+ {
+ return $this->value;
+ }
+
+ #[ApiProperty(jsonSchemaContext: ['example' => '/lisa/mary'])]
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ #[ApiProperty(jsonldContext: ['example' => '24'])]
+ public function getOrdinal(): string
+ {
+ return 1 === $this->value ? '1st' : '2nd';
+ }
+
+ #[ApiProperty(openapiContext: ['example' => '42'])]
+ public function getCardinal(): int
+ {
+ return $this->value;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/ApiResource/ResourceWithEnumProperty.php b/tests/Fixtures/TestBundle/ApiResource/ResourceWithEnumProperty.php
new file mode 100644
index 00000000000..f72bda258a2
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/ResourceWithEnumProperty.php
@@ -0,0 +1,58 @@
+
+ *
+ * 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\ApiResource;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum;
+
+#[ApiResource()]
+#[Get(
+ provider: self::class.'::providerItem',
+)]
+#[GetCollection(
+ provider: self::class.'::providerCollection',
+)]
+class ResourceWithEnumProperty
+{
+ public int $id = 1;
+
+ public ?BackedEnumIntegerResource $intEnum = null;
+
+ /** @var BackedEnumStringResource[] */
+ public array $stringEnum = [];
+
+ public ?GenderTypeEnum $gender = null;
+
+ /** @var GenderTypeEnum[] */
+ public array $genders = [];
+
+ public static function providerItem(Operation $operation, array $uriVariables): self
+ {
+ $self = new self();
+ $self->intEnum = BackedEnumIntegerResource::Yes;
+ $self->stringEnum = [BackedEnumStringResource::Maybe, BackedEnumStringResource::No];
+ $self->gender = GenderTypeEnum::FEMALE;
+ $self->genders = [GenderTypeEnum::FEMALE, GenderTypeEnum::MALE];
+
+ return $self;
+ }
+
+ public static function providerCollection(Operation $operation, array $uriVariables): array
+ {
+ return [self::providerItem($operation, $uriVariables)];
+ }
+}
diff --git a/tests/Functional/BackedEnumResourceTest.php b/tests/Functional/BackedEnumResourceTest.php
index e8f78d5d3bc..6bbd2b0f448 100644
--- a/tests/Functional/BackedEnumResourceTest.php
+++ b/tests/Functional/BackedEnumResourceTest.php
@@ -13,7 +13,12 @@
namespace ApiPlatform\Tests\Functional;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Link;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumIntegerResource;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumStringResource;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6264\Availability;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6264\AvailabilityStatus;
use Symfony\Component\HttpClient\HttpOptions;
@@ -94,49 +99,6 @@ public function testItemJson(string $uri, string $mimeType, array $expected): vo
$this->assertJsonEquals($expected);
}
- public static function providerEnumItemsGraphQl(): iterable
- {
- // Integer cases
- $query = <<<'GRAPHQL'
-query GetAvailability($identifier: ID!) {
- availability(id: $identifier) {
- value
- }
-}
-GRAPHQL;
- foreach (Availability::cases() as $case) {
- yield [$query, ['identifier' => '/availabilities/'.$case->value], ['data' => ['availability' => ['value' => $case->value]]]];
- }
-
- // String cases
- $query = <<<'GRAPHQL'
-query GetAvailabilityStatus($identifier: ID!) {
- availabilityStatus(id: $identifier) {
- value
- }
-}
-GRAPHQL;
- foreach (AvailabilityStatus::cases() as $case) {
- yield [$query, ['identifier' => '/availability_statuses/'.$case->value], ['data' => ['availability_status' => ['value' => $case->value]]]];
- }
- }
-
- /**
- * @dataProvider providerEnumItemsGraphQl
- *
- * @group legacy
- */
- public function testItemGraphql(string $query, array $variables, array $expected): void
- {
- $options = (new HttpOptions())
- ->setJson(['query' => $query, 'variables' => $variables])
- ->setHeaders(['Content-Type' => 'application/json']);
- self::createClient()->request('POST', '/graphql', $options->toArray());
-
- $this->assertResponseIsSuccessful();
- $this->assertJsonEquals($expected);
- }
-
public function testCollectionJson(): void
{
self::createClient()->request('GET', '/availabilities', ['headers' => ['Accept' => 'application/json']]);
@@ -258,4 +220,357 @@ public function testCollectionJsonLd(): void
],
]);
}
+
+ public static function providerEnums(): iterable
+ {
+ yield 'Int enum collection' => [BackedEnumIntegerResource::class, GetCollection::class, '_api_/backed_enum_integer_resources{._format}_get_collection'];
+ yield 'Int enum item' => [BackedEnumIntegerResource::class, Get::class, '_api_/backed_enum_integer_resources/{id}{._format}_get'];
+
+ yield 'String enum collection' => [BackedEnumStringResource::class, GetCollection::class, '_api_/backed_enum_string_resources{._format}_get_collection'];
+ yield 'String enum item' => [BackedEnumStringResource::class, Get::class, '_api_/backed_enum_string_resources/{id}{._format}_get'];
+ }
+
+ /** @dataProvider providerEnums */
+ public function testOnlyGetOperationsAddedWhenNonSpecified(string $resourceClass, string $operationClass, string $operationName): void
+ {
+ $factory = self::getContainer()->get('api_platform.metadata.resource.metadata_collection_factory');
+ $resourceMetadata = $factory->create($resourceClass);
+
+ $this->assertCount(1, $resourceMetadata);
+ $resource = $resourceMetadata[0];
+ $operations = iterator_to_array($resource->getOperations());
+ $this->assertCount(2, $operations);
+
+ $this->assertInstanceOf($operationClass, $operations[$operationName]);
+ }
+
+ public function testEnumsAreAssignedValuePropertyAsIdentifierByDefault(): void
+ {
+ $linkFactory = self::getContainer()->get('api_platform.metadata.resource.link_factory');
+ $result = $linkFactory->completeLink(new Link(fromClass: BackedEnumIntegerResource::class));
+ $identifiers = $result->getIdentifiers();
+
+ $this->assertCount(1, $identifiers);
+ $this->assertNotContains('id', $identifiers);
+ $this->assertContains('value', $identifiers);
+ }
+
+ public static function providerCollection(): iterable
+ {
+ yield 'JSON' => ['application/json', [
+ [
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ],
+ [
+ 'name' => 'No',
+ 'value' => 2,
+ 'description' => 'Computer says no',
+ ],
+ [
+ 'name' => 'Maybe',
+ 'value' => 3,
+ 'description' => 'Let me think about it',
+ ],
+ ]];
+
+ yield 'JSON:API' => ['application/vnd.api+json', [
+ 'links' => [
+ 'self' => '/backed_enum_integer_resources',
+ ],
+ 'meta' => [
+ 'totalItems' => 3,
+ ],
+ 'data' => [
+ [
+ 'id' => '/backed_enum_integer_resources/1',
+ 'type' => 'BackedEnumIntegerResource',
+ 'attributes' => [
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ],
+ ],
+ [
+ 'id' => '/backed_enum_integer_resources/2',
+ 'type' => 'BackedEnumIntegerResource',
+ 'attributes' => [
+ 'name' => 'No',
+ 'value' => 2,
+ 'description' => 'Computer says no',
+ ],
+ ],
+ [
+ 'id' => '/backed_enum_integer_resources/3',
+ 'type' => 'BackedEnumIntegerResource',
+ 'attributes' => [
+ 'name' => 'Maybe',
+ 'value' => 3,
+ 'description' => 'Let me think about it',
+ ],
+ ],
+ ],
+ ]];
+
+ yield 'LD+JSON' => ['application/ld+json', [
+ '@context' => '/contexts/BackedEnumIntegerResource',
+ '@id' => '/backed_enum_integer_resources',
+ '@type' => 'hydra:Collection',
+ 'hydra:totalItems' => 3,
+ 'hydra:member' => [
+ [
+ '@id' => '/backed_enum_integer_resources/1',
+ '@type' => 'BackedEnumIntegerResource',
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ],
+ [
+ '@id' => '/backed_enum_integer_resources/2',
+ '@type' => 'BackedEnumIntegerResource',
+ 'name' => 'No',
+ 'value' => 2,
+ 'description' => 'Computer says no',
+ ],
+ [
+ '@id' => '/backed_enum_integer_resources/3',
+ '@type' => 'BackedEnumIntegerResource',
+ 'name' => 'Maybe',
+ 'value' => 3,
+ 'description' => 'Let me think about it',
+ ],
+ ],
+ ]];
+
+ yield 'HAL+JSON' => ['application/hal+json', [
+ '_links' => [
+ 'self' => [
+ 'href' => '/backed_enum_integer_resources',
+ ],
+ 'item' => [
+ [
+ 'href' => '/backed_enum_integer_resources/1',
+ ],
+ [
+ 'href' => '/backed_enum_integer_resources/2',
+ ],
+ [
+ 'href' => '/backed_enum_integer_resources/3',
+ ],
+ ],
+ ],
+ 'totalItems' => 3,
+ '_embedded' => [
+ 'item' => [
+ [
+ '_links' => [
+ 'self' => [
+ 'href' => '/backed_enum_integer_resources/1',
+ ],
+ ],
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ],
+ [
+ '_links' => [
+ 'self' => [
+ 'href' => '/backed_enum_integer_resources/2',
+ ],
+ ],
+ 'name' => 'No',
+ 'value' => 2,
+ 'description' => 'Computer says no',
+ ],
+ [
+ '_links' => [
+ 'self' => [
+ 'href' => '/backed_enum_integer_resources/3',
+ ],
+ ],
+ 'name' => 'Maybe',
+ 'value' => 3,
+ 'description' => 'Let me think about it',
+ ],
+ ],
+ ],
+ ]];
+ }
+
+ /** @dataProvider providerCollection */
+ public function testCollection(string $mimeType, array $expected): void
+ {
+ self::createClient()->request('GET', '/backed_enum_integer_resources', ['headers' => ['Accept' => $mimeType]]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertJsonEquals($expected);
+ }
+
+ public static function providerItem(): iterable
+ {
+ yield 'JSON' => ['application/json', [
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ]];
+
+ yield 'JSON:API' => ['application/vnd.api+json', [
+ 'data' => [
+ 'id' => '/backed_enum_integer_resources/1',
+ 'type' => 'BackedEnumIntegerResource',
+ 'attributes' => [
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ],
+ ],
+ ]];
+
+ yield 'JSON:HAL' => ['application/hal+json', [
+ '_links' => [
+ 'self' => [
+ 'href' => '/backed_enum_integer_resources/1',
+ ],
+ ],
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ]];
+
+ yield 'LD+JSON' => ['application/ld+json', [
+ '@context' => '/contexts/BackedEnumIntegerResource',
+ '@id' => '/backed_enum_integer_resources/1',
+ '@type' => 'BackedEnumIntegerResource',
+ 'name' => 'Yes',
+ 'value' => 1,
+ 'description' => 'We say yes',
+ ]];
+ }
+
+ /** @dataProvider providerItem */
+ public function testItem(string $mimeType, array $expected): void
+ {
+ self::createClient()->request('GET', '/backed_enum_integer_resources/1', ['headers' => ['Accept' => $mimeType]]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertJsonEquals($expected);
+ }
+
+ public static function provider404s(): iterable
+ {
+ yield ['/backed_enum_integer_resources/42'];
+ yield ['/backed_enum_integer_resources/fortytwo'];
+ }
+
+ /** @dataProvider provider404s */
+ public function testItem404(string $uri): void
+ {
+ self::createClient()->request('GET', $uri);
+
+ $this->assertResponseStatusCodeSame(404);
+ }
+
+ public static function providerEnumItemsGraphQl(): iterable
+ {
+ // Integer cases
+ $query = <<<'GRAPHQL'
+query GetAvailability($identifier: ID!) {
+ availability(id: $identifier) {
+ value
+ }
+}
+GRAPHQL;
+ foreach (Availability::cases() as $case) {
+ yield [$query, ['identifier' => '/availabilities/'.$case->value], ['data' => ['availability' => ['value' => $case->value]]]];
+ }
+
+ // String cases
+ $query = <<<'GRAPHQL'
+query GetAvailabilityStatus($identifier: ID!) {
+ availabilityStatus(id: $identifier) {
+ value
+ }
+}
+GRAPHQL;
+ foreach (AvailabilityStatus::cases() as $case) {
+ yield [$query, ['identifier' => '/availability_statuses/'.$case->value], ['data' => ['availabilityStatus' => ['value' => $case->value]]]];
+ }
+ }
+
+ /**
+ * @dataProvider providerEnumItemsGraphQl
+ *
+ * @group legacy
+ */
+ public function testItemGraphql(string $query, array $variables, array $expected): void
+ {
+ $options = (new HttpOptions())
+ ->setJson(['query' => $query, 'variables' => $variables])
+ ->setHeaders(['Content-Type' => 'application/json']);
+ self::createClient()->request('POST', '/graphql', $options->toArray());
+
+ $this->assertResponseIsSuccessful();
+ $this->assertJsonEquals($expected);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testCollectionGraphQl(): void
+ {
+ $query = <<<'GRAPHQL'
+query {
+ backedEnumIntegerResources {
+ value
+ }
+}
+GRAPHQL;
+ $options = (new HttpOptions())
+ ->setJson(['query' => $query, 'variables' => []])
+ ->setHeaders(['Content-Type' => 'application/json']);
+ self::createClient()->request('POST', '/graphql', $options->toArray());
+
+ $this->assertResponseIsSuccessful();
+ $this->assertJsonEquals([
+ 'data' => [
+ 'backedEnumIntegerResources' => [
+ ['value' => 1],
+ ['value' => 2],
+ ['value' => 3],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testItemGraphQlInteger(): void
+ {
+ $query = <<<'GRAPHQL'
+query GetBackedEnumIntegerResource($identifier: ID!) {
+ backedEnumIntegerResource(id: $identifier) {
+ name
+ value
+ description
+ }
+}
+GRAPHQL;
+ $options = (new HttpOptions())
+ ->setJson(['query' => $query, 'variables' => ['identifier' => '/backed_enum_integer_resources/1']])
+ ->setHeaders(['Content-Type' => 'application/json']);
+ self::createClient()->request('POST', '/graphql', $options->toArray());
+
+ $this->assertResponseIsSuccessful();
+ $this->assertJsonEquals([
+ 'data' => [
+ 'backedEnumIntegerResource' => [
+ 'description' => 'We say yes',
+ 'name' => 'Yes',
+ 'value' => 1,
+ ],
+ ],
+ ]);
+ }
}
diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php
index 3ef06851574..67248042f5c 100644
--- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php
+++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php
@@ -269,4 +269,67 @@ public function testJsonApiIncludesSchema(): void
$this->assertArrayHasKey('kingdom', $properties['attributes']['properties']);
$this->assertArrayHasKey('phylum', $properties['attributes']['properties']);
}
+
+ /**
+ * Test issue #6317.
+ */
+ public function testBackedEnumExamplesAreNotLost(): void
+ {
+ $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6317\Issue6317', '--type' => 'output', '--format' => 'jsonld']);
+ $result = $this->tester->getDisplay();
+ $json = json_decode($result, associative: true);
+ $properties = $json['definitions']['Issue6317.jsonld']['properties'];
+
+ $this->assertArrayHasKey('example', $properties['id']);
+ $this->assertArrayHasKey('example', $properties['name']);
+ // jsonldContext
+ $this->assertArrayNotHasKey('example', $properties['ordinal']);
+ // openapiContext
+ $this->assertArrayNotHasKey('example', $properties['cardinal']);
+ }
+
+ public function testResourceWithEnumPropertiesSchema(): void
+ {
+ $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ResourceWithEnumProperty', '--type' => 'output', '--format' => 'jsonld']);
+ $result = $this->tester->getDisplay();
+ $json = json_decode($result, associative: true);
+ $properties = $json['definitions']['ResourceWithEnumProperty.jsonld']['properties'];
+
+ $this->assertSame(
+ [
+ 'type' => ['string', 'null'],
+ 'format' => 'iri-reference',
+ 'example' => 'https://example.com/',
+ ],
+ $properties['intEnum']
+ );
+ $this->assertSame(
+ [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'string',
+ 'format' => 'iri-reference',
+ 'example' => 'https://example.com/',
+ ],
+ ],
+ $properties['stringEnum']
+ );
+ $this->assertSame(
+ [
+ 'type' => ['string', 'null'],
+ 'enum' => ['male', 'female', null],
+ ],
+ $properties['gender']
+ );
+ $this->assertSame(
+ [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'string',
+ 'enum' => ['male', 'female'],
+ ],
+ ],
+ $properties['genders']
+ );
+ }
}
diff --git a/tests/State/Provider/BackedEnumProviderTest.php b/tests/State/Provider/BackedEnumProviderTest.php
new file mode 100644
index 00000000000..934744681cb
--- /dev/null
+++ b/tests/State/Provider/BackedEnumProviderTest.php
@@ -0,0 +1,67 @@
+
+ *
+ * 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\State\Provider;
+
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\Provider\BackedEnumProvider;
+use ApiPlatform\State\ProviderInterface;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumIntegerResource;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumStringResource;
+use PHPUnit\Framework\TestCase;
+use Prophecy\Argument;
+use Prophecy\PhpUnit\ProphecyTrait;
+
+final class BackedEnumProviderTest extends TestCase
+{
+ use ProphecyTrait;
+
+ public static function provideCollection(): iterable
+ {
+ yield 'Integer case enum' => [BackedEnumIntegerResource::class, BackedEnumIntegerResource::cases()];
+ yield 'String case enum' => [BackedEnumStringResource::class, BackedEnumStringResource::cases()];
+ }
+
+ /** @dataProvider provideCollection */
+ public function testProvideCollection(string $class, array $expected): void
+ {
+ $operation = new GetCollection(class: $class);
+
+ $this->testProvide($expected, $operation);
+ }
+
+ public static function provideItem(): iterable
+ {
+ yield 'Integer case enum' => [BackedEnumIntegerResource::class, 1, BackedEnumIntegerResource::Yes];
+ yield 'String case enum' => [BackedEnumStringResource::class, 'yes', BackedEnumStringResource::Yes];
+ }
+
+ /** @dataProvider provideItem */
+ public function testProvideItem(string $class, string|int $id, \BackedEnum $expected): void
+ {
+ $operation = new Get(class: $class);
+
+ $this->testProvide($expected, $operation, ['id' => $id]);
+ }
+
+ private function testProvide($expected, Operation $operation, array $uriVariables = [], array $context = []): void
+ {
+ $decorated = $this->prophesize(ProviderInterface::class);
+ $decorated->provide(Argument::any())->shouldNotBeCalled();
+ $provider = new BackedEnumProvider();
+
+ $this->assertSame($expected, $provider->provide($operation, $uriVariables, $context));
+ }
+}
diff --git a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php b/tests/Symfony/Bundle/Command/OpenApiCommandTest.php
index f012a8bcc52..cf68f461997 100644
--- a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php
+++ b/tests/Symfony/Bundle/Command/OpenApiCommandTest.php
@@ -117,6 +117,28 @@ public function testWriteToFile(): void
@unlink($tmpFile);
}
+ /**
+ * Test issue #6317.
+ */
+ public function testBackedEnumExamplesAreNotLost(): void
+ {
+ $this->tester->run(['command' => 'api:openapi:export']);
+ $result = $this->tester->getDisplay();
+ $json = json_decode($result, true, 512, \JSON_THROW_ON_ERROR);
+
+ $assertExample = function (array $properties, string $id): void {
+ $this->assertArrayHasKey('example', $properties[$id]); // default
+ $this->assertArrayHasKey('example', $properties['cardinal']); // openapiContext
+ $this->assertArrayNotHasKey('example', $properties['name']); // jsonSchemaContext
+ $this->assertArrayNotHasKey('example', $properties['ordinal']); // jsonldContext
+ };
+
+ $assertExample($json['components']['schemas']['Issue6317']['properties'], 'id');
+ $assertExample($json['components']['schemas']['Issue6317.jsonld']['properties'], 'id');
+ $assertExample($json['components']['schemas']['Issue6317.jsonapi']['properties']['data']['properties']['attributes']['properties'], '_id');
+ $assertExample($json['components']['schemas']['Issue6317.jsonhal']['properties'], 'id');
+ }
+
private function assertYaml(string $data): void
{
try {