diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTestCase.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTestCase.php index 0982b2e0e9ef..5b02b69576e6 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTestCase.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTestCase.php @@ -576,6 +576,31 @@ public function testSingleChoiceWithPreferred() ); } + public function testSingleChoiceWithPreferredIsNotDuplicated() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&b'], + 'duplicate_preferred_choices' => false, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --', 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-control"] + [not(@required)] + [ + ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + ] + [count(./option)=3] +' + ); + } + public function testSingleChoiceWithSelectedPreferred() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTestCase.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTestCase.php index 630663a60da2..576f2b18f66f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTestCase.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTestCase.php @@ -584,6 +584,31 @@ public function testSingleChoiceWithPreferred() ); } + public function testSingleChoiceWithPreferredIsNotDuplicated() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&b'], + 'duplicate_preferred_choices' => false, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --', 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + ] + [count(./option)=3] +' + ); + } + public function testSingleChoiceWithSelectedPreferred() { $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 36014a590b41..160e107417d4 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -28,7 +28,7 @@ "symfony/asset-mapper": "^6.3|^7.0", "symfony/dependency-injection": "^5.4|^6.0|^7.0", "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/form": "^6.3|^7.0", + "symfony/form": "^6.4|^7.0", "symfony/html-sanitizer": "^6.1|^7.0", "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/http-kernel": "^6.2|^7.0", diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 3918636e566a..9fba1a3f5acf 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -8,6 +8,8 @@ CHANGELOG `model_timezone` option in `DateType`, `DateTimeType`, and `TimeType` * Deprecate `PostSetDataEvent::setData()`, use `PreSetDataEvent::setData()` instead * Deprecate `PostSubmitEvent::setData()`, use `PreSubmitDataEvent::setData()` or `SubmitDataEvent::setData()` instead + * Add `duplicate_preferred_choices` option in `ChoiceType` + * Add `$duplicatePreferredChoices` parameter to `ChoiceListFactoryInterface::createView()` 6.3 --- diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php index 40c0604ea4de..03bdff5dc9d5 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -145,8 +145,12 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value return $this->lists[$hash]; } - public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = []): ChoiceListView + /** + * @param bool $duplicatePreferredChoices + */ + public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView { + $duplicatePreferredChoices = \func_num_args() > 7 ? func_get_arg(7) : true; $cache = true; if ($preferredChoices instanceof Cache\PreferredChoice) { @@ -193,11 +197,12 @@ public function createView(ChoiceListInterface $list, mixed $preferredChoices = $index, $groupBy, $attr, - $labelTranslationParameters + $labelTranslationParameters, + $duplicatePreferredChoices, ); } - $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters]); + $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters, $duplicatePreferredChoices]); if (!isset($this->views[$hash])) { $this->views[$hash] = $this->decoratedFactory->createView( @@ -207,7 +212,8 @@ public function createView(ChoiceListInterface $list, mixed $preferredChoices = $index, $groupBy, $attr, - $labelTranslationParameters + $labelTranslationParameters, + $duplicatePreferredChoices, ); } diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php index 62c3e8d2eaa2..89633710b619 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php @@ -77,6 +77,9 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, callable $va * pass false to discard the label * @param array|callable|null $attr The callable generating the HTML attributes * @param array|callable $labelTranslationParameters The parameters used to translate the choice labels + * @param bool $duplicatePreferredChoices Whether the preferred choices should be duplicated + * on top of the list and in their original position + * or only in the top of the list */ - public function createView(ChoiceListInterface $list, array|callable $preferredChoices = null, callable|false $label = null, callable $index = null, callable $groupBy = null, array|callable $attr = null, array|callable $labelTranslationParameters = []): ChoiceListView; + public function createView(ChoiceListInterface $list, array|callable $preferredChoices = null, callable|false $label = null, callable $index = null, callable $groupBy = null, array|callable $attr = null, array|callable $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView; } diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index fb30fc6ded4c..aa371362c811 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -52,8 +52,12 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, callable $va return new LazyChoiceList($loader, $value); } - public function createView(ChoiceListInterface $list, array|callable $preferredChoices = null, callable|false $label = null, callable $index = null, callable $groupBy = null, array|callable $attr = null, array|callable $labelTranslationParameters = []): ChoiceListView + /** + * @param bool $duplicatePreferredChoices + */ + public function createView(ChoiceListInterface $list, array|callable $preferredChoices = null, callable|false $label = null, callable $index = null, callable $groupBy = null, array|callable $attr = null, array|callable $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView { + $duplicatePreferredChoices = \func_num_args() > 7 ? func_get_arg(7) : true; $preferredViews = []; $preferredViewsOrder = []; $otherViews = []; @@ -92,7 +96,8 @@ public function createView(ChoiceListInterface $list, array|callable $preferredC $preferredChoices, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); } @@ -130,7 +135,8 @@ public function createView(ChoiceListInterface $list, array|callable $preferredC $preferredChoices, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); } @@ -139,7 +145,7 @@ public function createView(ChoiceListInterface $list, array|callable $preferredC return new ChoiceListView($otherViews, $preferredViews); } - private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews): void + private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void { // $value may be an integer or a string, since it's stored in the array // keys. We want to guarantee it's a string though. @@ -180,12 +186,16 @@ private static function addChoiceView($choice, string $value, $label, array $key if (null !== $isPreferred && false !== $preferredKey = $isPreferred($choice, $key, $value)) { $preferredViews[$nextIndex] = $view; $preferredViewsOrder[$nextIndex] = $preferredKey; - } - $otherViews[$nextIndex] = $view; + if ($duplicatePreferredChoices) { + $otherViews[$nextIndex] = $view; + } + } else { + $otherViews[$nextIndex] = $view; + } } - private static function addChoiceViewsFromStructuredValues(array $values, $label, array $choices, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews): void + private static function addChoiceViewsFromStructuredValues(array $values, $label, array $choices, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void { foreach ($values as $key => $value) { if (null === $value) { @@ -208,7 +218,8 @@ private static function addChoiceViewsFromStructuredValues(array $values, $label $isPreferred, $preferredViewsForGroup, $preferredViewsOrder, - $otherViewsForGroup + $otherViewsForGroup, + $duplicatePreferredChoices, ); if (\count($preferredViewsForGroup) > 0) { @@ -234,12 +245,13 @@ private static function addChoiceViewsFromStructuredValues(array $values, $label $isPreferred, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); } } - private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews): void + private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void { $groupLabels = $groupBy($choice, $keys[$value], $value); @@ -256,7 +268,8 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi $isPreferred, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); return; @@ -286,7 +299,8 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi $isPreferred, $preferredViews[$groupLabel]->choices, $preferredViewsOrder[$groupLabel], - $otherViews[$groupLabel]->choices + $otherViews[$groupLabel]->choices, + $duplicatePreferredChoices, ); } } diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index fa66290e3448..dab8a5d77acb 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -109,8 +109,12 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value return $this->decoratedFactory->createListFromLoader($loader, $value, $filter); } - public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = []): ChoiceListView + /** + * @param bool $duplicatePreferredChoices + */ + public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = []/* , bool $duplicatePreferredChoices = true */): ChoiceListView { + $duplicatePreferredChoices = \func_num_args() > 7 ? func_get_arg(7) : true; $accessor = $this->propertyAccessor; if (\is_string($label)) { @@ -182,7 +186,8 @@ public function createView(ChoiceListInterface $list, mixed $preferredChoices = $index, $groupBy, $attr, - $labelTranslationParameters + $labelTranslationParameters, + $duplicatePreferredChoices, ); } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index e31d810df12d..1cc25c3b6ed5 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -354,6 +354,7 @@ public function configureOptions(OptionsResolver $resolver) 'choice_attr' => null, 'choice_translation_parameters' => [], 'preferred_choices' => [], + 'duplicate_preferred_choices' => true, 'group_by' => null, 'empty_data' => $emptyData, 'placeholder' => $placeholderDefault, @@ -383,6 +384,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('choice_translation_parameters', ['null', 'array', 'callable', ChoiceTranslationParameters::class]); $resolver->setAllowedTypes('placeholder_attr', ['array']); $resolver->setAllowedTypes('preferred_choices', ['array', \Traversable::class, 'callable', 'string', PropertyPath::class, PreferredChoice::class]); + $resolver->setAllowedTypes('duplicate_preferred_choices', 'bool'); $resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', PropertyPath::class, GroupBy::class]); } @@ -465,7 +467,8 @@ private function createChoiceListView(ChoiceListInterface $choiceList, array $op $options['choice_name'], $options['group_by'], $options['choice_attr'], - $options['choice_translation_parameters'] + $options['choice_translation_parameters'], + $options['duplicate_preferred_choices'], ); } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json index 3a9b7a7ecce4..27371fd6f668 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json @@ -12,6 +12,7 @@ "choice_translation_parameters", "choice_value", "choices", + "duplicate_preferred_choices", "expanded", "group_by", "multiple", diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt index a15ac42dae0f..c8aee5e78327 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt @@ -14,13 +14,13 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") choice_translation_parameters invalid_message auto_initialize csrf_token_manager choice_value trim block_name choices block_prefix - expanded by_reference - group_by data - multiple disabled - placeholder form_attr - placeholder_attr getter - preferred_choices help - help_attr + duplicate_preferred_choices by_reference + expanded data + group_by disabled + multiple form_attr + placeholder getter + placeholder_attr help + preferred_choices help_attr help_html help_translation_parameters inherit_data
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: