EasyAdminBundle icon indicating copy to clipboard operation
EasyAdminBundle copied to clipboard

Add support for php 8.1 enums for choice field

Open cobyl opened this issue 3 years ago • 9 comments

Proposed change allows to use php 8.1 enum in choice field:

if we have enum in entity:

    #[ORM\Column(type: Types::STRING, length: 8, nullable: true, enumType: Status::class)]
    private Status $status;

then we can in configureFields:

        $status = ChoiceField::new('status')->setChoices(Status::cases());
//or
        $status = ChoiceField::new('status')->setChoices(
            array_reduce(Status::cases(), function (array $elements, Status $status) {
                return $elements + ['enum.status.'.$status->value => $status];
            }, [])
        );
//or
        $status = ChoiceField::new('status');

without that change in the case of enums we got the error "Warning: array_flip(): Can only flip string and integer values, entry skipped" there:

        $flippedChoices = array_flip($choices);

cobyl avatar Jan 20 '22 18:01 cobyl

issue: https://github.com/EasyCorp/EasyAdminBundle/issues/4989

cobyl avatar Jan 20 '22 18:01 cobyl

Because of doctrine limitations work only with this kind of enums:

enum Status:string
{
    case WAITING = 'waiting';
    case ERROR = 'error';
}

and NOT with

enum Status
{
    case WAITING;
    case ERROR;
}

cobyl avatar Jan 20 '22 18:01 cobyl

Since Symfony has EnumType we can do it like this (workaround):

// for index because of array_flip
            ChoiceField::new('packingType')
                ->onlyOnIndex()
                ->setChoices(function () {
                    $choices = array_map(static fn (?PackingUnit $unit) => [$unit->value => $unit->name], PackingUnit::cases());

                    return array_merge(...$choices);
                })
                ->setFormType(EnumType::class)
                ->setFormTypeOption('class', PackingUnit::class)
                ->setFormTypeOption('choice_label', function (PackingUnit $enum) {
                    return $enum->value;
                }),
// for form
            ChoiceField::new('packingType')
                ->onlyOnForms()
                ->setChoices(function () {
                    $choices = array_map(static fn (?PackingUnit $unit) => [$unit->value => $unit], PackingUnit::cases());

                    return array_merge(...$choices);
                })
                ->setFormType(EnumType::class)
                ->setFormTypeOption('class', PackingUnit::class)
                ->setFormTypeOption('choice_label', function (PackingUnit $enum) {
                    return $enum->value;
                }),

oleg-andreyev avatar Jan 28 '22 09:01 oleg-andreyev

@oleg-andreyev didn't work for me... waiting for this PR

ERuban avatar Feb 03 '22 15:02 ERuban

@ERuban updated my workaround.

oleg-andreyev avatar Feb 03 '22 17:02 oleg-andreyev

For me this fixes it for all pages (with the current implementation)

$choiceField = ChoiceField::new('status')
    ->setChoices(Status::cases())
    ->setFormType(EnumType::class)
    ->setFormTypeOption('class', Status::class)
;

if (in_array($pageName, [Crud::PAGE_INDEX, Crud::PAGE_DETAIL], true)) {
    $choiceField->setChoices(array_reduce(
        Status::cases(),
        static fn (array $choices, Status $status) => $choices + [$status->name => $status->value],
        [],
    ));
}

yield $choiceField;

akalineskou avatar Feb 05 '22 01:02 akalineskou

A couple of the workarounds here work, but cause duplication if the key matches the value. e.g.

case OTHER = 'OTHER';

Results in:

OTHER, OTHER

shaneparsons avatar Apr 08 '22 19:04 shaneparsons

@javiereguiluz please, take a look on this

asanikovich avatar May 31 '22 11:05 asanikovich

For me this fixes it for all pages (with the current implementation)

$choiceField = ChoiceField::new('status')
    ->setChoices(Status::cases())
    ->setFormType(EnumType::class)
    ->setFormTypeOption('class', Status::class)
;

if (in_array($pageName, [Crud::PAGE_INDEX, Crud::PAGE_DETAIL], true)) {
    $choiceField->setChoices(array_reduce(
        Status::cases(),
        static fn (array $choices, Status $status) => $choices + [$status->name => $status->value],
        [],
    ));
}

yield $choiceField;

it looks nice for me, but it is not possible to use the ->setTranslatableChoices() in this case. Or maybe use a different label displayed instead of the Enum name.

Here is an example:

My Enum:

enum PageContentType: string
{
    case Banner = 'banner';
    case Iframe = 'iframe';
    case Image = 'image';
    case Quote = 'quote';

    public function label(): string
    {
        return match($this)
        {
            self::Banner => 'Bannière',
            self::Iframe => 'Iframe (vidéo / map)',
            self::Image => 'Image et texte',
            self::Quote => 'Citation',
        };
    }
}

And there the Field:

 ChoiceField::new('type', 'Type')->setColumns(4)
                ->setChoices(PageContentType::cases())
                ->setFormType(EnumType::class)
                ->setFormTypeOption('class', PageContentType::class)

In the first case, The label displayed on the EA select is "Banner" with the value "banner" but I would like to display a specific label (I tried with the function label()) or be able to translate the "Banner" ?

Maybe it is an issue related to EnumType itself...

EDIT:

If I create a "messages.fr.yaml" with Banner: "Bannière FR" image

We can see the translation. Is it a good way of translation the Enum Key in translation file without write a speciale var like "label.banner"?

Snowbaha avatar Jun 24 '22 08:06 Snowbaha

Hello What's the status of this PR ? :) it would be very useful, I had to abandon enum for the moment and go for a class with const instead

oussj avatar Dec 27 '22 13:12 oussj

One another area, which this PR doesn't address, is the ChoiceFilter. Maybe it would be good to include this as well?

kiler129 avatar Jan 21 '23 07:01 kiler129

Thanks Tomek! This has been finally merged!

I added some tests/docs (https://github.com/EasyCorp/EasyAdminBundle/commit/934d5e64234919c4f2c0d933c3086d5d10fe4c5b) but it'd be great if more people could test this in real apps before tagging a new release. Thanks!

javiereguiluz avatar Feb 04 '23 12:02 javiereguiluz

Thanks, it works, add PR https://github.com/EasyCorp/EasyAdminBundle/pull/5610 with some php-cs-fixer and PHP < 8.1 issues

ksn135 avatar Feb 06 '23 08:02 ksn135

Hi, I have errors when use Enum with ChoiceField.

Screenshot 2023-02-08 at 13 11 17

Entity implementation: Screenshot 2023-02-08 at 13 13 44

Controller: yield ChoiceField::new('weight.unit', new TranslatableMessage('weight.unit.label')) ->setColumns('form-field--small');

Before this last release I've used it like that: yield ChoiceField::new('weight.unit', new TranslatableMessage('weight.unit.label')) ->setChoices($isInfoPage ? WeightUnit::getAsArray() : WeightUnit::cases()) ->setFormType(EnumType::class) ->setFormTypeOption('class', WeightUnit::class) ->setFormTypeOption('choice_label', function (WeightUnit $enum) { return new TranslatableMessage('weight.unit.'.$enum->value); }) ->setCustomOption(ChoiceField::OPTION_USE_TRANSLATABLE_CHOICES, $isInfoPage) ->setColumns('form-field--small');

The problem is that wen I use ChoiceType symfony can't convert Enum value because I don't use EnumType.

abozhinov avatar Feb 08 '23 11:02 abozhinov

@abozhinov

Same problem. It can be solve in your entity by changing the getter method ex

#[ORM\Column(type: 'string', length: 10, enumType: UserType::class, name: "user_type")]
private ?UserType $type = null;

public function getTypeEnum(): ?UserType 
{
      return $this->type;
}

public function getType(): ?string
{
      return $this->type?->value;
}

public function setType($type): self
{
     if(!$type instanceof UserType ) {
          $type= UserType::from($type);
     }
     $this->type = $type;
     return $this;
}

babwar avatar Feb 08 '23 17:02 babwar

There is no point to use Enum and return string. I think the best will be to adapt ChoiceField to use EnumType.

abozhinov avatar Feb 08 '23 18:02 abozhinov

I don't say it's the solution. I just put here my quickest solution i used (it can help someone). It's not a minor release (my app was broken today when putting it in prod).

babwar avatar Feb 08 '23 18:02 babwar

@abozhinov thanks for reporting this. I'd need your help to solve it. I've created #5620 but I'm not sure if it's the right fix. Thanks.

javiereguiluz avatar Feb 08 '23 19:02 javiereguiluz

@javiereguiluz the issue is that ChoiceType can't convert Enum to String. I think we should create new EnumField to move the logic from ChoiceField to the new field type.

abozhinov avatar Feb 08 '23 19:02 abozhinov

I have the same problem. +1 for a new dedicated EnumField.

COil avatar Feb 09 '23 16:02 COil

Well, I decided this as a workaround for the current 4.x branch. Javier @javiereguiluz how to implement this better in the bundle itself?

<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
    <entity name="Ksn135\CompanyBundle\Entity\DocSetting">

...
        <field name="type" type="string" length="20" nullable="false" enum-type="Ksn135\CompanyBundle\Enums\DocType" />
...    

    </entity>
</doctrine-mapping>
enum DocType: string
{
    case INDIVIDUAL = 'individual';
    case TYPICAL = 'typical';

    public static function choices(): array
    {
        return [
            'admin_label.enum.docType.' . self::INDIVIDUAL->value => self::INDIVIDUAL->name,
            'admin_label.enum.docType.' . self::TYPICAL->value => self::TYPICAL->name,
        ];
    }
}
class DocSettingCrudController extends AbstractCrudController
{
...
    public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
    {
        $builder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
        $builder->get('type')->addModelTransformer(
            new CallbackTransformer(
                static fn(mixed $value): mixed => $value, // no convertion
                static fn(?string $value): ?DocType => DocType::tryFrom($value)
            )
        );

        return $builder;
    }

    public function configureFields(string $pageName): iterable
    {
        return [
...
            Fields\ChoiceField::new ('type', 'admin_label.doc.field.type')->setRequired(true)
                ->setFormType(EnumType::class)
                ->setFormTypeOption('class', DocType::class)
                ->setFormTypeOption('choice_label', static fn($choice): ?string => 'admin_label.enum.docType.' . strtolower($choice))
                ->setFormTypeOption('choice_value', static fn($choice): ?string => $choice instanceof \BackedEnum ? $choice->value : $choice)
                ->setChoices(\in_array($pageName, [Crud::PAGE_INDEX, Crud::PAGE_DETAIL], true) ? DocType::choices() : DocType::cases())
...
     ];
  }
}
# messages.en.yaml
admin_label:
  enum:
    docType:
      individual: The Individual
      typical: Very typical
Screenshot 2023-02-15 at 16 39 01 Screenshot 2023-02-15 at 16 39 15 Screenshot 2023-02-15 at 16 39 30

ksn135 avatar Feb 15 '23 13:02 ksn135

I found a solution. Check my PR -> https://github.com/EasyCorp/EasyAdminBundle/pull/5640

abozhinov avatar Feb 18 '23 13:02 abozhinov