diff --git a/src/Factory/FieldFactory.php b/src/Factory/FieldFactory.php index 8f6025c8a3..0e52a1bfb4 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 42b7888b5c..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); @@ -60,18 +61,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 in forms. + // So, keep choices with integer keys if using EnumType with translatable enum, otherwise set name as key. + if ($isIndexOrDetail || !$choicesSupportTranslatableInterface || EnumType::class !== $field->getFormType()) { + $processedEnumChoices = []; + foreach ($choices as $choice) { + $processedEnumChoices[$choice->name] = $choice; + } + + $choices = $processedEnumChoices; + } + $field->setFormTypeOptionIfNotSet('class', $enumTypeClass); } @@ -110,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; } diff --git a/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php b/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/FormEnumTranslationController.php new file mode 100644 index 0000000000..b212a00480 --- /dev/null +++ b/tests/Functional/Apps/DefaultApp/src/Controller/Synthetic/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/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 5e30d23b5d..a979d46d6a 100644 --- a/tests/Functional/Apps/DefaultApp/src/Entity/BlogPost.php +++ b/tests/Functional/Apps/DefaultApp/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\Functional\Apps\DefaultApp\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/Functional/Apps/DefaultApp/src/Enum/BlogPostStateEnum.php b/tests/Functional/Apps/DefaultApp/src/Enum/BlogPostStateEnum.php new file mode 100644 index 0000000000..f4c6e33515 --- /dev/null +++ b/tests/Functional/Apps/DefaultApp/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), + }; + } +} diff --git a/tests/Functional/Fields/Choice/FormEnumTranslationControllerTest.php b/tests/Functional/Fields/Choice/FormEnumTranslationControllerTest.php new file mode 100644 index 0000000000..fdd373fcf4 --- /dev/null +++ b/tests/Functional/Fields/Choice/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')); + } + } +}