From ed5b364e13df24a09c99d998259df71aabb86486 Mon Sep 17 00:00:00 2001 From: Matthieu Renard Date: Mon, 17 Nov 2025 19:18:38 +0100 Subject: [PATCH 1/4] Fix for issue #7242 : broken translations for enums in forms With symfony/form >= v7.3.5, EnumType uses the keys of the choices option as labels for the enum cases, breaking the translation that happened previously when enum implements TranslatableInterface. The translation now occurs only when the choices option has integer keys (more precisely, currently only if it satisfies array_list()). Fixing this by changing the way ChoiceFields are configured with the ChoiceConfigurator, to define the choices option with integer keys when the enum is translatable. --- src/Factory/FieldFactory.php | 9 +++- src/Field/Configurator/ChoiceConfigurator.php | 20 ++++++--- .../FormEnumTranslationControllerTest.php | 45 +++++++++++++++++++ .../FormEnumTranslationController.php | 27 +++++++++++ .../src/DataFixtures/AppFixtures.php | 4 +- tests/TestApplication/src/Entity/BlogPost.php | 23 +++++++++- .../src/Enum/BlogPostStateEnum.php | 22 +++++++++ 7 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 tests/Controller/FormEnumTranslationControllerTest.php create mode 100644 tests/TestApplication/src/Controller/FormEnumTranslationController.php create mode 100644 tests/TestApplication/src/Enum/BlogPostStateEnum.php diff --git a/src/Factory/FieldFactory.php b/src/Factory/FieldFactory.php index 7d2b469f8c..eb9875dd85 100644 --- a/src/Factory/FieldFactory.php +++ b/src/Factory/FieldFactory.php @@ -15,6 +15,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField; +use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; use EasyCorp\Bundle\EasyAdminBundle\Field\CollectionField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; @@ -187,7 +188,13 @@ private function replaceGenericFieldsWithSpecificFields(FieldCollection $fields, } /** @phpstan-ignore-next-line function.alreadyNarrowedType */ $fieldType = property_exists($fieldMapping, 'type') ? $fieldMapping->type : $fieldMapping['type']; - $guessedFieldFqcn = self::$doctrineTypeToFieldFqcn[$fieldType] ?? null; + + // Special handling for enums, that are represented as string or as a simple array of strings + if ((Types::STRING === $fieldType || Types::SIMPLE_ARRAY === $fieldType) && isset($fieldMapping['enumType'])) { + $guessedFieldFqcn = ChoiceField::class; + } else { + $guessedFieldFqcn = self::$doctrineTypeToFieldFqcn[$fieldType] ?? null; + } if (null === $guessedFieldFqcn) { throw new \RuntimeException(sprintf('The Doctrine type of the "%s" field is "%s", which is not supported by EasyAdmin. For Doctrine\'s Custom Mapping Types have a look at EasyAdmin\'s field docs.', $fieldDto->getProperty(), $fieldType)); } diff --git a/src/Field/Configurator/ChoiceConfigurator.php b/src/Field/Configurator/ChoiceConfigurator.php index 8be7628b48..cdc92074d9 100644 --- a/src/Field/Configurator/ChoiceConfigurator.php +++ b/src/Field/Configurator/ChoiceConfigurator.php @@ -60,18 +60,24 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c } if ($allChoicesAreEnums && array_is_list($choices) && \count($choices) > 0) { - $processedEnumChoices = []; - foreach ($choices as $choice) { - $processedEnumChoices[$choice->name] = $choice; - } - - $choices = $processedEnumChoices; - // Update form type to be EnumType if current form type is still ChoiceType // Leave the form type as is if user set something else explicitly if (ChoiceType::class === $field->getFormType()) { $field->setFormType(EnumType::class); } + + // When dealing with enums that implement TranslatableInterface, they are now translated by Symfony only if + // the keys of the choices are integers. + // So, keep choices with integer keys if using EnumType with translatable enum, otherwise set name as key. + if (!$choicesSupportTranslatableInterface || EnumType::class !== $field->getFormType()) { + $processedEnumChoices = []; + foreach ($choices as $choice) { + $processedEnumChoices[$choice->name] = $choice; + } + + $choices = $processedEnumChoices; + } + $field->setFormTypeOptionIfNotSet('class', $enumTypeClass); } diff --git a/tests/Controller/FormEnumTranslationControllerTest.php b/tests/Controller/FormEnumTranslationControllerTest.php new file mode 100644 index 0000000000..7aa7fb7c19 --- /dev/null +++ b/tests/Controller/FormEnumTranslationControllerTest.php @@ -0,0 +1,45 @@ +client->followRedirects(); + + $this->blogPosts = $this->entityManager->getRepository(BlogPost::class); + } + + protected function getControllerFqcn(): string + { + return FormEnumTranslationController::class; + } + + protected function getDashboardFqcn(): string + { + return DashboardController::class; + } + + public function testFieldsFormatValue() + { + $translator = static::getContainer()->get(TranslatorInterface::class); + static::assertInstanceOf(TranslatorInterface::class, $translator); + $this->client->request('GET', $this->generateNewFormUrl()); + + foreach (BlogPostStateEnum::cases() as $case) { + self::assertAnySelectorTextSame('option', $case->trans($translator, locale: 'en')); + } + } +} diff --git a/tests/TestApplication/src/Controller/FormEnumTranslationController.php b/tests/TestApplication/src/Controller/FormEnumTranslationController.php new file mode 100644 index 0000000000..f7972adf2a --- /dev/null +++ b/tests/TestApplication/src/Controller/FormEnumTranslationController.php @@ -0,0 +1,27 @@ + + */ +class FormEnumTranslationController extends AbstractCrudController +{ + public static function getEntityFqcn(): string + { + return BlogPost::class; + } + + public function configureFields(string $pageName): iterable + { + // Test translating enums in forms. When enums implement TranslatableInterface, this should be done by Symfony + // automagically. + return [ + ChoiceField::new('state', 'State'), + ]; + } +} diff --git a/tests/TestApplication/src/DataFixtures/AppFixtures.php b/tests/TestApplication/src/DataFixtures/AppFixtures.php index ca1c3468c6..1d6fa7f418 100644 --- a/tests/TestApplication/src/DataFixtures/AppFixtures.php +++ b/tests/TestApplication/src/DataFixtures/AppFixtures.php @@ -11,6 +11,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\Page; use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\User; use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\Website; +use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Enum\BlogPostStateEnum; class AppFixtures extends Fixture { @@ -42,7 +43,8 @@ public function load(ObjectManager $manager): void ->setCreatedAt(new \DateTimeImmutable('2020-11-'.($i + 1).' 09:00:00')) ->setPublishedAt(new \DateTimeImmutable('2020-11-'.($i + 1).' 11:00:00')) ->addCategory($this->getReference('category'.($i % 10), Category::class)) - ->setAuthor($this->getReference('user'.($i % 5), User::class)); + ->setAuthor($this->getReference('user'.($i % 5), User::class)) + ->setState(BlogPostStateEnum::cases()[$i % \count(BlogPostStateEnum::cases())]); if ($i < 10) { $blogPost->setPublisher( diff --git a/tests/TestApplication/src/Entity/BlogPost.php b/tests/TestApplication/src/Entity/BlogPost.php index 697e6e1d3e..9ce66190c0 100644 --- a/tests/TestApplication/src/Entity/BlogPost.php +++ b/tests/TestApplication/src/Entity/BlogPost.php @@ -5,6 +5,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Enum\BlogPostStateEnum; #[ORM\Entity] class BlogPost @@ -39,6 +40,9 @@ class BlogPost #[ORM\ManyToOne(targetEntity: User::class)] private $publisher; + #[ORM\Column(enumType: BlogPostStateEnum::class)] + private ?BlogPostStateEnum $state = BlogPostStateEnum::Draft; + public function __construct() { $this->categories = new ArrayCollection(); @@ -150,8 +154,25 @@ public function getPublisher() return $this->publisher; } - public function setPublisher(?User $publisher): void + public function setPublisher(?User $publisher): self { $this->publisher = $publisher; + + return $this; + } + + public function getState(): ?BlogPostStateEnum + { + return $this->state; + } + + public function setState(string|BlogPostStateEnum|null $state): self + { + if (!$state instanceof BlogPostStateEnum) { + $state = BlogPostStateEnum::tryFrom($state); + } + $this->state = $state; + + return $this; } } diff --git a/tests/TestApplication/src/Enum/BlogPostStateEnum.php b/tests/TestApplication/src/Enum/BlogPostStateEnum.php new file mode 100644 index 0000000000..90bfe727fc --- /dev/null +++ b/tests/TestApplication/src/Enum/BlogPostStateEnum.php @@ -0,0 +1,22 @@ + $translator->trans('BlogPostStateEnum.draft', locale: $locale), + self::Published => $translator->trans('BlogPostStateEnum.published', locale: $locale), + self::Deleted => $translator->trans('BlogPostStateEnum.deleted', locale: $locale), + }; + } +} From bc3d5ae47914caeca8ff610eee1203245482f06c Mon Sep 17 00:00:00 2001 From: Matthieu Renard Date: Sun, 1 Mar 2026 17:48:57 +0100 Subject: [PATCH 2/4] Moving tests files to fit the new layout --- .../Synthetic}/FormEnumTranslationController.php | 2 +- .../Apps/DefaultApp}/src/Entity/BlogPost.php | 2 +- .../Apps/DefaultApp}/src/Enum/BlogPostStateEnum.php | 2 +- .../Fields/Choice}/FormEnumTranslationControllerTest.php | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) rename tests/{TestApplication/src/Controller => Functional/Apps/DefaultApp/src/Controller/Synthetic}/FormEnumTranslationController.php (88%) rename tests/{TestApplication => Functional/Apps/DefaultApp}/src/Entity/BlogPost.php (97%) rename tests/{TestApplication => Functional/Apps/DefaultApp}/src/Enum/BlogPostStateEnum.php (89%) rename tests/{Controller => Functional/Fields/Choice}/FormEnumTranslationControllerTest.php (74%) diff --git a/tests/TestApplication/src/Controller/FormEnumTranslationController.php b/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php similarity index 88% rename from tests/TestApplication/src/Controller/FormEnumTranslationController.php rename to tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php index f7972adf2a..82dfe20dc1 100644 --- a/tests/TestApplication/src/Controller/FormEnumTranslationController.php +++ b/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php @@ -1,6 +1,6 @@ Date: Sun, 1 Mar 2026 18:01:42 +0100 Subject: [PATCH 3/4] Fix tests use imports --- .../Controller/Synthetic/FormEnumTranslationController.php | 2 +- .../Apps/DefaultApp/src/DataFixtures/AppFixtures.php | 4 +++- tests/Functional/Apps/DefaultApp/src/Entity/BlogPost.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php b/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php index 82dfe20dc1..b212a00480 100644 --- a/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php +++ b/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php @@ -4,7 +4,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; -use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\BlogPost; +use EasyCorp\Bundle\EasyAdminBundle\Tests\Functional\Apps\DefaultApp\Entity\BlogPost; /** * @extends AbstractCrudController diff --git a/tests/Functional/Apps/DefaultApp/src/DataFixtures/AppFixtures.php b/tests/Functional/Apps/DefaultApp/src/DataFixtures/AppFixtures.php index 8ef792cf59..8eaad382d1 100644 --- a/tests/Functional/Apps/DefaultApp/src/DataFixtures/AppFixtures.php +++ b/tests/Functional/Apps/DefaultApp/src/DataFixtures/AppFixtures.php @@ -28,6 +28,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Tests\Functional\Apps\DefaultApp\Entity\Synthetic\SortTestRelatedEntity; use EasyCorp\Bundle\EasyAdminBundle\Tests\Functional\Apps\DefaultApp\Entity\User; use EasyCorp\Bundle\EasyAdminBundle\Tests\Functional\Apps\DefaultApp\Entity\Website; +use EasyCorp\Bundle\EasyAdminBundle\Tests\Functional\Apps\DefaultApp\Enum\BlogPostStateEnum; class AppFixtures extends Fixture { @@ -59,7 +60,8 @@ public function load(ObjectManager $manager): void ->setCreatedAt(new \DateTimeImmutable('2020-11-'.($i + 1).' 09:00:00')) ->setPublishedAt(new \DateTimeImmutable('2020-11-'.($i + 1).' 11:00:00')) ->addCategory($this->getReference('category'.($i % 10), Category::class)) - ->setAuthor($this->getReference('user'.($i % 5), User::class)); + ->setAuthor($this->getReference('user'.($i % 5), User::class)) + ->setState(BlogPostStateEnum::cases()[$i % \count(BlogPostStateEnum::cases())]); if ($i < 10) { $blogPost->setPublisher( diff --git a/tests/Functional/Apps/DefaultApp/src/Entity/BlogPost.php b/tests/Functional/Apps/DefaultApp/src/Entity/BlogPost.php index 59bbb98dc2..a979d46d6a 100644 --- a/tests/Functional/Apps/DefaultApp/src/Entity/BlogPost.php +++ b/tests/Functional/Apps/DefaultApp/src/Entity/BlogPost.php @@ -5,7 +5,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Enum\BlogPostStateEnum; +use EasyCorp\Bundle\EasyAdminBundle\Tests\Functional\Apps\DefaultApp\Enum\BlogPostStateEnum; #[ORM\Entity] class BlogPost From e2412d91b109508fb46026e5691ada35cda42c1f Mon Sep 17 00:00:00 2001 From: Matthieu Renard Date: Sun, 1 Mar 2026 18:58:30 +0100 Subject: [PATCH 4/4] Fixed a bug that did not display fields translated on non-forms pages. --- src/Field/Configurator/ChoiceConfigurator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Field/Configurator/ChoiceConfigurator.php b/src/Field/Configurator/ChoiceConfigurator.php index 4aa0047236..783beedf34 100644 --- a/src/Field/Configurator/ChoiceConfigurator.php +++ b/src/Field/Configurator/ChoiceConfigurator.php @@ -33,6 +33,7 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c $choicesSupportTranslatableInterface = false; $isExpanded = true === $field->getCustomOption(ChoiceField::OPTION_RENDER_EXPANDED); $isMultipleChoice = true === $field->getCustomOption(ChoiceField::OPTION_ALLOW_MULTIPLE_CHOICES); + $isIndexOrDetail = \in_array($context->getCrud()->getCurrentPage(), [Crud::PAGE_INDEX, Crud::PAGE_DETAIL], true); $choices = $this->getChoices($field->getCustomOption(ChoiceField::OPTION_CHOICES), $entityDto, $field); @@ -67,9 +68,9 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c } // When dealing with enums that implement TranslatableInterface, they are now translated by Symfony only if - // the keys of the choices are integers. + // the keys of the choices are integers in forms. // So, keep choices with integer keys if using EnumType with translatable enum, otherwise set name as key. - if (!$choicesSupportTranslatableInterface || EnumType::class !== $field->getFormType()) { + if ($isIndexOrDetail || !$choicesSupportTranslatableInterface || EnumType::class !== $field->getFormType()) { $processedEnumChoices = []; foreach ($choices as $choice) { $processedEnumChoices[$choice->name] = $choice; @@ -116,7 +117,6 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c $field->setFormTypeOption('attr.data-ea-autocomplete-render-items-as-html', true === $field->getCustomOption(ChoiceField::OPTION_ESCAPE_HTML_CONTENTS) ? 'false' : 'true'); $fieldValue = $field->getValue(); - $isIndexOrDetail = \in_array($context->getCrud()->getCurrentPage(), [Crud::PAGE_INDEX, Crud::PAGE_DETAIL], true); if (null === $fieldValue || !$isIndexOrDetail) { return; }