merged branch Tobion/choicelist (PR #5024)

Commits
-------

4a5dadb [Form] raise exception when label for choice cannot be read
a3cbf6b [Form] add type hint in ChoiceList
8053933 [Form] fix ChoiceList and ObjectChoiceList when choices is a Traversable
6f7ea8d [Form] fix ObjectChoiceList when property path is '0'
8da33eb [Form] fixed phpdoc of ChoiceList

Discussion
----------

	[Form] fixed ChoiceList

see commits

---------------------------------------------------------------------------

by bschussek at 2012-07-25T12:22:36Z

`iterator_to_array()` enforces an additional iteration of the traversable object. Instead of doing this in the constructor, we can just as well leave the array type hint in and let userland implement the conversion to an array.

If we want to save that additional iteration though, removing the type hint is the only option.

I don't really mind which of the two approaches we take. I just don't like a mix of them.

---------------------------------------------------------------------------

by Tobion at 2012-07-25T23:18:08Z

Ok finished by allowing Traversable in the topmost hierarchy. Other Traversables as choice item are treated as-is.
Also added phpdoc, fixed `extractLabels`, unified raised exceptions and added exception when label cannot be read that would otherwise create a php notice.

---------------------------------------------------------------------------

by bschussek at 2012-07-26T05:28:04Z

Great, looks good! 👍
This commit is contained in:
Fabien Potencier 2012-07-26 07:57:20 +02:00
commit c98f048022
4 changed files with 115 additions and 61 deletions

View File

@ -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.
*
* <code>
* $choices = array(true, false);
@ -66,17 +69,24 @@ class ChoiceList implements ChoiceListInterface
* Creates a new 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. 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. 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.
* should match the structure of $choices.
* @param array $preferredChoices A flat array of choices that should be
* presented to the user with priority.
* 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,22 +246,25 @@ 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 UnexpectedTypeException If the structure of the $labels array
* does not match the structure of the
* $choices array.
* @throws \InvalidArgumentException If the structures of the choices and labels array do not match.
* @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(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices)
{
// Add choices to the nested buckets
foreach ($choices as $group => $choice) {
if (!isset($labels[$group])) {
throw new \InvalidArgumentException('The structures of the choices and labels array do not match.');
}
if (is_array($choice)) {
// Don't do the work if the array is empty
if (count($choice) > 0) {
@ -281,14 +294,16 @@ class ChoiceList implements ChoiceListInterface
*
* @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.
* 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.
*/
protected function addChoiceGroup($group, &$bucketForPreferred, &$bucketForRemaining, array $choices, array $labels, array $preferredChoices)
protected function addChoiceGroup($group, array &$bucketForPreferred, array &$bucketForRemaining, array $choices, array $labels, array $preferredChoices)
{
// If this is a choice group, create a new level in the choice
// key hierarchy
@ -325,7 +340,7 @@ class ChoiceList implements ChoiceListInterface
*
* @throws InvalidConfigurationException If no valid value or index could be created.
*/
protected function addChoice(&$bucketForPreferred, &$bucketForRemaining, $choice, $label, array $preferredChoices)
protected function addChoice(array &$bucketForPreferred, array &$bucketForRemaining, $choice, $label, array $preferredChoices)
{
$index = $this->createIndex($choice);

View File

@ -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;
/**
@ -57,11 +56,12 @@ class ObjectChoiceList extends ChoiceList
/**
* Creates a new object choice list.
*
* @param array $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.
* @param array|\Traversable $choices The array of choices. 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.
* @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
@ -78,9 +78,9 @@ class ObjectChoiceList extends ChoiceList
*/
public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null)
{
$this->labelPath = $labelPath ? new PropertyPath($labelPath) : null;
$this->groupPath = $groupPath ? new PropertyPath($groupPath) : null;
$this->valuePath = $valuePath ? new PropertyPath($valuePath) : null;
$this->labelPath = null !== $labelPath ? new PropertyPath($labelPath) : null;
$this->groupPath = null !== $groupPath ? new PropertyPath($groupPath) : null;
$this->valuePath = null !== $valuePath ? new PropertyPath($valuePath) : null;
parent::__construct($choices, array(), $preferredChoices);
}
@ -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) {

View File

@ -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(
@ -35,13 +35,12 @@ class SimpleChoiceList extends ChoiceList
* Creates a new simple choice list.
*
* @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.
* the labels as values. Choices may also be given
* 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.
* presented to the user with priority.
*/
public function __construct(array $choices, array $preferredChoices = array())
{
@ -79,17 +78,15 @@ class SimpleChoiceList extends ChoiceList
* Takes care of splitting the single $choices array passed in the
* constructor into choices and labels.
*
* @param array $bucketForPreferred
* @param array $bucketForRemaining
* @param array $choices
* @param array $labels
* @param array $preferredChoices
*
* @throws UnexpectedTypeException
*
* @see parent::addChoices
* @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(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices)
{
// Add choices to the nested buckets
foreach ($choices as $choice => $label) {

View File

@ -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());
@ -141,4 +173,15 @@ class ChoiceListTest extends \PHPUnit_Framework_TestCase
$choices = array($this->obj2, $this->obj3, 'foobar');
$this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices));
}
/**
* @expectedException \InvalidArgumentException
*/
public function testNonMatchingLabels()
{
$this->list = new ChoiceList(
array($this->obj1, $this->obj2),
array('A')
);
}
}