PhpEnums icon indicating copy to clipboard operation
PhpEnums copied to clipboard

🚧 V2: Rewrite for PHP 8.1 Enum

Open ogizanagi opened this issue 4 years ago • 6 comments

First step was made in https://github.com/Elao/PhpEnums/pull/165 to rewrite the lib with the new native enum types & adding some integrations based on it and our specificities (readables, flags, …).

📚 See current V2 documentation here

🚧 More work to come. Help welcome to integrate what could be useful, from 1.x features or whole new ones.

Will probable create a 2.0.0-alpha1 tag in the next weeks.


V2 Features

Core features

From V1, core features extending the PHP 8.1 native enum capabilities

  • [x] Readable enums âžœ Done in #165.
  • [x] Flag enums âžœ Done in #165. It requires a new FlagBag object to manipulate flags, since PHP native enums are restricted to their cases and cannot dynamically declare combinations.

New features

Whole new features or integration, leveraging new enum capabilities

(Pending) official integrations

From V1, likely to be dropped in the future, or already having an official integration

  • [x] Serializer integration Core PR: https://github.com/symfony/symfony/issues/40241
  • [ ] Doctrine DBAL Types & types generator: ~Doctrine is likely to provide DBAL types for enums in the future, but the current type system lacks a way to easily register it.~ âžœ Doctrine guys found a workaround in https://github.com/doctrine/orm/pull/9304. We might consider dropping our implementation, but collection and flag bag support is still valid. Also our current implementation supports specifying a default on null.
    • [x] Basic \BackedEnum support âžœ Done in #165. Documentation
    • [ ] Collection of enums (csv & json)
    • [x] FlagBag DBAL type for persisting flag enums combination âžœ #186
  • [ ] Doctrine ODM Types & types generator:
    • [x] Basic \BackedEnum support: âžœ #176
    • [x] Collection of enums: âžœ #176
    • [ ] FlagBag ODM type for persisting flag enums combination
  • [x] HTTP Kernel Controller Argument Resolver Nothing in Symfony core yet?
    • Core PR => https://github.com/symfony/symfony/pull/44831
    • V2 PR: https://github.com/Elao/PhpEnums/pull/169
  • [x] API Platform no specificity for readable or flagged enum? Anything else to consider? Core PR: https://github.com/api-platform/core/issues/4349
  • [x] Faker could be contributed to FakerPHP directly for native enum support âžœ rejected in https://github.com/FakerPHP/Faker/pull/421. We might only want to port randomEnum from V1 to get a random enum case from given enum FQCN. Porting the namespace aliases feature might not be relevant, since it was targeting Nelmio Alice's fixtures generator DSL, but Foundry is getting more popularity now. âžœ #177
  • [ ] Validator constraint. Rejected for now in https://github.com/symfony/symfony/pull/43047. Could provide the alternative here and re-discuss in Symfony's core later if proven useful.

Extra integrations

From V1, might still be relevant/adapted

Extra tasks

  • [ ] Write an UPGRADE-2.x.md guide, with most basic hints for upgrading your application code from 1.x to 2.x

ogizanagi avatar Feb 18 '21 12:02 ogizanagi

https://github.com/symfony/symfony/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+enum+label%3A%22Help+wanted%22

Likely to be dropped in the future, if any rewrite of this library is done based on the new enum type:

Might still benefits:

Extras:

  • Readable enums could be using PHP 8 attributes to provide their labels: https://github.com/Elao/PhpEnums/projects/1#card-66229974: https://3v4l.org/Gag4r#v8.1rc3 :
<?php

#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
class EnumLabel
{
    public function __construct(public string $label)
    {
    }
}

enum Suit: string
{
    #[\EnumLabel('suit.hearts')]
    case Hearts = 'H';
    
    #[\EnumLabel('suit.diamonds')]
    case Diamonds = 'D';
    
    #[\EnumLabel('suit.diamonds')]
    case Clubs = 'C';
    
    #[\EnumLabel('suit.spades')]
    case Spades = 'S';
}

var_dump(Suit::Hearts);
var_dump($reflector = new \ReflectionEnum(Suit::class));
var_dump($reflector->getCase('Hearts')->getAttributes(\EnumLabel::class)[0]->newInstance());
var_dump((new \ReflectionClassConstant(Suit::class, 'Hearts'))->getAttributes(\EnumLabel::class)[0]->newInstance());

