-
-
Notifications
You must be signed in to change notification settings - Fork 892
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
Support for Enums #2254
Comments
Not sure if anything can be simplify for a custom class considered as an enum. At least, it may add support for an enum library like this wonderful one? https://github.com/greg0ire/enum :-) |
The Schema Generator component already supports https://github.com/myclabs/php-enum, if we add support for an enum lib in core, it would be better to start with the same one for consistency. Note: I've nothing against greg0ire's one! |
Totally think we should be using a standard package like myclabs/php-enum, I happened to not in my app for other reasons. |
You can also use DoctrineEnumType for the part on Doctrine |
@dunglas I didn't know about already enable support. So it makes sense to use the same library, indeed. |
See also: https://github.com/elao/PhpEnums
|
@alanpoulain based on their readme, they have also added: |
symfony/symfony#40241 waiting for this then we're adding them |
Been looking around, found this issue and that the Doctrine team is also working on support from the ORM itself. Thought you guys might like that here :) There's also an interesting discussion (leading to the earlier PR). |
Hi guys, ORM folks worked real quick. https://github.com/doctrine/orm/pull/9304/files is now merged. Updated your composer dependency to: Bug that I've run into: item normalization does not recognize enums, so for POST / PUT methods using enums does not work out of the box. Test Backed Enum: enum Alpha2: string
{
case EN = 'en';
case NL = 'nl';
} Entity using BackedEnum: class Message implements Entity
{
private int $id;
public UuidInterface $uuid;
public function __construct(
public Alpha2 $language,
public string $code,
public string $text
) {
$this->uuid = Uuid::uuid4();
}
/// stuff
} Doctrine Entity mapping: <?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="App\Message\Message" table="messages">
<id name="id" type="integer">
<generator strategy="IDENTITY"/>
<options>
<option name="unsigned">1</option>
</options>
</id>
<field name="uuid" type="uuid" unique="true"/>
<field name="language" type="string" length="2" enum-type="App\Language\Alpha2"/>
<field name="code"/>
<field name="text"/>
<unique-constraints>
<unique-constraint columns="language,code" name="unique_code_per_language" />
</unique-constraints>
</entity>
</doctrine-mapping> (Notice in the above: When data is populated with fixtures, there is no problem. My assumption is because when doing a When running requests of POST and PUT it fails on normalization. Beginning of output:
I've got no solution at the moment, haven't debugged it. Saw Doctrine folks merge their thing and thought to immediately give it a whirl to see if it would work. Hopefully this helps out. Might have a look later today (CET), but no promises ;) |
Been looking into the above, again, not sure if this will help ;) I'm currently thinking the values not being set is due to the Symfony PropertyInfo bundle. Reasoning: handling a PUT or POST which receives a On that line it executes: (highlighted few of the properties in screenshot to show it concerns same requests as mentioned earlier) I'm thinking that here there should have been recognition that the property on the resolved Hope it helps, I've got to do things now, not behind a computer :) |
For me it appeared to all work. Except for I got it working though by allowing int for the setter and handling that case:
But that is a bit of a hack, right? |
Does (or will) the core provide built-in exposures or recommendations to expose enum cases? On the front-end side, cases could be renderered as |
Yes we'll definitely support enums out of the box (and also in Schema Generator). I cannot give an ETA yet however. |
Maybe this will help? So for those who have been a bit frustrated that this is not probably supported yet. I seem to have found a workaround. Keep in mind that this will not create fancy enum objects and instead simply interprets the backing type. (Obviously only works for backed enums). 1. Create a EnumMetadataFactory.php: This will allow openAPI and (api platform for that matter) to generate proper typesservices:
App\ApiPlatform\EnumMetadataFactory:
decorates: "api_platform.metadata.property.metadata_factory"
arguments: ['@App\ApiPlatform\EnumMetadataFactory.inner']
decoration_priority: 0 <?php
declare(strict_types=1);
namespace App\ApiPlatform;
use ApiPlatform\Core\Exception\PropertyNotFoundException;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use Symfony\Component\PropertyInfo\Type;
final class EnumMetadataFactory implements PropertyMetadataFactoryInterface
{
private PropertyMetadataFactoryInterface|null $decorated;
public function __construct(PropertyMetadataFactoryInterface $decorated = null)
{
$this->decorated = $decorated;
}
/**
* {@inheritdoc}
*
* @param array<mixed> $options
*/
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
if (null === $this->decorated) {
$propertyMetadata = new ApiProperty();
} else {
try {
$propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
} catch (PropertyNotFoundException $propertyNotFoundException) {
$propertyMetadata = new ApiProperty();
}
}
if (!class_exists($resourceClass)) {
return $propertyMetadata;
}
$reflectionClass = new \ReflectionClass($resourceClass);
$defaultProperties = $reflectionClass->getDefaultProperties();
// sets enum and description for openapi_context and graphql based on named enum type
if (property_exists($resourceClass, $property)) {
$reflectionProperty = $reflectionClass->getProperty($property);
$propertyType = $reflectionProperty->getType();
if ($propertyType instanceof \ReflectionNamedType) {
$propertyClassName = $propertyType->getName();
if (enum_exists($propertyClassName)) {
$descriptionParts = [];
if ($propertyMetadata->getDescription()) {
$descriptionParts[] = $propertyMetadata->getDescription();
$descriptionParts[] = '';
}
$enum = [];
foreach ($propertyClassName::cases() as $case) {
$enum[] = $case->value;
$descriptionParts[] = '- *'.$case->value.'*: '.(method_exists($propertyClassName, 'getDescription') ? $propertyClassName::getDescription($case) : '');
}
$openapiContext = [
'enum' => $enum,
'type' => 'string',
];
$propertyMetadata = $propertyMetadata->withOpenapiContext($openapiContext);
$propertyMetadata = $propertyMetadata->withDescription(implode("\n", $descriptionParts));
}
}
}
// corrects enum default to primitive value (will also effect example value)
if (\array_key_exists($property, $defaultProperties) && null !== $defaultProperties[$property]) {
$default = $defaultProperties[$property];
if (is_object($default) && enum_exists(get_class($default)) && property_exists($default, 'value')) {
$propertyMetadata = $propertyMetadata->withDefault($default->value);
}
}
return $propertyMetadata;
}
} Don't forget to clear the cache with 2. Configure your field with the
|
@dunglas can you provide any more information about the plans for native enum support in API-platform now that version 3 has been released? |
I'm working on it 🙂 |
In my case I wanted to use strings ( enum Gender: int
{
case MALE = 1;
case FEMALE = 2;
case OTHER = 3;
} #[ApiResource]
final class Profile
{
#[ApiProperty(description: 'Profile gender.')]
#[ORM\Column(type: Types::SMALLINT, enumType: Gender::class)]
public ?Gender $gender = null;
} A custom normalizer to handle string values in and out use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class EnumNormalizer implements DenormalizerInterface, DenormalizerAwareInterface, NormalizerInterface, NormalizationAwareInterface
{
use NormalizerAwareTrait;
use DenormalizerAwareTrait;
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
return enum_exists($type) && is_string($data);
}
public function getSupportedTypes(?string $format): array
{
return [
\UnitEnum::class => true,
];
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
// Check if the provided type is an enum
if (!enum_exists($type)) {
throw new \InvalidArgumentException('Type must be an enum');
}
// Convert the string data to uppercase to match the enum case names
$enumValue = strtoupper($data);
// Check if the enum value exists within the specified enum type
if (!defined("{$type}::{$enumValue}")) {
// Gather available enum cases to enhance the error message
$availableCases = array_map(function ($case) { return strtolower($case->name); }, $type::cases());
throw new BadRequestHttpException("Invalid value for enum {$type}: {$data}. Must be one of: ".implode(', ', $availableCases));
}
// Return the enum case constant
return constant("{$type}::{$enumValue}");
}
public function normalize(
mixed $object,
?string $format = null,
array $context = []
): null|array|\ArrayObject|bool|float|int|string {
return strtolower($object->name);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof \UnitEnum;
}
} Thanks to @jensstalder , I changes the enums values to the use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
final class EnumMetadataFactory implements PropertyMetadataFactoryInterface
{
public function __construct(private readonly ?PropertyMetadataFactoryInterface $decorated = null) {}
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
$propertyMetadata = $this->createPropertyMetadata($resourceClass, $property, $options);
$reflectionClass = new \ReflectionClass($resourceClass);
if (property_exists($resourceClass, $property)) {
$reflectionProperty = $reflectionClass->getProperty($property);
$propertyType = $reflectionProperty->getType();
if ($propertyType instanceof \ReflectionNamedType && enum_exists($propertyType->getName())) {
$propertyMetadata = $this->handleEnumProperty($propertyMetadata, $propertyType->getName());
}
}
$defaultProperties = $reflectionClass->getDefaultProperties();
if (\array_key_exists($property, $defaultProperties) && null !== $defaultProperties[$property]) {
$propertyMetadata = $this->handleDefaultProperty($propertyMetadata, $defaultProperties[$property]);
}
return $propertyMetadata;
}
private function createPropertyMetadata(string $resourceClass, string $property, array $options): ApiProperty
{
return $this->decorated ? $this->decorated->create($resourceClass, $property, $options) : new ApiProperty();
}
private function handleEnumProperty(ApiProperty $propertyMetadata, string $enumClass): ApiProperty
{
$enum = array_map(fn ($case) => strtolower($case->name), $enumClass::cases());
$descriptionParts = array_map(fn ($case) => '- *'.$case->name.'*: '.(method_exists($enumClass, 'getDescription') ? $enumClass::getDescription($case) : ''), $enumClass::cases());
$openapiContext = [
'enum' => $enum,
'type' => 'string',
];
return $propertyMetadata
->withOpenapiContext($openapiContext)
->withDescription(implode("\n", $descriptionParts))
;
}
private function handleDefaultProperty(ApiProperty $propertyMetadata, $default): ApiProperty
{
if (is_object($default) && enum_exists(get_class($default)) && property_exists($default, 'value')) {
return $propertyMetadata->withDefault($default->value);
}
return $propertyMetadata;
}
} Now I can use strings as input data curl --location --request POST '/profiles' \
--header 'Content-Type: application/json' \
--data '{
"gender": "female"
}' And retrieve them as string {
"@context": "/contexts/Profile",
"@id": "/profiles",
"@type": "hydra:Collection",
"hydra:totalItems": 11,
"hydra:member": [
{
"@id": "/profiles/1",
"@type": "Profile",
"gender": "male"
},
{
"@id": "/profiles/2",
"@type": "Profile",
"gender": "female"
},
]
} |
edit by soyuka
With PHP 8.1 enums coming to PHP we want to implement the following into API Platform:
end edit
This issue is more for documentation on how to include enums with API Platform and maybe some discussion on how this could potentially be brought into the core.
I've been able to implement enums by doing the following:
Create a class which represents the enum, there is a base enum class that looks like this:
where the subclass would look like:
Create and Register a custom doctrine type for the enum:
In symfony it would look like:
Create a Normalizer for the enum and register it after the object normalizer
Create a trait and class to utilize the enum!
With all of those steps in place, you should be able to have strict enum classes to use in PHP that are saved and retrieved from the db properly, documented in the swagger API, and are properly serialized/deserialized from a string to the proper enum class and back when using the API.
I'm not sure of the best way API Platform could simplify this process except for maybe providing the default enum implementation which would allow API Platform to include the enum normalizer and possibly the doctrine mapping types..?
The text was updated successfully, but these errors were encountered: