8000 [Form] Fixed handling of choices passed in choice groups · symfony/symfony@7510419 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7510419

Browse files
committed
[Form] Fixed handling of choices passed in choice groups
1 parent cc13cc5 commit 7510419

20 files changed

+810
-344
lines changed

src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,21 @@ class ArrayChoiceList implements ChoiceListInterface
3030
*
3131
* @var array
3232
*/
33-
protected $choices = array();
33+
protected $choices;
3434

3535
/**
36-
* The values of the choices.
36+
* The values indexed by the original keys.
3737
*
38-
* @var string[]
38+
* @var array
39+
*/
40+
protected $structuredValues;
41+
42+
/**
43+
* The original keys of the choices array.
44+
*
45+
* @var int[]|string[]
3946
*/
40-
protected $values = array();
47+
protected $originalKeys;
4148

4249
/**
4350
* The callback for creating the value for a choice.
@@ -51,31 +58,41 @@ class ArrayChoiceList implements ChoiceListInterface
5158
*
5259
* The given choice array must have the same array keys as the value array.
5360
*
54-
* @param array $choices The selectable choices
55-
* @param callable|null $value The callable for creating the value for a
56-
* choice. If `null` is passed, incrementing
57-
* integers are used as values
61+
* @param array|\Traversable $choices The selectable choices
62+
* @param callable|null $value The callable for creating the value
63+
* for a choice. If `null` is passed,
64+
* incrementing integers are used as
65+
* values
5866
*/
59-
public function __construct(array $choices, $value = null)
67+
public function __construct($choices, $value = null)
6068
{
6169
if (null !== $value && !is_callable($value)) {
6270
throw new UnexpectedTypeException($value, 'null or callable');
6371
}
6472

65-
$this->choices = $choices;
66-
$this->values = array();
67-
$this->valueCallback = $value;
73+
if ($choices instanceof \Traversable) {
74+
$choices = iterator_to_array($choices);
75+
}
6876

69-
if (null === $value) {
70-
$i = 0;
71-
foreach ($this->choices as $key => $choice) {
72-
$this->values[$key] = (string) $i++;
73-
}
77+
if (null !== $value) {
78+
// If a deterministic value generator was passed, use it later
79+
$this->valueCallback = $value;
7480
} else {
75-
foreach ($choices as $key => $choice) {
76-
$this->values[$key] = (string) call_user_func($value, $choice);
77-
}
81+
// Otherwise simply generate incrementing integers as values
82+
$i = 0;
83+
$value = function () use (&$i) {
84+
return $i++;
85+
};
7886
}
87+
88+
// If the choices are given as recursive array (i.e. with explicit
89+
// choice groups), flatten the array. The grouping information is needed
90+
// in the view only.
91+
$this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues);
92+
93+
$this->choices = $choicesByValues;
94+
$this->originalKeys = $keysByValues;
95+
$this->structuredValues = $structuredValues;
7996
}
8097

8198
/**
@@ -91,7 +108,23 @@ public function getChoices()
91108
*/
92109
public function getValues()
93110
{
94-
return $this->values;
111+
return array_map('strval', array_keys($this->choices));
112+
}
113+
114+
/**
115+
* {@inheritdoc}
116+
*/
117+
public function getStructuredValues()
118+
{
119+
return $this->structuredValues;
120+
}
121+
122+
/**
123+
* {@inheritdoc}
124+
*/
125+
public function getOriginalKeys()
126+
{
127+
return $this->originalKeys;
95128
}
96129