# output:
enum(Suit::Hearts)
object(ReflectionEnum)#2 (1) {
  ["name"]=>
  string(4) "Suit"
}
object(EnumLabel)#3 (1) {
  ["label"]=>
  string(11) "suit.hearts"
}
object(EnumLabel)#3 (1) {
  ["label"]=>
  string(11) "suit.hearts"
}

ogizanagi avatar Feb 18 '21 15:02 ogizanagi

Tried my hands with porting the ApiPlatform bridge over. Maybe it helps?

<?php

/*
 * This file is part of the "elao/enum" package.
 *
 * Copyright (C) Elao
 *
 * @author Elao <[email protected]>
 */

namespace App\ApiPlatform\Core\JsonSchema\Type;

use ApiPlatform\Core\JsonSchema\Schema;
use ApiPlatform\Core\JsonSchema\TypeFactoryInterface;
use Elao\Enum\ReadableEnumInterface;
use Symfony\Component\PropertyInfo\Type;

final class ElaoEnumType implements TypeFactoryInterface
{
    /** @var TypeFactoryInterface */
    private $decorated;

    public function __construct(TypeFactoryInterface $decorated)
    {
        $this->decorated = $decorated;
    }

    /**
     * {@inheritdoc}
     */
    public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array
    {
        if (!is_a($enum = $type->getClassName(), ReadableEnumInterface::class, true)) {
            return $this->decorated->getType($type, $format, $readableLink, $serializerContext, $schema);
        }

        $ref = new \ReflectionEnum($type->getClassName());
        $docType = '';
        switch ($ref->getBackingType()) {
            case 'string': $docType = 'string'; break;
            case 'int': $docType = 'integer'; break;
            case null:
            default:
                $docType = 'string';
                break;
        }
        $schema = [];
        $cases = $enum::cases();
        if ($type->isNullable() && !$type->isCollection()) {
            $cases[] = null;
        }
        $readableCases = [];
        foreach ($cases as $case) {
            //$readableCases[$case->value] = $case->getReadable();
            $readableCases[$case->value] = $case->value . '=' . $case->name;
        }
        $enumSchema = [
            'type' => $docType,
            'enum' => $readableCases,
            'example' => $cases[0]->value,
        ];

        if ($type->isCollection()) {
            $schema['type'] = 'array';
            $schema['items'] = $enumSchema;
        } else {
            $schema = $enumSchema;
        }

        if ($type->isNullable()) {
            $schema['nullable'] = true;
        }

        return $schema;
    }
}

jensstalder avatar Dec 18 '21 12:12 jensstalder

Hi @jensstalder . Thank you very much for looking at this.

Could you please explain me a bit more what would be the purpose of such a bridge now with native PHP enum? I see the $readableCases used as $enumSchema.enum in your sample, but is that correct? An API would rely on the backed value (as in the previous version of this library), not the readable one. But is there some places where the readable label could be relevant?

You might have more clues than me on what could be relevant to provide in such a bridge regarding the features that are specific to this lib v2 extending native enums capabilities. So, your help is appreciated :)

ogizanagi avatar Dec 18 '21 12:12 ogizanagi

@ogizanagi Good question. The only benefit I see in relation to JsonSchema is that it shows up in the open API docs. My original goal was to extend the graphql implementation so that the types also get interpreted as enum. I have not found a solution for that yet. But thinking about it, it's true that API platform core should instead simply handle native enums better for both cases (JsonSchema and GraphQL), and it might not be necessary to do this within this package. But this might be a bit to opinionated from the view of the platform? As far as I can tell though, API platform currently only considers the backing type as if it were a simple string or int field? And Graphql endpoint hides the enum typed field completely (but that could be another issue).

jensstalder avatar Dec 18 '21 13:12 jensstalder

For Doctrine there is one issue affecting this lib

  • ENUMs cannot be used as IDs (I made a PR https://github.com/doctrine/orm/pull/9629 which hopefully will be merged soon)

Please add it on the list

EDIT: merged

michnovka avatar Apr 09 '22 14:04 michnovka

@ogizanagi the https://github.com/doctrine/orm/pull/9629 has been merged in doctrine/orm 2.12.x , so you can remove the known issue from the list

michnovka avatar Apr 24 '22 12:04 michnovka