Skip to content

Commit

Permalink
Merge 8acce05 into 1a120ee
Browse files Browse the repository at this point in the history
  • Loading branch information
GwendolenLynch authored Apr 14, 2024
2 parents 1a120ee + 8acce05 commit b51a0d8
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/Metadata/Resource/Factory/LinkFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -155,6 +158,10 @@ private function getIdentifiersFromResourceClass(string $resourceClass): array
return ['id'];
}

if (!$hasIdProperty && !$identifiers && enum_exists($resourceClass)) {
return ['value'];
}

return $identifiers;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Metadata/Resource/Factory/OperationDefaultsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(), new Get()]);
}

if (($defaultOperations = $this->defaults['operations'] ?? null) && null === $resource->getOperations()) {
$operations = [];

Expand Down
72 changes: 72 additions & 0 deletions src/State/Provider/BackedEnumProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Operation;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class BackedEnumProvider implements ProviderInterface
{
public function __construct(private ProviderInterface $decorated)
{
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$resourceClass = $operation->getClass();
if (!$resourceClass || !is_a($resourceClass, \BackedEnum::class, true)) {
return $this->decorated->provide($operation, $uriVariables, $context);
}

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;
}
}
4 changes: 4 additions & 0 deletions src/Symfony/Bundle/Resources/config/state/provider.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
</service>

<service id="api_platform.state_provider.backed_enum" class="ApiPlatform\State\Provider\BackedEnumProvider" decorates="api_platform.state_provider.main" decoration-priority="300">
<argument type="service" id="api_platform.state_provider.backed_enum.inner" />
</service>

<service id="api_platform.error_listener" class="ApiPlatform\Symfony\EventListener\ErrorListener">
<argument key="$controller">api_platform.symfony.main_controller</argument>
<argument key="$logger" type="service" id="logger" on-invalid="null" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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',
};
}
}
33 changes: 33 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/BackedEnumStringResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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',
};
}
}
108 changes: 108 additions & 0 deletions tests/Functional/BackedEnumResourceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\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;

final class BackedEnumResourceTest extends ApiTestCase
{
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 function testCollection(): void
{
self::createClient()->request('GET', '/backed_enum_integer_resources', ['headers' => ['Accept' => 'application/json']]);

$this->assertResponseIsSuccessful();
$this->assertJsonEquals([
[
'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',
],
]);
}

public function testItem(): void
{
self::createClient()->request('GET', '/backed_enum_integer_resources/1', ['headers' => ['Accept' => 'application/json']]);

$this->assertResponseIsSuccessful();
$this->assertJsonEquals([
'name' => 'Yes',
'value' => 1,
'description' => 'We say yes',
]);
}

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);
}
}
67 changes: 67 additions & 0 deletions tests/State/Provider/BackedEnumProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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($decorated->reveal());

$this->assertSame($expected, $provider->provide($operation, $uriVariables, $context));
}
}

0 comments on commit b51a0d8

Please sign in to comment.