97130
/**
@@ -102,17 +135,8 @@ public function getChoicesForValues(array $values)
102135
$choices = array();
103136

104137
foreach ($values as $i => $givenValue) {
105-
foreach ($this->values as $j => $value) {
106-
if ($value !== (string) $givenValue) {
107-
continue;
108-
}
109-
110-
$choices[$i] = $this->choices[$j];
111-
unset($values[$i]);
112-
113-
if (0 === count($values)) {
114-
break 2;
115-
}
138+
if (isset($this->choices[$givenValue])) {
139+
$choices[$i] = $this->choices[$givenValue];
116140
}
117141
}
118142

@@ -131,28 +155,56 @@ public function getValuesForChoices(array $choices)
131155
$givenValues = array();
132156

133157
foreach ($choices as $i => $givenChoice) {
134-
$givenValues[$i] = (string) call_user_func($this->valueCallback, $givenChoice);
158+
$givenValues[$i] = call_user_func($this->valueCallback, $givenChoice);
135159
}
136160

137-
return array_intersect($givenValues, $this->values);
161+
return array_intersect($givenValues, array_keys($this->choices));
138162
}
139163

140164
// Otherwise compare choices by identity
141165
foreach ($choices as $i => $givenChoice) {
142-
foreach ($this->choices as $j => $choice) {
143-
if ($choice !== $givenChoice) {
144-
continue;
145-
}
146-
147-
$values[$i] = $this->values[$j];
148-
unset($choices[$i]);
149-
150-
if (0 === count($choices)) {
151-
break 2;
166+
foreach ($this->choices as $value => $choice) {
167+
if ($choice === $givenChoice) {
168+
$values[$i] = (string) $value;
169+
break;
152170
}
153171
}
154172
}
155173

156174
return $values;
157175
}
176+
177+
/**
178+
* Flattens an array into the given output variables.
179+
*
180+
* @param array $choices The array to flatten
181+
* @param callable $value The callable for generating choice values
182+
* @param array $choicesByValues The flattened choices indexed by the
183+
* corresponding values
184+
* @param array $keysByValues The original keys indexed by the
185+
* corresponding values
186+
*
187+
* @internal Must not be used by user-land code
188+
*/
189+
protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues)
190+
{
191+
if (null === $choicesByValues) {
192+
$choicesByValues = array();
193+
$keysByValues = array();
194+
$structuredValues = array();
195+
}
196+
197+
foreach ($choices as $key => $choice) {
198+
if (is_array($choice)) {
199+
$this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]);
200+
201+
continue;
202+
}
203+
204+
$choiceValue = (string) call_user_func($value, $choice);
205+
$choicesByValues[$choiceValue] = $choice;
206+
$keysByValues[$choiceValue] = $key;
207+
$structuredValues[$key] = $choiceValue;
208+
}
209+
}
158210
}

src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class ArrayKeyChoiceList extends ArrayChoiceList
6262
* @return int|string The choice as PHP array key
6363
*
6464
* @throws InvalidArgumentException If the choice is not scalar
65+
*
66+
* @internal Must not be used outside this class
6567
*/
6668
public static function toArrayKey($choice)
6769
{
@@ -89,23 +91,27 @@ public static function toArrayKey($choice)
8991
* If no values are given, the choices are cast to strings and used as
9092
* values.
9193
*
92-
* @param array $choices The selectable choices
93-
* @param callable $value The callable for creating the value for a
94-
* choice. If `null` is passed, the choices are
95-
* cast to strings and used as values
94+
* @param array|\Traversable $choices The selectable choices
95+
* @param callable $value The callable for creating the value
96+
* for a choice. If `null` is passed, the
97+
* choices are cast to strings and used
98+
* as values
9699
*
97100
* @throws InvalidArgumentException If the keys of the choices don't match
98101
* the keys of the values or if any of the
99102
* choices is not scalar
100103
*/
101-
public function __construct(array $choices, $value = null)
104+
public function __construct($choices, $value = null)
102105
{
103-
$choices = array_map(array(__CLASS__, 'toArrayKey'), $choices);
104-
106+
// If no values are given, use the choices as values
107+
// Since the choices are stored in the collection keys, i.e. they are
108+
// strings or integers, we are guaranteed to be able to convert them
109+
// to strings
105110
if (null === $value) {
106111
$value = function ($choice) {
107112
return (string) $choice;
108113
};
114+
109115
$this->useChoicesAsValues = true;
110116
}
111117

@@ -122,7 +128,7 @@ public function getChoicesForValues(array $values)
122128

123129
// If the values are identical to the choices, so we can just return
124130
// them to improve performance a little bit
125-
return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values));
131+
return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, array_keys($this->choices)));
126132
}
127133

128134
return parent::getChoicesForValues($values);
@@ -143,4 +149,38 @@ public function getValuesForChoices(array $choices)
143149

