Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[Form] fix ChoiceList and ObjectChoiceList when choices is a Traversable
  • Loading branch information
Tobion committed Jul 25, 2012
1 parent 6f7ea8d commit 8053933
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 38 deletions.
44 changes: 27 additions & 17 deletions src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php
Expand Up @@ -20,7 +20,10 @@
* 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.
*
* <code>
* $choices = array(true, false);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -236,17 +246,17 @@ public function getIndicesForValues(array $values)
/**
* 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) {
Expand Down Expand Up @@ -278,13 +288,13 @@ protected function addChoices(&$bucketForPreferred, &$bucketForRemaining, array
* 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.
*/
Expand Down
Expand Up @@ -13,7 +13,6 @@

use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\InvalidPropertyException;

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -93,19 +93,18 @@ public function __construct($choices, $labelPath = null, array $preferredChoices
* @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 {
Expand Down Expand Up @@ -142,10 +141,10 @@ protected function initialize($choices, array $labels, array $preferredChoices)
*
* 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)
Expand All @@ -160,7 +159,7 @@ protected function createValue($choice)
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) {
Expand Down
Expand Up @@ -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.
*
* <code>
* $choiceList = new SimpleChoiceList(array(
Expand All @@ -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.
*/
Expand Down Expand Up @@ -74,12 +73,20 @@ public function getValuesForChoices(array $choices)
}

/**
* {@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) {
Expand Down
Expand Up @@ -73,6 +73,38 @@ public function testInitArray()
$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());
Expand Down

0 comments on commit 8053933

Please sign in to comment.