diff --git a/doc/fields.rst b/doc/fields.rst index 26cdd0d3b5..f59fae39b0 100644 --- a/doc/fields.rst +++ b/doc/fields.rst @@ -1127,8 +1127,8 @@ the full address is only visible on the detail and edit pages:: [$local, $domain] = explode('@', $email, 2); $field->setFormattedValue(substr($local, 0, 1).'***@'.$domain); - } - } + } + } .. tip:: diff --git a/src/Filter/ChoiceFilter.php b/src/Filter/ChoiceFilter.php index dd79515c32..dcd0283041 100644 --- a/src/Filter/ChoiceFilter.php +++ b/src/Filter/ChoiceFilter.php @@ -39,16 +39,44 @@ public function setChoices(array $choices): self } /** - * @param array $choiceGenerator + * @param array $choiceGenerator */ public function setTranslatableChoices(array $choiceGenerator): self { + // Support passing a list of enum cases (e.g. MyEnum::cases()) when the + // enum implements TranslatableInterface. In that case, the submitted + // value must be the enum backing value (or its name for UnitEnum), + // otherwise the keys (0, 1, 2, ...) of the list would be submitted + // and the query would never match any row. + if (array_is_list($choiceGenerator) && [] !== $choiceGenerator && $this->areAllEnums($choiceGenerator)) { + $choices = []; + foreach ($choiceGenerator as $case) { + $key = $case instanceof \BackedEnum ? $case->value : $case->name; + $choices[$key] = $case; + } + $choiceGenerator = $choices; + } + $this->dto->setFormTypeOption('value_type_options.choices', array_keys($choiceGenerator)); $this->dto->setFormTypeOption('value_type_options.choice_label', static fn ($value) => $choiceGenerator[$value]); return $this; } + /** + * @param array $values + */ + private function areAllEnums(array $values): bool + { + foreach ($values as $value) { + if (!$value instanceof \UnitEnum) { + return false; + } + } + + return true; + } + public function renderExpanded(bool $isExpanded = true): self { $this->dto->setFormTypeOption('value_type_options.expanded', $isExpanded); diff --git a/tests/Unit/Filter/ChoiceFilterTest.php b/tests/Unit/Filter/ChoiceFilterTest.php new file mode 100644 index 0000000000..e38f10d265 --- /dev/null +++ b/tests/Unit/Filter/ChoiceFilterTest.php @@ -0,0 +1,65 @@ +setTranslatableChoices([ + 'open' => new TranslatableMessage('Open'), + 'closed' => new TranslatableMessage('Closed'), + ]); + $dto = $filter->getAsDto(); + + $this->assertSame(['open', 'closed'], $dto->getFormTypeOption('value_type_options.choices')); + + $choiceLabel = $dto->getFormTypeOption('value_type_options.choice_label'); + $this->assertIsCallable($choiceLabel); + $this->assertInstanceOf(TranslatableMessage::class, $choiceLabel('open')); + $this->assertInstanceOf(TranslatableMessage::class, $choiceLabel('closed')); + } + + public function testSetTranslatableChoicesWithTranslatableBackedEnumCases(): void + { + $filter = ChoiceFilter::new('status')->setTranslatableChoices(TranslatableBackedEnumChoice::cases()); + $dto = $filter->getAsDto(); + + // the submitted values must be the enum backing values, not the list indices (0, 1, ...) + $this->assertSame(['draft', 'published'], $dto->getFormTypeOption('value_type_options.choices')); + + $choiceLabel = $dto->getFormTypeOption('value_type_options.choice_label'); + $this->assertIsCallable($choiceLabel); + $this->assertSame(TranslatableBackedEnumChoice::Draft, $choiceLabel('draft')); + $this->assertSame(TranslatableBackedEnumChoice::Published, $choiceLabel('published')); + } + + public function testSetTranslatableChoicesWithBackedEnumCases(): void + { + $filter = ChoiceFilter::new('status')->setTranslatableChoices(BackedEnumChoice::cases()); + $dto = $filter->getAsDto(); + + $this->assertSame(['draft', 'published'], $dto->getFormTypeOption('value_type_options.choices')); + + $choiceLabel = $dto->getFormTypeOption('value_type_options.choice_label'); + $this->assertSame(BackedEnumChoice::Draft, $choiceLabel('draft')); + } + + public function testSetTranslatableChoicesWithTranslatableUnitEnumCases(): void + { + $filter = ChoiceFilter::new('status')->setTranslatableChoices(TranslatableUnitEnumChoice::cases()); + $dto = $filter->getAsDto(); + + $this->assertSame(['Draft', 'Published'], $dto->getFormTypeOption('value_type_options.choices')); + + $choiceLabel = $dto->getFormTypeOption('value_type_options.choice_label'); + $this->assertSame(TranslatableUnitEnumChoice::Draft, $choiceLabel('Draft')); + } +} diff --git a/tests/Unit/Filter/Fixtures/BackedEnumChoice.php b/tests/Unit/Filter/Fixtures/BackedEnumChoice.php new file mode 100644 index 0000000000..ef09cc1cb1 --- /dev/null +++ b/tests/Unit/Filter/Fixtures/BackedEnumChoice.php @@ -0,0 +1,9 @@ + $translator->trans('TranslatableBackedEnumChoice.draft', locale: $locale), + self::Published => $translator->trans('TranslatableBackedEnumChoice.published', locale: $locale), + }; + } +} diff --git a/tests/Unit/Filter/Fixtures/TranslatableUnitEnumChoice.php b/tests/Unit/Filter/Fixtures/TranslatableUnitEnumChoice.php new file mode 100644 index 0000000000..02a3866921 --- /dev/null +++ b/tests/Unit/Filter/Fixtures/TranslatableUnitEnumChoice.php @@ -0,0 +1,20 @@ + $translator->trans('TranslatableUnitEnumChoice.draft', locale: $locale), + self::Published => $translator->trans('TranslatableUnitEnumChoice.published', locale: $locale), + }; + } +}