4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Form\Extension\Core\ChoiceList
;
14 use Symfony\Component\Form\FormConfigBuilder
;
15 use Symfony\Component\Form\Exception\UnexpectedTypeException
;
16 use Symfony\Component\Form\Exception\InvalidConfigurationException
;
17 use Symfony\Component\Form\Exception\InvalidArgumentException
;
18 use Symfony\Component\Form\Extension\Core\View\ChoiceView
;
21 * A choice list for choices of arbitrary data types.
23 * Choices and labels are passed in two arrays. The indices of the choices
24 * and the labels should match. Choices may also be given as hierarchy of
25 * unlimited depth by creating nested arrays. The title of the sub-hierarchy
26 * can be stored in the array key pointing to the nested array. The topmost
27 * level of the hierarchy may also be a \Traversable.
30 * $choices = array(true, false);
31 * $labels = array('Agree', 'Disagree');
32 * $choiceList = new ChoiceList($choices, $labels);
35 * @author Bernhard Schussek <bschussek@gmail.com>
37 class ChoiceList
implements ChoiceListInterface
40 * The choices with their indices as keys.
44 private $choices = array();
47 * The choice values with the indices of the matching choices as keys.
51 private $values = array();
54 * The preferred view objects as hierarchy containing also the choice groups
55 * with the indices of the matching choices as bottom-level keys.
59 private $preferredViews = array();
62 * The non-preferred view objects as hierarchy containing also the choice
63 * groups with the indices of the matching choices as bottom-level keys.
67 private $remainingViews = array();
70 * Creates a new choice list.
72 * @param array|\Traversable $choices The array of choices. Choices may also be given
73 * as hierarchy of unlimited depth. Hierarchies are
74 * created by creating nested arrays. The title of
75 * the sub-hierarchy can be stored in the array
76 * key pointing to the nested array. The topmost
77 * level of the hierarchy may also be a \Traversable.
78 * @param array $labels The array of labels. The structure of this array
79 * should match the structure of $choices.
80 * @param array $preferredChoices A flat array of choices that should be
81 * presented to the user with priority.
83 * @throws UnexpectedTypeException If the choices are not an array or \Traversable.
85 public function __construct($choices, array $labels, array $preferredChoices = array())
87 if (!is_array($choices) && !$choices instanceof \Traversable
) {
88 throw new UnexpectedTypeException($choices, 'array or \Traversable');
91 $this->initialize($choices, $labels, $preferredChoices);
95 * Initializes the list with choices.
97 * Safe to be called multiple times. The list is cleared on every call.
99 * @param array|\Traversable $choices The choices to write into the list.
100 * @param array $labels The labels belonging to the choices.
101 * @param array $preferredChoices The choices to display with priority.
103 protected function initialize($choices, array $labels, array $preferredChoices)
105 $this->choices
= array();
106 $this->values
= array();
107 $this->preferredViews
= array();
108 $this->remainingViews
= array();
111 $this->preferredViews
,
112 $this->remainingViews
,
122 public function getChoices()
124 return $this->choices
;
130 public function getValues()
132 return $this->values
;
138 public function getPreferredViews()
140 return $this->preferredViews
;
146 public function getRemainingViews()
148 return $this->remainingViews
;
154 public function getChoicesForValues(array $values)
156 $values = $this->fixValues($values);
159 foreach ($values as $j => $givenValue) {
160 foreach ($this->values
as $i => $value) {
161 if ($value === $givenValue) {
162 $choices[] = $this->choices
[$i];
165 if (0 === count($values)) {
178 public function getValuesForChoices(array $choices)
180 $choices = $this->fixChoices($choices);
183 foreach ($this->choices
as $i => $choice) {
184 foreach ($choices as $j => $givenChoice) {
185 if ($choice === $givenChoice) {
186 $values[] = $this->values
[$i];
189 if (0 === count($choices)) {
202 public function getIndicesForChoices(array $choices)
204 $choices = $this->fixChoices($choices);
207 foreach ($this->choices
as $i => $choice) {
208 foreach ($choices as $j => $givenChoice) {
209 if ($choice === $givenChoice) {
213 if (0 === count($choices)) {
226 public function getIndicesForValues(array $values)
228 $values = $this->fixValues($values);
231 foreach ($this->values
as $i => $value) {
232 foreach ($values as $j => $givenValue) {
233 if ($value === $givenValue) {
237 if (0 === count($values)) {
248 * Recursively adds the given choices to the list.
250 * @param array $bucketForPreferred The bucket where to store the preferred
252 * @param array $bucketForRemaining The bucket where to store the
253 * non-preferred view objects.
254 * @param array|\Traversable $choices The list of choices.
255 * @param array $labels The labels corresponding to the choices.
256 * @param array $preferredChoices The preferred choices.
258 * @throws InvalidArgumentException If the structures of the choices and labels array do not match.
259 * @throws InvalidConfigurationException If no valid value or index could be created for a choice.
261 protected function addChoices(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices)
263 // Add choices to the nested buckets
264 foreach ($choices as $group => $choice) {
265 if (!array_key_exists($group, $labels)) {
266 throw new InvalidArgumentException('The structures of the choices and labels array do not match.');
269 if (is_array($choice)) {
270 // Don't do the work if the array is empty
271 if (count($choice) > 0) {
272 $this->addChoiceGroup(
294 * Recursively adds a choice group.
296 * @param string $group The name of the group.
297 * @param array $bucketForPreferred The bucket where to store the preferred
299 * @param array $bucketForRemaining The bucket where to store the
300 * non-preferred view objects.
301 * @param array $choices The list of choices in the group.
302 * @param array $labels The labels corresponding to the choices in the group.
303 * @param array $preferredChoices The preferred choices.
305 * @throws InvalidConfigurationException If no valid value or index could be created for a choice.
307 protected function addChoiceGroup($group, array &$bucketForPreferred, array &$bucketForRemaining, array $choices, array $labels, array $preferredChoices)
309 // If this is a choice group, create a new level in the choice
311 $bucketForPreferred[$group] = array();
312 $bucketForRemaining[$group] = array();
315 $bucketForPreferred[$group],
316 $bucketForRemaining[$group],
322 // Remove child levels if empty
323 if (empty($bucketForPreferred[$group])) {
324 unset($bucketForPreferred[$group]);
326 if (empty($bucketForRemaining[$group])) {
327 unset($bucketForRemaining[$group]);
334 * @param array $bucketForPreferred The bucket where to store the preferred
336 * @param array $bucketForRemaining The bucket where to store the
337 * non-preferred view objects.
338 * @param mixed $choice The choice to add.
339 * @param string $label The label for the choice.
340 * @param array $preferredChoices The preferred choices.
342 * @throws InvalidConfigurationException If no valid value or index could be created.
344 protected function addChoice(array &$bucketForPreferred, array &$bucketForRemaining, $choice, $label, array $preferredChoices)
346 $index = $this->createIndex($choice);
348 if ('' === $index || null === $index || !FormConfigBuilder
::isValidName((string) $index)) {
349 throw new InvalidConfigurationException(sprintf('The index "%s" created by the choice list is invalid. It should be a valid, non-empty Form name.', $index));
352 $value = $this->createValue($choice);
354 if (!is_string($value)) {
355 throw new InvalidConfigurationException(sprintf('The value created by the choice list is of type "%s", but should be a string.', gettype($value)));
358 $view = new ChoiceView($choice, $value, $label);
360 $this->choices
[$index] = $this->fixChoice($choice);
361 $this->values
[$index] = $value;
363 if ($this->isPreferred($choice, $preferredChoices)) {
364 $bucketForPreferred[$index] = $view;
366 $bucketForRemaining[$index] = $view;
371 * Returns whether the given choice should be preferred judging by the
372 * given array of preferred choices.
374 * Extension point to optimize performance by changing the structure of the
375 * $preferredChoices array.
377 * @param mixed $choice The choice to test.
378 * @param array $preferredChoices An array of preferred choices.
380 * @return Boolean Whether the choice is preferred.
382 protected function isPreferred($choice, array $preferredChoices)
384 return false !== array_search($choice, $preferredChoices, true);
388 * Creates a new unique index for this choice.
390 * Extension point to change the indexing strategy.
392 * @param mixed $choice The choice to create an index for
394 * @return integer|string A unique index containing only ASCII letters,
395 * digits and underscores.
397 protected function createIndex($choice)
399 return count($this->choices
);
403 * Creates a new unique value for this choice.
405 * By default, an integer is generated since it cannot be guaranteed that
406 * all values in the list are convertible to (unique) strings. Subclasses
407 * can override this behaviour if they can guarantee this property.
409 * @param mixed $choice The choice to create a value for
411 * @return string A unique string.
413 protected function createValue($choice)
415 return (string) count($this->values
);
419 * Fixes the data type of the given choice value to avoid comparison
422 * @param mixed $value The choice value.
424 * @return string The value as string.
426 protected function fixValue($value)
428 return (string) $value;
432 * Fixes the data types of the given choice values to avoid comparison
435 * @param array $values The choice values.
437 * @return array The values as strings.
439 protected function fixValues(array $values)
441 foreach ($values as $i => $value) {
442 $values[$i] = $this->fixValue($value);
449 * Fixes the data type of the given choice index to avoid comparison
452 * @param mixed $index The choice index.
454 * @return integer|string The index as PHP array key.
456 protected function fixIndex($index)
458 if (is_bool($index) || (string) (int) $index === (string) $index) {
462 return (string) $index;
466 * Fixes the data types of the given choice indices to avoid comparison
469 * @param array $indices The choice indices.
471 * @return array The indices as strings.
473 protected function fixIndices(array $indices)
475 foreach ($indices as $i => $index) {
476 $indices[$i] = $this->fixIndex($index);
483 * Fixes the data type of the given choice to avoid comparison problems.
485 * Extension point. In this implementation, choices are guaranteed to
486 * always maintain their type and thus can be typesafely compared.
488 * @param mixed $choice The choice.
490 * @return mixed The fixed choice.
492 protected function fixChoice($choice)
498 * Fixes the data type of the given choices to avoid comparison problems.
500 * @param array $choices The choices.
502 * @return array The fixed choices.
506 protected function fixChoices(array $choices)