Skip to content

Commit 81e4449

Browse files
feat(state): backed enum provider
1 parent b0288cf commit 81e4449

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\State\Provider;
15+
16+
use ApiPlatform\Metadata\CollectionOperationInterface;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\State\ProviderInterface;
19+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
20+
21+
final class BackedEnumProvider implements ProviderInterface
22+
{
23+
public function __construct(private ProviderInterface $decorated)
24+
{
25+
}
26+
27+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
28+
{
29+
$resourceClass = $operation->getClass();
30+
if (!$resourceClass || !is_a($resourceClass, \BackedEnum::class, true)) {
31+
return $this->decorated->provide($operation, $uriVariables, $context);
32+
}
33+
34+
if ($operation instanceof CollectionOperationInterface) {
35+
return $resourceClass::cases();
36+
}
37+
38+
$id = $uriVariables['id'] ?? $uriVariables['value'] ?? null;
39+
if (null === $id) {
40+
throw new NotFoundHttpException('Not Found');
41+
}
42+
43+
if ($enum = $this->resolveEnum($resourceClass, $id)) {
44+
return $enum;
45+
}
46+
47+
throw new NotFoundHttpException('Not Found');
48+
}
49+
50+
/**
51+
* @param class-string $resourceClass
52+
*/
53+
private function resolveEnum(string $resourceClass, string|int $id): ?\BackedEnum
54+
{
55+
$reflectEnum = new \ReflectionEnum($resourceClass);
56+
$type = (string) $reflectEnum->getBackingType();
57+
58+
if ('int' === $type) {
59+
if (!is_numeric($id)) {
60+
return null;
61+
}
62+
$enum = $resourceClass::tryFrom((int) $id);
63+
} else {
64+
$enum = $resourceClass::tryFrom($id);
65+
}
66+
67+
// @deprecated enums will be indexable only by value in 4.0
68+
$enum ??= array_reduce($resourceClass::cases(), static fn ($c, \BackedEnum $case) => $id === $case->name ? $case : $c, null);
69+
70+
return $enum;
71+
}
72+
}

src/Symfony/Bundle/Resources/config/state/provider.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
3131
</service>
3232

33+
<service id="api_platform.state_provider.backed_enum" class="ApiPlatform\State\Provider\BackedEnumProvider" decorates="api_platform.state_provider.main" decoration-priority="300">
34+
<argument type="service" id="api_platform.state_provider.backed_enum.inner" />
35+
</service>
36+
3337
<service id="api_platform.error_listener" class="ApiPlatform\Symfony\EventListener\ErrorListener">
3438
<argument key="$controller">api_platform.symfony.main_controller</argument>
3539
<argument key="$logger" type="service" id="logger" on-invalid="null" />
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\State\Provider;
15+
16+
use ApiPlatform\Metadata\Get;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\State\Provider\BackedEnumProvider;
20+
use ApiPlatform\State\ProviderInterface;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumIntegerResource;
22+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumStringResource;
23+
use PHPUnit\Framework\TestCase;
24+
use Prophecy\Argument;
25+
use Prophecy\PhpUnit\ProphecyTrait;
26+
27+
final class BackedEnumProviderTest extends TestCase
28+
{
29+
use ProphecyTrait;
30+
31+
public static function provideCollection(): iterable
32+
{
33+
yield 'Integer case enum' => [BackedEnumIntegerResource::class, BackedEnumIntegerResource::cases()];
34+
yield 'String case enum' => [BackedEnumStringResource::class, BackedEnumStringResource::cases()];
35+
}
36+
37+
/** @dataProvider provideCollection */
38+
public function testProvideCollection(string $class, array $expected): void
39+
{
40+
$operation = new GetCollection(class: $class);
41+
42+
$this->testProvide($expected, $operation);
43+
}
44+
45+
public static function provideItem(): iterable
46+
{
47+
yield 'Integer case enum' => [BackedEnumIntegerResource::class, 1, BackedEnumIntegerResource::Yes];
48+
yield 'String case enum' => [BackedEnumStringResource::class, 'yes', BackedEnumStringResource::Yes];
49+
}
50+
51+
/** @dataProvider provideItem */
52+
public function testProvideItem(string $class, string|int $id, \BackedEnum $expected): void
53+
{
54+
$operation = new Get(class: $class);
55+
56+
$this->testProvide($expected, $operation, ['id' => $id]);
57+
}
58+
59+
private function testProvide($expected, Operation $operation, array $uriVariables = [], array $context = []): void
60+
{
61+
$decorated = $this->prophesize(ProviderInterface::class);
62+
$decorated->provide(Argument::any())->shouldNotBeCalled();
63+
$provider = new BackedEnumProvider($decorated->reveal());
64+
65+
$this->assertSame($expected, $provider->provide($operation, $uriVariables, $context));
66+
}
67+
}

0 commit comments

Comments
 (0)