Skip to content

Commit 4b745b9

Browse files
committed
[Form] Fixed: Duplicate choice labels are remembered when using "choices_as_values" = false
1 parent d1a50a2 commit 4b745b9

File tree

2 files changed

+89
-1
lines changed

2 files changed

+89
-1
lines changed

src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ public function finishView(FormView $view, FormInterface $form, array $options)
238238
*/
239239
public function configureOptions(OptionsResolver $resolver)
240240
{
241+
$choiceLabels = array();
241242
$choiceListFactory = $this->choiceListFactory;
242243

243244
$emptyData = function (Options $options) {
@@ -252,6 +253,44 @@ public function configureOptions(OptionsResolver $resolver)
252253
return $options['required'] ? null : '';
253254
};
254255

256+
// BC closure, to be removed in 3.0
257+
$choicesNormalizer = function (Options $options, $choices) use (&$choiceLabels) {
258+
// Unset labels from previous invocations
259+
$choiceLabels = array();
260+
261+
// This closure is irrelevant when "choices_as_values" is set to true
262+
if ($options['choices_as_values']) {
263+
return $choices;
264+
}
265+
266+
ChoiceType::normalizeLegacyChoices($choices, $choiceLabels);
267+
268+
return $choices;
269+
};
270+
271+
// BC closure, to be removed in 3.0
272+
$choiceLabel = function (Options $options) use (&$choiceLabels) {
273+
// If the choices contain duplicate labels, the normalizer of the
274+
// "choices" option stores them in the $choiceLabels variable
275+
276+
// Trigger the normalizer
277+
$options->offsetGet('choices');
278+
279+
// Pick labels from $choiceLabels if available
280+
// Don't invoke count() to avoid creating a copy of the array (yet)
281+
if ($choiceLabels) {
282+
// Don't pass the labels by reference. We do want to create a
283+
// copy here so that every form has an own version of that
284+
// variable (contrary to the global reference shared by all
285+
// forms)
286+
return function ($choice, $key) use ($choiceLabels) {
287+
return $choiceLabels[$key];
288+
};
289+
}
290+
291+
return;
292+
};
293+
255294
$choiceListNormalizer = function (Options $options, $choiceList) use ($choiceListFactory) {
256295
if ($choiceList) {
257296
@trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED);
@@ -322,7 +361,7 @@ public function configureOptions(OptionsResolver $resolver)
322361
'choices' => array(),
323362
'choices_as_values' => false,
324363
'choice_loader' => null,
325-
'choice_label' => null,
364+
'choice_label' => $choiceLabel,
326365
'choice_name' => null,
327366
'choice_value' => null,
328367
'choice_attr' => null,
@@ -340,6 +379,7 @@ public function configureOptions(OptionsResolver $resolver)
340379
'choice_translation_domain' => true,
341380
));
342381

382+
$resolver->setNormalizer('choices', $choicesNormalizer);
343383
$resolver->setNormalizer('choice_list', $choiceListNormalizer);
344384
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
345385
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
@@ -454,4 +494,35 @@ private function createChoiceListView(ChoiceListInterface $choiceList, array $op
454494
$options['choice_attr']
455495
);
456496
}
497+
498+
/**
499+
* When "choices_as_values" is set to false, the choices are in the keys and
500+
* their labels in the values. Labels may occur twice. The form component
501+
* flips the choices array in the new implementation, so duplicate labels
502+
* are lost. Store them in a utility array that is used from the
503+
* "choice_label" closure by default.
504+
*
505+
* @param array $choices The choice labels indexed by choices.
506+
* Labels are replaced by generated keys.
507+
* @param array $choiceLabels The array that receives the choice labels
508+
* indexed by generated keys.
509+
* @param int|null $nextKey The next generated key.
510+
*/
511+
private static function normalizeLegacyChoices(array &$choices, array &$choiceLabels, &$nextKey = null)
512+
{
513+
if (null === $nextKey) {
514+
$nextKey = 0;
515+
}
516+
517+
foreach ($choices as $choice => &$choiceLabel) {
518+
if (is_array($choiceLabel)) {
519+
self::normalizeLegacyChoices($choiceLabel, $choiceLabels, $nextKey);
520+
continue;
521+
}
522+
523+
$choiceLabels[$nextKey] = $choiceLabel;
524+
$choices[$choice] = $nextKey;
525+
++$nextKey;
526+
}
527+
}
457528
}

src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,23 @@ public function testPassChoiceDataToView()
16941694
), $view->vars['choices']);
16951695
}
16961696

1697+
/**
1698+
* @group legacy
1699+
*/
1700+
public function testDuplicateChoiceLabels()
1701+
{
1702+
$form = $this->factory->create('choice', null, array(
1703+
'choices' => array('a' => 'A', 'b' => 'B', 'c' => 'A'),
1704+
));
1705+
$view = $form->createView();
1706+
1707+
$this->assertEquals(array(
1708+
new ChoiceView('a', 'a', 'A'),
1709+
new ChoiceView('b', 'b', 'B'),
1710+
new ChoiceView('c', 'c', 'A'),
1711+
), $view->vars['choices']);
1712+
}
1713+
16971714
public function testAdjustFullNameForMultipleNonExpanded()
16981715
{
16991716
$form = $this->factory->createNamed('name', 'choice', null, array(

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy