Skip to content

Support for Enums #2254

Closed
Closed
@ragboyjr

Description

@ragboyjr

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:

  1. Create a class which represents the enum, there is a base enum class that looks like this:

    abstract class Enum
    {
        protected static $allowedValues = [];
        protected $value;
    
        public function __construct($value) {
            if (!\in_array($value, static::$allowedValues)) {
                throw new \InvalidArgumentException("Invalid value '${value}' for enum. Allowed values are: " . \implode(', ', static::$allowedValues));
            }
    
            $this->value = $value;
        }
    
        public static function getAllValues() {
            return self::$allowedValues;
        }
    
        public function __invoke() {
            return $this->value;
        }
    
        public function __toString() {
            return (string) $this->value;
        }
    }

    where the subclass would look like:

    final class Direction extends Enum
    {
        const UP = 'up';
        const DOWN = 'down';
    
        protected static $allowedValues = [self::UP, self::DOWN];
    
        public static function up(): self {
            return new self(self::UP);
        }
        public static function down(): self {
            return new self(self::DOWN);
        }
    }
  2. Create and Register a custom doctrine type for the enum:

    abstract class EnumType extends StringType
    {
        public function convertToDatabaseValue($value, AbstractPlatform $platform) {
            if ($value instanceof Enum) {
                return (string) $value;
            }
    
            throw new \InvalidArgumentException('Value must be an Enum instance.');
        }
    
        public function requiresSQLCommentHint(AbstractPlatform $platform) {
            return false;
        }
    }
    
    final class DirectionEnumType extends EnumType
    {
        public function convertToPHPValue($value, AbstractPlatform $platform) {
            return new Direction($value);
        }
    
        public function getName() {
            return 'direction_enum';
        }
    }

    In symfony it would look like:

     doctrine:
       dbal:
         types:
           channel_enum:
             class: App\Doctrine\Type\DirectionEnumType
  3. Create a Normalizer for the enum and register it after the object normalizer

    class EnumNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
    {
        /**
         * {@inheritdoc}
         */
        public function normalize($object, $format = null, array $context = array()) {
            /** @var Enum $object */
            return $object();
        }
    
        /**
         * {@inheritdoc}
         */
        public function supportsNormalization($data, $format = null) {
            return $data instanceof Enum;
        }
    
        /**
         * {@inheritdoc}
         */
        public function denormalize($data, $class, $format = null, array $context = array()) {
            return new $class($data);
        }
    
        /**
         * {@inheritdoc}
         */
        public function supportsDenormalization($data, $type, $format = null) {
            return is_subclass_of($type, Enum::class);
        }
    
        public function hasCacheableSupportsMethod(): bool {
            return __CLASS__ === \get_class($this); // copied from DateTimeNormalizer, not quite sure what this is for.
        }
    }
    services:
      App\Serializer\Normalizer\EnumNormalizer:
        tags:
          - {name: serializer.normalizer, priority: -916 }
  4. Create a trait and class to utilize the enum!

    trait WithDirection
    {
        /**
         * @ORM\Column(type="direction_enum")
         * @ApiProperty(swaggerContext={"enum"={"up", "down"}})
         */
        private $direction;
    
        public function getDirection(): Direction {
            return $this->direction;
        }
    }
    
    class AcmeEntity
    {
        use WithDirection;
    
        public function __construct(Direction $direction) {
            $this->direction = $direction;
        }
    }

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..?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions