Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: BackedEnum resources #6309

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
}
Expand Down
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';
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this match what you had in mind @soyuka?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uri variable should be as followed:

uriVariables: ['id' => new Link(parameterName: 'id', identifiers: ['value'])]

in the metadata directly so that we don't need any condition here right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, but I'm confused again 😇 … I'm probably missing something obvious, but the Link is constructed directly above, and the parameterName is set at return: $link->withParameterName($parameterName) … So is your suggestion how that return array should be shaped? If so what about non-enums?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which raises another thing about the createLinksFromIdentifiers method … If the $identifier array is empty there is an early return (lines 56-58), and then there is the if (1 < \count($identifiers)) { (lines 66-69) test that looks like dead code, correct?

Scrap this one, I was reading the comparison backwards in my head … confusion reigns today 😟

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to check this into the details, will try to find some time!


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
7 changes: 6 additions & 1 deletion 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(paginationEnabled: false), new Get()]);
}

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

Expand All @@ -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;
}
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/graphql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@
<service id="api_platform.graphql.state_provider" alias="api_platform.state_provider.locator" />
<service id="api_platform.graphql.state_processor" alias="api_platform.graphql.state_processor.normalize" />

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

<service id="api_platform.graphql.state_provider.read" class="ApiPlatform\GraphQl\State\Provider\ReadProvider" decorates="api_platform.graphql.state_provider" decoration-priority="500">
<argument type="service" id="api_platform.graphql.state_provider.read.inner" />
<argument type="service" id="api_platform.symfony.iri_converter" />
Expand Down
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 @@ -25,6 +25,10 @@
<argument type="service" id="translator" on-invalid="null" />
</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',
};
}
}
48 changes: 48 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/Issue6317/Issue6317.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?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\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;
}
}
58 changes: 58 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/ResourceWithEnumProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?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;
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)];
}
}