diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index 156735b81751e..54c54baedca68 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -53,6 +53,27 @@ class ArrayChoiceList implements ChoiceListInterface */ protected $valueCallback; + /** + * The raw choice of the choices array. + * + * @var array + */ + protected $rawChoices; + + /** + * The raw choice values of the choices array. + * + * @var array + */ + protected $rawChoiceValues; + + /** + * The raw keys of the choices array. + * + * @var int[]|string[] + */ + protected $rawKeys; + /** * Creates a list with the given choices and values. * @@ -63,6 +84,8 @@ class ArrayChoiceList implements ChoiceListInterface * for a choice. If `null` is passed, * incrementing integers are used as * values + * + * @throws UnexpectedTypeException */ public function __construct($choices, $value = null) { @@ -88,11 +111,14 @@ public function __construct($choices, $value = null) // If the choices are given as recursive array (i.e. with explicit // choice groups), flatten the array. The grouping information is needed // in the view only. - $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues); + $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues, $rawChoices, $rawChoiceValues, $rawKeys); $this->choices = $choicesByValues; $this->originalKeys = $keysByValues; $this->structuredValues = $structuredValues; + $this->rawChoices = $rawChoices; + $this->rawChoiceValues = $rawChoiceValues; + $this->rawKeys = $rawKeys; } /** @@ -127,6 +153,30 @@ public function getOriginalKeys() return $this->originalKeys; } + /** + * {@inheritdoc} + */ + public function getRawChoices() + { + return $this->rawChoices; + } + + /** + * {@inheritdoc} + */ + public function getRawChoiceValues() + { + return $this->rawChoiceValues; + } + + /** + * {@inheritdoc} + */ + public function getRawKeys() + { + return $this->rawKeys; + } + /** * {@inheritdoc} */ @@ -183,20 +233,27 @@ public function getValuesForChoices(array $choices) * corresponding values * @param array $keysByValues The original keys indexed by the * corresponding values + * @param $structuredValues + * @param $rawChoices + * @param $rawChoiceValues + * @param $rawKeys * * @internal Must not be used by user-land code */ - protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues) + protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues, &$rawChoices, &$rawChoiceValues, &$rawKeys) { if (null === $choicesByValues) { $choicesByValues = array(); $keysByValues = array(); $structuredValues = array(); + $rawChoices = array(); + $rawChoiceValues = array(); + $rawKeys = array(); } foreach ($choices as $key => $choice) { if (is_array($choice)) { - $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]); + $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key], $rawChoices[$key], $rawChoiceValues[$key], $rawKeys[$key]); continue; } @@ -205,6 +262,9 @@ protected function flatten(array $choices, $value, &$choicesByValues, &$keysByVa $choicesByValues[$choiceValue] = $choice; $keysByValues[$choiceValue] = $key; $structuredValues[$key] = $choiceValue; + $rawChoices[$key] = $choice; + $rawChoiceValues[$key] = $choiceValue; + $rawKeys[$key] = $key; } } } diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index 7c3c107d0f720..361353b1b98dd 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -159,20 +159,27 @@ public function getValuesForChoices(array $choices) * corresponding values * @param array $keysByValues The original keys indexed by the * corresponding values + * @param $structuredValues + * @param $rawChoices + * @param $rawChoiceValues + * @param $rawKeys * * @internal Must not be used by user-land code */ - protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues) + protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues, &$rawChoices, &$rawChoiceValues, &$rawKeys) { if (null === $choicesByValues) { $choicesByValues = array(); $keysByValues = array(); $structuredValues = array(); + $rawChoices = array(); + $rawChoiceValues = array(); + $rawKeys = array(); } foreach ($choices as $choice => $key) { if (is_array($key)) { - $this->flatten($key, $value, $choicesByValues, $keysByValues, $structuredValues[$choice]); + $this->flatten($key, $value, $choicesByValues, $keysByValues, $structuredValues[$choice], $rawChoices[$choice], $rawChoiceValues[$choice], $rawKeys[$choice]); continue; } @@ -181,6 +188,9 @@ protected function flatten(array $choices, $value, &$choicesByValues, &$keysByVa $choicesByValues[$choiceValue] = $choice; $keysByValues[$choiceValue] = $key; $structuredValues[$key] = $choiceValue; + $rawChoices[$choice] = $choiceValue; + $rawChoiceValues[$choice] = $choiceValue; + $rawKeys[$choice] = $key; } } } diff --git a/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php index b59e77bf795a5..ac13a22cf82f9 100644 --- a/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php @@ -88,6 +88,27 @@ public function getStructuredValues(); */ public function getOriginalKeys(); + /** + * Returns the raw choices. + * + * @return array The raw choices + */ + public function getRawChoices(); + + /** + * Returns the raw choice values. + * + * @return array The raw choice values + */ + public function getRawChoiceValues(); + + /** + * Returns the raw keys. + * + * @return int[]|string[] The raw choice keys + */ + public function getRawKeys(); + /** * Returns the choices corresponding to the given values. * diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index ef93ffdd76336..3dd97b973be5b 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -103,12 +103,14 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, // choice is not added to any group if (is_callable($groupBy)) { foreach ($choices as $value => $choice) { + $stringValue = (string) $value; + self::addChoiceViewGroupedBy( $groupBy, $choice, - (string) $value, + $stringValue, $label, - $keys, + $keys[$stringValue], $index, $attr, $preferredChoices, @@ -118,11 +120,11 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, } } else { // Otherwise use the original structure of the choices - self::addChoiceViewsGroupedBy( - $list->getStructuredValues(), + self::addChoiceViews( + $list->getRawChoices(), + $list->getRawChoiceValues(), $label, - $choices, - $keys, + $list->getRawKeys(), $index, $attr, $preferredChoices, @@ -148,48 +150,47 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, return new ChoiceListView($otherViews, $preferredViews); } - private static function addChoiceView($choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceView($data, $value, $labelCallback, $label, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { // $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. - $key = $keys[$value]; - $nextIndex = is_int($index) ? $index++ : call_user_func($index, $choice, $key, $value); + $nextIndex = is_int($index) ? $index++ : call_user_func($index, $data, $label, $value); $view = new ChoiceView( - $choice, + $data, $value, // If the labels are null, use the original choice key by default - null === $label ? (string) $key : (string) call_user_func($label, $choice, $key, $value), + null === $labelCallback ? (string) $label : (string) call_user_func($labelCallback, $data, $label, $value), // The attributes may be a callable or a mapping from choice indices // to nested arrays - is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : array()) + is_callable($attr) ? call_user_func($attr, $data, $label, $value) : (isset($attr[$label]) ? $attr[$label] : array()) ); // $isPreferred may be null if no choices are preferred - if ($isPreferred && call_user_func($isPreferred, $choice, $key, $value)) { + if ($isPreferred && call_user_func($isPreferred, $data, $label, $value)) { $preferredViews[$nextIndex] = $view; } else { $otherViews[$nextIndex] = $view; } } - private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceViews($dataValues, $values, $labelCallback, $labels, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { - foreach ($groupBy as $key => $value) { - if (null === $value) { + foreach ($dataValues as $key => $data) { + if (null === $data) { continue; } // Add the contents of groups to new ChoiceGroupView instances - if (is_array($value)) { + if (is_array($data)) { $preferredViewsForGroup = array(); $otherViewsForGroup = array(); - self::addChoiceViewsGroupedBy( - $value, - $label, - $choices, - $keys, + self::addChoiceViews( + $data, + $values[$key], + $labelCallback, + $labels[$key], $index, $attr, $isPreferred, @@ -210,10 +211,10 @@ private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $key // Add ungrouped items directly self::addChoiceView( - $choices[$value], - $value, - $label, - $keys, + $data, + $values[$key], + $labelCallback, + $labels[$key], $index, $attr, $isPreferred, @@ -223,17 +224,17 @@ private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $key } } - private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceViewGroupedBy($groupBy, $data, $value, $labelCallback, $label, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { - $groupLabel = call_user_func($groupBy, $choice, $keys[$value], $value); + $groupLabel = call_user_func($groupBy, $data, $label, $value); if (null === $groupLabel) { // If the callable returns null, don't group the choice self::addChoiceView( - $choice, + $data, $value, + $labelCallback, $label, - $keys, $index, $attr, $isPreferred, @@ -254,10 +255,10 @@ private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label } self::addChoiceView( - $choice, + $data, $value, + $labelCallback, $label, - $keys, $index, $attr, $isPreferred, diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php index f691d71330715..11a699e0e4ab7 100644 --- a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -101,6 +101,42 @@ public function getStructuredValues() return $this->loadedList->getStructuredValues(); } + /** + * {@inheritdoc} + */ + public function getRawChoices() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getRawChoices(); + } + + /** + * {@inheritdoc} + */ + public function getRawChoiceValues() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getRawChoiceValues(); + } + + /** + * {@inheritdoc} + */ + public function getRawKeys() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getRawKeys(); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php b/src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php index 929ef8c290ded..46afd8d2c2ab7 100644 --- a/src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php +++ b/src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php @@ -101,6 +101,30 @@ public function getOriginalKeys() return array_flip($this->structuredValues); } + /** + * {@inheritdoc} + */ + public function getRawChoices() + { + throw new \BadMethodCallException('Raw choices not support by LegacyChoiceList'); + } + + /** + * {@inheritdoc} + */ + public function getRawChoiceValues() + { + throw new \BadMethodCallException('Raw choice values not support by LegacyChoiceList'); + } + + /** + * {@inheritdoc} + */ + public function getRawKeys() + { + throw new \BadMethodCallException('Raw keys not support by LegacyChoiceList'); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index 741cde5b43921..32fa0790b6dd6 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Tests\ChoiceList\Factory; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\LazyChoiceList; @@ -574,6 +575,107 @@ public function testCreateViewFlatGroupByOriginalStructure() $this->assertGroupedView($view); } + public function testCreateViewDuplicateArrayChoiceListValues() + { + $list = new ArrayChoiceList( + array( + 'A' => 'a', + 'AA' => 'a', + 'Group 1' => array( + 'E' => 'e', + 'EE' => 'e', + 'A' => 'abc', + ), + 'Group 2' => array( + 'B' => 'b', + 'E' => 'e', + ), + 'AAA' => 'a', + ), + function ($choice) { + return $choice; + } + ); + + $view = $this->factory->createView($list); + + $this->assertEquals( + new ChoiceListView( + array( + 0 => new ChoiceView('a', 'a', 'A'), + 1 => new ChoiceView('a', 'a', 'AA'), + 'Group 1' => new ChoiceGroupView( + 'Group 1', + array( + 2 => new ChoiceView('e', 'e', 'E'), + 3 => new ChoiceView('e', 'e', 'EE'), + 4 => new ChoiceView('abc', 'abc', 'A'), + ) + ), + 'Group 2' => new ChoiceGroupView( + 'Group 2', + array( + 5 => new ChoiceView('b', 'b', 'B'), + 6 => new ChoiceView('e', 'e', 'E'), + ) + ), + 7 => new ChoiceView('a', 'a', 'AAA'), + ), + array() + ), + $view + ); + } + + public function testCreateViewDuplicateArrayKeyChoiceListValues() + { + $list = new ArrayKeyChoiceList( + array( + 'A' => 'a', + 'AA' => 'a', + 'Group 1' => array( + 'E' => 'e', + 'EE' => 'e', + 'A' => 'abc', + ), + 'Group 2' => array( + 'B' => 'b', + 'E' => 'e', + ), + 'AAA' => 'a', + ) + ); + + $view = $this->factory->createView($list); + + $this->assertEquals( + new ChoiceListView( + array( + 0 => new ChoiceView('A', 'A', 'a'), + 1 => new ChoiceView('AA', 'AA', 'a'), + 'Group 1' => new ChoiceGroupView( + 'Group 1', + array( + 2 => new ChoiceView('E', 'E', 'e'), + 3 => new ChoiceView('EE', 'EE', 'e'), + 4 => new ChoiceView('A', 'A', 'abc'), + ) + ), + 'Group 2' => new ChoiceGroupView( + 'Group 2', + array( + 5 => new ChoiceView('B', 'B', 'b'), + 6 => new ChoiceView('E', 'E', 'e'), + ) + ), + 7 => new ChoiceView('AAA', 'AAA', 'a'), + ), + array() + ), + $view + ); + } + public function testCreateViewFlatGroupByEmpty() { $view = $this->factory->createView(