diff --git a/composer.json b/composer.json index 4ae60cda619..329333f381d 100644 --- a/composer.json +++ b/composer.json @@ -94,6 +94,7 @@ "symfony/security-bundle": "^6.4 || ^7.0", "symfony/security-core": "^6.4 || ^7.0", "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", "symfony/twig-bundle": "^6.4 || ^7.0", "symfony/uid": "^6.4 || ^7.0", "symfony/validator": "^6.4 || ^7.0", diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6264/Availability.php b/tests/Fixtures/TestBundle/ApiResource/Issue6264/Availability.php new file mode 100644 index 00000000000..a714307a13c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6264/Availability.php @@ -0,0 +1,30 @@ + + * + * 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\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; + +#[ApiResource(normalizationContext: ['groups' => ['get']])] +#[GetCollection(provider: Availability::class.'::getCases')] +#[Get(provider: Availability::class.'::getCase')] +enum Availability: int +{ + use BackedEnumTrait; + + case Available = 0; + case Cancelled = 10; + case Postponed = 200; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6264/AvailabilityStatus.php b/tests/Fixtures/TestBundle/ApiResource/Issue6264/AvailabilityStatus.php new file mode 100644 index 00000000000..a6825d72e8d --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6264/AvailabilityStatus.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\ApiResource\Issue6264; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; + +#[ApiResource(normalizationContext: ['groups' => ['get']])] +#[GetCollection(provider: AvailabilityStatus::class.'::getCases')] +#[Get(provider: AvailabilityStatus::class.'::getCase')] +enum AvailabilityStatus: string +{ + use BackedEnumTrait; + + case Pending = 'pending'; + case Reviewed = 'reviewed'; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumTrait.php b/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumTrait.php new file mode 100644 index 00000000000..20b2d60eee2 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6264/BackedEnumTrait.php @@ -0,0 +1,46 @@ + + * + * 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 BackedEnumTrait +{ + public static function values(): array + { + return array_map(static fn (\BackedEnum $feature) => $feature->value, self::cases()); + } + + public function getId(): string|int + { + return $this->value; + } + + #[Groups(['get'])] + public function getValue(): string|int + { + return $this->value; + } + + public static function getCases(): array + { + return self::cases(); + } + + 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/Functional/BackedEnumPropertyTest.php b/tests/Functional/BackedEnumPropertyTest.php new file mode 100644 index 00000000000..90e1ab3a2ec --- /dev/null +++ b/tests/Functional/BackedEnumPropertyTest.php @@ -0,0 +1,96 @@ + + * + * 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\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Person; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Component\HttpClient\HttpOptions; + +final class BackedEnumPropertyTest extends ApiTestCase +{ + public function testJson(): void + { + $person = $this->createPerson(); + + self::createClient()->request('GET', '/people/'.$person->getId(), ['headers' => ['Accept' => 'application/json']]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + 'genderType' => GenderTypeEnum::FEMALE->value, + 'name' => 'Sonja', + 'academicGrades' => [], + 'pets' => [], + ]); + } + + /** @group legacy */ + public function testGraphQl(): void + { + $person = $this->createPerson(); + + $query = <<<'GRAPHQL' +query GetPerson($identifier: ID!) { + person(id: $identifier) { + genderType + } +} +GRAPHQL; + $options = (new HttpOptions()) + ->setJson(['query' => $query, 'variables' => ['identifier' => '/people/'.$person->getId()]]) + ->setHeaders(['Content-Type' => 'application/json']); + self::createClient()->request('POST', '/graphql', $options->toArray()); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + 'data' => [ + 'person' => [ + 'genderType' => GenderTypeEnum::FEMALE->name, + ], + ], + ]); + } + + private function createPerson(): Person + { + $this->recreateSchema(); + + /** @var EntityManagerInterface $manager */ + $manager = static::getContainer()->get('doctrine')->getManager(); + $person = new Person(); + $person->name = 'Sonja'; + $person->genderType = GenderTypeEnum::FEMALE; + $manager->persist($person); + $manager->flush(); + + return $person; + } + + private function recreateSchema(array $options = []): void + { + self::bootKernel($options); + + /** @var EntityManagerInterface $manager */ + $manager = static::getContainer()->get('doctrine')->getManager(); + /** @var ClassMetadata[] $classes */ + $classes = $manager->getMetadataFactory()->getAllMetadata(); + $schemaTool = new SchemaTool($manager); + + @$schemaTool->dropSchema($classes); + @$schemaTool->createSchema($classes); + } +} diff --git a/tests/Functional/BackedEnumResourceTest.php b/tests/Functional/BackedEnumResourceTest.php new file mode 100644 index 00000000000..e8f78d5d3bc --- /dev/null +++ b/tests/Functional/BackedEnumResourceTest.php @@ -0,0 +1,261 @@ + + * + * 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\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6264\Availability; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6264\AvailabilityStatus; +use Symfony\Component\HttpClient\HttpOptions; + +final class BackedEnumResourceTest extends ApiTestCase +{ + public static function providerEnumItemsJson(): iterable + { + // Integer cases + foreach (Availability::cases() as $case) { + yield ['/availabilities/'.$case->value, 'application/json', ['value' => $case->value]]; + + yield ['/availabilities/'.$case->value, 'application/vnd.api+json', [ + 'data' => [ + 'id' => '/availabilities/'.$case->value, + 'type' => 'Availability', + 'attributes' => [ + 'value' => $case->value, + ], + ], + ]]; + + yield ['/availabilities/'.$case->value, 'application/hal+json', [ + '_links' => [ + 'self' => [ + 'href' => '/availabilities/'.$case->value, + ], + ], + 'value' => $case->value, + ]]; + + yield ['/availabilities/'.$case->value, 'application/ld+json', [ + '@context' => '/contexts/Availability', + '@id' => '/availabilities/'.$case->value, + '@type' => 'Availability', + 'value' => $case->value, + ]]; + } + + // String cases + foreach (AvailabilityStatus::cases() as $case) { + yield ['/availability_statuses/'.$case->value, 'application/json', ['value' => $case->value]]; + + yield ['/availability_statuses/'.$case->value, 'application/vnd.api+json', [ + 'data' => [ + 'id' => '/availability_statuses/'.$case->value, + 'type' => 'AvailabilityStatus', + 'attributes' => [ + 'value' => $case->value, + ], + ], + ]]; + + yield ['/availability_statuses/'.$case->value, 'application/hal+json', [ + '_links' => [ + 'self' => [ + 'href' => '/availability_statuses/'.$case->value, + ], + ], + 'value' => $case->value, + ]]; + + yield ['/availability_statuses/'.$case->value, 'application/ld+json', [ + '@context' => '/contexts/AvailabilityStatus', + '@id' => '/availability_statuses/'.$case->value, + '@type' => 'AvailabilityStatus', + 'value' => $case->value, + ]]; + } + } + + /** @dataProvider providerEnumItemsJson */ + public function testItemJson(string $uri, string $mimeType, array $expected): void + { + self::createClient()->request('GET', $uri, ['headers' => ['Accept' => $mimeType]]); + + $this->assertResponseIsSuccessful(); + $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']]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + ['value' => 0], + ['value' => 10], + ['value' => 200], + ]); + } + + public function testCollectionJsonApi(): void + { + self::createClient()->request('GET', '/availabilities', ['headers' => ['Accept' => 'application/vnd.api+json']]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + 'links' => [ + 'self' => '/availabilities', + ], + 'meta' => [ + 'totalItems' => 3, + ], + 'data' => [ + [ + 'id' => '/availabilities/0', + 'type' => 'Availability', + 'attributes' => [ + 'value' => 0, + ], + ], + [ + 'id' => '/availabilities/10', + 'type' => 'Availability', + 'attributes' => [ + 'value' => 10, + ], + ], + [ + 'id' => '/availabilities/200', + 'type' => 'Availability', + 'attributes' => [ + 'value' => 200, + ], + ], + ], + ]); + } + + public function testCollectionHal(): void + { + self::createClient()->request('GET', '/availabilities', ['headers' => ['Accept' => 'application/hal+json']]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + '_links' => [ + 'self' => [ + 'href' => '/availabilities', + ], + 'item' => [ + ['href' => '/availabilities/0'], + ['href' => '/availabilities/10'], + ['href' => '/availabilities/200'], + ], + ], + 'totalItems' => 3, + '_embedded' => [ + 'item' => [ + [ + '_links' => [ + 'self' => ['href' => '/availabilities/0'], + ], + 'value' => 0, + ], + [ + '_links' => [ + 'self' => ['href' => '/availabilities/10'], + ], + 'value' => 10, + ], + [ + '_links' => [ + 'self' => ['href' => '/availabilities/200'], + ], + 'value' => 200, + ], + ], + ], + ]); + } + + public function testCollectionJsonLd(): void + { + self::createClient()->request('GET', '/availabilities', ['headers' => ['Accept' => 'application/ld+json']]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + '@context' => '/contexts/Availability', + '@id' => '/availabilities', + '@type' => 'hydra:Collection', + 'hydra:totalItems' => 3, + 'hydra:member' => [ + [ + '@id' => '/availabilities/0', + '@type' => 'Availability', + 'value' => 0, + ], + [ + '@id' => '/availabilities/10', + '@type' => 'Availability', + 'value' => 10, + ], + [ + '@id' => '/availabilities/200', + '@type' => 'Availability', + 'value' => 200, + ], + ], + ]); + } +}