diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 61d9bbc503..75d1928ce1 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -20,7 +20,10 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView; * A choice list for choices of arbitrary data types. * * Choices and labels are passed in two arrays. The indices of the choices - * and the labels should match. + * and the labels should match. Choices may also be given as hierarchy of + * unlimited depth by creating nested arrays. The title of the sub-hierarchy + * can be stored in the array key pointing to the nested array. The topmost + * level of the hierarchy may also be a \Traversable. * * * $choices = array(true, false); @@ -69,14 +72,21 @@ class ChoiceList implements ChoiceListInterface * as hierarchy of unlimited depth. Hierarchies are * created by creating nested arrays. The title of * the sub-hierarchy can be stored in the array - * key pointing to the nested array. + * key pointing to the nested array. The topmost + * level of the hierarchy may also be a \Traversable. * @param array $labels The array of labels. The structure of this array * should match the structure of $choices. * @param array $preferredChoices A flat array of choices that should be * presented to the user with priority. + * + * @throws UnexpectedTypeException If the choices are not an array or \Traversable. */ public function __construct($choices, array $labels, array $preferredChoices = array()) { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + $this->initialize($choices, $labels, $preferredChoices); } @@ -236,17 +246,17 @@ class ChoiceList implements ChoiceListInterface /** * Recursively adds the given choices to the list. * - * @param array $bucketForPreferred The bucket where to store the preferred - * view objects. - * @param array $bucketForRemaining The bucket where to store the - * non-preferred view objects. - * @param array $choices The list of choices. - * @param array $labels The labels corresponding to the choices. - * @param array $preferredChoices The preferred choices. + * @param array $bucketForPreferred The bucket where to store the preferred + * view objects. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred view objects. + * @param array|\Traversable $choices The list of choices. + * @param array $labels The labels corresponding to the choices. + * @param array $preferredChoices The preferred choices. * * @throws InvalidConfigurationException If no valid value or index could be created for a choice. */ - protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, array $choices, array $labels, array $preferredChoices) + protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, $choices, array $labels, array $preferredChoices) { // Add choices to the nested buckets foreach ($choices as $group => $choice) { @@ -278,13 +288,13 @@ class ChoiceList implements ChoiceListInterface * Recursively adds a choice group. * * @param string $group The name of the group. - * @param array $bucketForPreferred The bucket where to store the preferred - * view objects. - * @param array $bucketForRemaining The bucket where to store the - * non-preferred view objects. - * @param array $choices The list of choices in the group. - * @param array $labels The labels corresponding to the choices in the group. - * @param array $preferredChoices The preferred choices. + * @param array $bucketForPreferred The bucket where to store the preferred + * view objects. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred view objects. + * @param array $choices The list of choices in the group. + * @param array $labels The labels corresponding to the choices in the group. + * @param array $preferredChoices The preferred choices. * * @throws InvalidConfigurationException If no valid value or index could be created for a choice. */ diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index 6c3c79bf8a..89e06c217b 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -13,7 +13,6 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; use Symfony\Component\Form\Util\PropertyPath; use Symfony\Component\Form\Exception\StringCastException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\InvalidPropertyException; /** @@ -58,10 +57,11 @@ class ObjectChoiceList extends ChoiceList * Creates a new object choice list. * * @param array|\Traversable $choices The array of choices. Choices may also be given - * as hierarchy of unlimited depth. Hierarchies are - * created by creating nested arrays. The title of - * the sub-hierarchy can be stored in the array - * key pointing to the nested array. + * as hierarchy of unlimited depth by creating nested + * arrays. The title of the sub-hierarchy can be + * stored in the array key pointing to the nested + * array. The topmost level of the hierarchy may also + * be a \Traversable. * @param string $labelPath A property path pointing to the property used * for the choice labels. The value is obtained * by calling the getter on the object. If the @@ -93,19 +93,18 @@ class ObjectChoiceList extends ChoiceList * @param array|\Traversable $choices The choices to write into the list. * @param array $labels Ignored. * @param array $preferredChoices The choices to display with priority. + * + * @throws \InvalidArgumentException When passing a hierarchy of choices and using + * the "groupPath" option at the same time. */ protected function initialize($choices, array $labels, array $preferredChoices) { - if (!is_array($choices) && !$choices instanceof \Traversable) { - throw new UnexpectedTypeException($choices, 'array or \Traversable'); - } - if (null !== $this->groupPath) { $groupedChoices = array(); foreach ($choices as $i => $choice) { if (is_array($choice)) { - throw new \InvalidArgumentException('You should pass a plain object array (without groups, $code, $previous) when using the "groupPath" option'); + throw new \InvalidArgumentException('You should pass a plain object array (without groups, $code, $previous) when using the "groupPath" option.'); } try { @@ -142,10 +141,10 @@ class ObjectChoiceList extends ChoiceList * * If a property path for the value was given at object creation, * the getter behind that path is now called to obtain a new value. - * * Otherwise a new integer is generated. * * @param mixed $choice The choice to create a value for + * * @return integer|string A unique value without character limitations. */ protected function createValue($choice) @@ -160,7 +159,7 @@ class ObjectChoiceList extends ChoiceList private function extractLabels($choices, array &$labels) { foreach ($choices as $i => $choice) { - if (is_array($choice) || $choice instanceof \Traversable) { + if (is_array($choice)) { $labels[$i] = array(); $this->extractLabels($choice, $labels[$i]); } elseif ($this->labelPath) { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 109a461b4a..7792676e2f 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -11,14 +11,14 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; -use Symfony\Component\Form\Exception\UnexpectedTypeException; - /** * A choice list for choices of type string or integer. * * Choices and their associated labels can be passed in a single array. Since * choices are passed as array keys, only strings or integer choices are - * allowed. + * allowed. Choices may also be given as hierarchy of unlimited depth by + * creating nested arrays. The title of the sub-hierarchy can be stored in the + * array key pointing to the nested array. * * * $choiceList = new SimpleChoiceList(array( @@ -36,10 +36,9 @@ class SimpleChoiceList extends ChoiceList * * @param array $choices The array of choices with the choices as keys and * the labels as values. Choices may also be given - * as hierarchy of unlimited depth. Hierarchies are - * created by creating nested arrays. The title of - * the sub-hierarchy is stored in the array - * key pointing to the nested array. + * as hierarchy of unlimited depth by creating nested + * arrays. The title of the sub-hierarchy is stored + * in the array key pointing to the nested array. * @param array $preferredChoices A flat array of choices that should be * presented to the user with priority. */ @@ -74,12 +73,20 @@ class SimpleChoiceList extends ChoiceList } /** - * {@inheritdoc} + * Recursively adds the given choices to the list. * * Takes care of splitting the single $choices array passed in the * constructor into choices and labels. + * + * @param array $bucketForPreferred The bucket where to store the preferred + * view objects. + * @param array $bucketForRemaining The bucket where to store the + * non-preferred view objects. + * @param array|\Traversable $choices The list of choices. + * @param array $labels Ignored. + * @param array $preferredChoices The preferred choices. */ - protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, array $choices, array $labels, array $preferredChoices) + protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, $choices, array $labels, array $preferredChoices) { // Add choices to the nested buckets foreach ($choices as $choice => $label) { diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php index 534563547f..bfd58ee583 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php @@ -73,6 +73,38 @@ class ChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(0 => new ChoiceView($this->obj1, '0', 'A'), 2 => new ChoiceView($this->obj3, '2', 'C'), 3 => new ChoiceView($this->obj4, '3', 'D')), $this->list->getRemainingViews()); } + /** + * Necessary for interoperability with MongoDB cursors or ORM relations as + * choices parameter. A choice itself that is an object implementing \Traversable + * is not treated as hierarchical structure, but as-is. + */ + public function testInitNestedTraversable() + { + $traversableChoice = new \ArrayIterator(array($this->obj3, $this->obj4)); + + $this->list = new ChoiceList( + new \ArrayIterator(array( + 'Group' => array($this->obj1, $this->obj2), + 'Not a Group' => $traversableChoice + )), + array( + 'Group' => array('A', 'B'), + 'Not a Group' => 'C', + ), + array($this->obj2) + ); + + $this->assertSame(array($this->obj1, $this->obj2, $traversableChoice), $this->list->getChoices()); + $this->assertSame(array('0', '1', '2'), $this->list->getValues()); + $this->assertEquals(array( + 'Group' => array(1 => new ChoiceView($this->obj2, '1', 'B')) + ), $this->list->getPreferredViews()); + $this->assertEquals(array( + 'Group' => array(0 => new ChoiceView($this->obj1, '0', 'A')), + 2 => new ChoiceView($traversableChoice, '2', 'C') + ), $this->list->getRemainingViews()); + } + public function testInitNestedArray() { $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());