144150
return parent::getValuesForChoices($choices);
145151
}
152+
153+
/**
154+
* Flattens and flips an array into the given output variable.
155+
*
156+
* @param array $choices The array to flatten
157+
* @param callable $value The callable for generating choice values
158+
* @param array $choicesByValues The flattened choices indexed by the
159+
* corresponding values
160+
* @param array $keysByValues The original keys indexed by the
161+
* corresponding values
162+
*
163+
* @internal Must not be used by user-land code
164+
*/
165+
protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues)
166+
{
167+
if (null === $choicesByValues) {
168+
$choicesByValues = array();
169+
$keysByValues = array();
170+
$structuredValues = array();
171+
}
172+
173+
foreach ($choices as $choice => $key) {
174+
if (is_array($key)) {
175+
$this->flatten($key, $value, $choicesByValues, $keysByValues, $structuredValues[$choice]);
176+
177+
continue;
178+
}
179+
180+
$choiceValue = (string) call_user_func($value, $choice);
181+
$choicesByValues[$choiceValue] = $choice;
182+
$keysByValues[$choiceValue] = $key;
183+
$structuredValues[$key] = $choiceValue;
184+
}
185+
}
146186
}

src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,80 @@
1414
/**
1515
* A list of choices that can be selected in a choice field.
1616
*
17-
* A choice list assigns string values to each of a list of choices. These
18-
* string values are displayed in the "value" attributes in HTML and submitted
19-
* back to the server.
17+
* A choice list assigns unique string values to each of a list of choices.
18+
* These string values are displayed in the "value" attributes in HTML and
19+
* submitted back to the server.
2020
*
2121
* The acceptable data types for the choices depend on the implementation.
2222
* Values must always be strings and (within the list) free of duplicates.
2323
*
24-
* The choices returned by {@link getChoices()} and the values returned by
25-
* {@link getValues()} must have the same array indices.
26-
*
2724
* @author Bernhard Schussek <bschussek@gmail.com>
2825
*/
2926
interface ChoiceListInterface
3027
{
3128
/**
3229
* Returns all selectable choices.
3330
*
34-
* The keys of the choices correspond to the keys of the values returned by
35-
* {@link getValues()}.
36-
*
37-
* @return array The selectable choices
31+
* @return array The selectable choices indexed by the corresponding values
3832
*/
3933
public function getChoices();
4034

4135
/**
4236
* Returns the values for the choices.
4337
*
44-
* The keys of the values correspond to the keys of the choices returned by
45-
* {@link getChoices()}.
38+
* The values are strings that do not contain duplicates.
4639
*
4740
* @return string[] The choice values
4841
*/
4942
public function getValues();
5043

44+
/**
45+
* Returns the values in the structure originally passed to the list.
46+
*
47+
* Contrary to {@link getValues()}, the result is indexed by the original
48+
* keys of the choices. If the original array contained nested arrays, these
49+
* nested arrays are represented here as well:
50+
*
51+
* $form->add('field', 'choice', array(
52+
* 'choices' => array(
53+
* 'Decided' => array('Yes' => true, 'No' => false),
54+
* 'Undecided' => array('Maybe' => null),
55+
* ),
56+
* ));
57+
*
58+
* In this example, the result of this method is:
59+
*
60+
* array(
61+
* 'Decided' => array('Yes' => '0', 'No' => '1'),
62+
* 'Undecided' => array('Maybe' => '2'),
63+
* )
64+
*
65+
* @return string[] The choice values
66+
*/
67+
public function getStructuredValues();
68+
69+
/**
70+
* Returns the original keys of the choices.
71+
*
72+
* The original keys are the keys of the choice array that was passed in the
73+
* "choice" option of the choice type. Note that this array may contain
74+
* duplicates if the "choice" option contained choice groups:
75+
*
76+
* $form->add('field', 'choice', array(
77+
* 'choices' => array(
78+
* 'Decided' => array(true, false),
9A44 79+
* 'Undecided' => array(null),
80+
* ),
81+
* ));
82+
*
83+
* In this example, the original key 0 appears twice, once for `true` and
84+
* once for `null`.
85+
*
86+
* @return int[]|string[] The original choice keys indexed by the
87+
* corresponding choice values
88+
*/
89+
public function getOriginalKeys();
90+
5191
/**
5292
* Returns the choices corresponding to the given values.
5393
*

0 commit comments

Comments
 (0)
0