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

Add support for php 8.1 enums for choice field #4988

Merged
merged 1 commit into from
Feb 4, 2023

Conversation

cobyl
Copy link
Contributor

@cobyl cobyl commented Jan 20, 2022

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
Copy link
Contributor Author

cobyl commented Jan 20, 2022

issue: #4989

@cobyl
Copy link
Contributor Author

cobyl commented Jan 20, 2022

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;
}

@javiereguiluz javiereguiluz added this to the 4.x milestone Jan 20, 2022
@oleg-andreyev
Copy link
Contributor

oleg-andreyev commented Jan 28, 2022

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;
                }),

@ERuban
Copy link

ERuban commented Feb 3, 2022

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

@oleg-andreyev
Copy link
Contributor

@ERuban updated my workaround.

});
} else {
$choices = array_reduce($choices, function ($elements, $enum) {
return $elements + [$enum->value => $enum->value];
Copy link
Contributor

@akalineskou akalineskou Feb 5, 2022

Choose a reason for hiding this comment

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

@akalineskou
Copy link
Contributor

akalineskou commented Feb 5, 2022

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;

@shaneparsons
Copy link

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

@asanikovich
Copy link
Contributor

@javiereguiluz please, take a look on this

@Snowbaha
Copy link

Snowbaha commented Jun 24, 2022

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

@javiereguiluz javiereguiluz added the priority: important Bugs to fix and features to implement label Dec 7, 2022
@oussj
Copy link

oussj commented Dec 27, 2022

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

@kiler129
Copy link
Contributor

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

@javiereguiluz
Copy link
Collaborator

Thanks Tomek! This has been finally merged!

I added some tests/docs (934d5e6) but it'd be great if more people could test this in real apps before tagging a new release. Thanks!

@ksn135
Copy link
Contributor

ksn135 commented Feb 6, 2023

Thanks, it works, add PR #5610 with some php-cs-fixer and PHP < 8.1 issues

@abozhinov
Copy link
Contributor

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.

@babwar
Copy link

babwar commented Feb 8, 2023

@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;
}

@abozhinov
Copy link
Contributor

abozhinov commented Feb 8, 2023

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

@babwar
Copy link

babwar commented Feb 8, 2023

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

@javiereguiluz
Copy link
Collaborator

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

@abozhinov
Copy link
Contributor

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

@COil
Copy link
Contributor

COil commented Feb 9, 2023

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

@ksn135
Copy link
Contributor

ksn135 commented Feb 15, 2023

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

@abozhinov
Copy link
Contributor

I found a solution. Check my PR -> #5640

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature priority: important Bugs to fix and features to implement
Projects
None yet
Development

Successfully merging this pull request may close these issues.