merged branch bschussek/issue3156 (PR #3218)

Commits
-------

57cc531 [Form] Improved PHPDocs of choice lists
9e7e2af [Form] Fixed PHPDoc: Used {@inheritdoc} where applicable
2c530cc Fixed typos in UPGRADE file
7899bea Added examples to UPGRADE
d346ae6 Improved choice list sections of UPGRADE and CHANGELOG
a676598 [Form] Added class LazyChoiceList

Discussion
----------

[Form] Added LazyChoiceList

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: -
Todo: -

![Travis Build Status](https://secure.travis-ci.org/bschussek/symfony.png?branch=issue3156)

Adds a ChoiceList implementation that satisfies people who formerly extended ArrayChoiceList and loaded choices lazily in its `load` method.

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

by craue at 2012-01-30T12:56:49Z

👍

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

by craue at 2012-01-30T14:55:38Z

Not sure if it's a bug in this PR or in #3156, but the labels get replaced by their keys:

```php
<?php

use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;

class MyChoiceList extends LazyChoiceList {
	protected function loadChoiceList() {
		$choices = array(
			'bla' => 'blaaaaaahhhh',
		);
		return new SimpleChoiceList($choices, array(), ChoiceList::COPY_CHOICE, ChoiceList::COPY_CHOICE);
	}

	public function getChoices() {
		$choices = parent::getChoices();

		// $choices is array('bla' => 'bla')

		return $choices;
	}
}
```

If it's not this PR, I can of course open a new ticket for that. But I'm only working with `LazyChoiceList`s.

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

by stof at 2012-01-30T16:07:41Z

@craue the ``SimpleChoiceList`` is an implementation using the same string as label and value. If you need different ones, you need to use the ``ChoiceList`` implementation.

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

by craue at 2012-01-30T16:22:06Z

@stof: That would make `SimpleChoiceList` useless for almost any case. Are you sure?

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

by craue at 2012-01-30T16:33:31Z

The bug even occurs when using

```
return new ChoiceList(array_keys($choices), array_values($choices), array(), ChoiceList::COPY_CHOICE, ChoiceList::COPY_CHOICE);
```

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

by stof at 2012-01-30T16:40:19Z

well, the SimpleChoiceList is for simple cases (thus its naming) where you want the same for the label and the values. And if you look at the class, you will see it extends the ChoiceList implementation which is the generic one.

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

by craue at 2012-01-30T16:53:36Z

For me, "simple" would mean that it just takes the array given, using keys as indices and values as labels. No fancy stuff messing around with anything. :D But, is there anything wrong in this code or in mine? @bschussek: Please enlighten me.

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

by bschussek at 2012-01-30T17:16:58Z

You are both wrong :) `getChoices` does not return the choices with their associated labels anymore. What you get now are the choice indices as array keys and the choice values as array values. How both are determined depends on the index and value generation strategy, which, in your case, are both COPY_CHOICE.

The difference between simple and complex choice lists is, that simple choice lists can only contain scalar values as choices, while other choice lists (such as ObjectChoiceList, EntityChoiceList) may contain objects as choices.

Choice labels are now stored in ChoiceView objects, which are returned by the various `get*Views` methods.

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

by craue at 2012-01-30T18:07:43Z

It's pretty annoying having provided an array with keys and values when initializing the `ChoiceList` but being unable to retrieve it again. Guess I just over-used or even abused those choice lists as kind of (not only form related) lookup tables.

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

by bschussek at 2012-01-30T19:27:21Z

@craue: What's your use case?

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

by craue at 2012-01-30T20:10:16Z

I just used choice lists extensively, even for not directly form-related stuff. In one case, I'm using two of them (which are also used individually) to build up a third one. That went well using the old `ArrayChoiceList`s and their `getChoices` method. Just thinking about creating another set of model classes which just contain my lists. So for only one select field in a form it'll take three classes then: (A) a list, (B) a choice list based on A, (C) a choice form type based on B. Oh well ... :D

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

by craue at 2012-01-30T21:31:32Z

Anyway, this PR for `LazyChoiceList` is great, so please merge it. ;)

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

by craue at 2012-01-31T14:00:46Z

@bschussek: Is it ready to be merged?

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

by bschussek at 2012-01-31T16:59:17Z

Yes
This commit is contained in:
Fabien Potencier 2012-01-31 18:09:24 +01:00
commit 4682b096f6
7 changed files with 395 additions and 87 deletions

View File

@ -166,43 +166,30 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
* allowed setting different options for RepeatedType fields (like the label)
* added support for empty form name at root level, this enables rendering forms
without form name prefix in field names
* [BC BREAK] made form naming more restrictive. Form and field names must
start with a letter, digit or underscore and only contain letters, digits,
underscores, hyphens and colons
* [BC BREAK] form and field names must start with a letter, digit or underscore
and only contain letters, digits, underscores, hyphens and colons
* [BC BREAK] changed default name of the prototype in the "collection" type
from "$$name$$" to "__name__". No dollars are appended/prepended to custom
names anymore.
* [BC BREAK] greatly improved `ChoiceListInterface` and all of its
implementations. `EntityChoiceList` was adapted, the methods `getEntities()`,
`getEntitiesByKeys()`, `getIdentifier()` and `getIdentifierValues()` were
removed/made private. Instead of the first two you can use `getChoices()`
and `getChoicesByValues()`, for the latter two no replacement exists.
`ArrayChoiceList` was replaced by `SimpleChoiceList`.
`PaddedChoiceList`, `MonthChoiceList` and `TimezoneChoiceList` were removed.
Their functionality was merged into `DateType`, `TimeType` and `TimezoneType`.
* [BC BREAK] removed `EntitiesToArrayTransformer` and `EntityToIdTransformer`.
The former has been replaced by `CollectionToArrayTransformer` in combination
with `EntityChoiceList`, the latter is not required in the core anymore.
* [BC BREAK] improved ChoiceListInterface and all of its implementations
* [BC BREAK] removed EntitiesToArrayTransformer and EntityToIdTransformer.
The former has been replaced by CollectionToArrayTransformer in combination
with EntityChoiceList, the latter is not required in the core anymore.
* [BC BREAK] renamed
* `ArrayToBooleanChoicesTransformer` to `ChoicesToBooleanArrayTransformer`
* `ScalarToBooleanChoicesTransformer` to `ChoiceToBooleanArrayTransformer`
* `ArrayToChoicesTransformer` to `ChoicesToValuesTransformer`
* `ScalarToChoiceTransformer` to `ChoiceToValueTransformer`
* ArrayToBooleanChoicesTransformer to ChoicesToBooleanArrayTransformer
* ScalarToBooleanChoicesTransformer to ChoiceToBooleanArrayTransformer
* ArrayToChoicesTransformer to ChoicesToValuesTransformer
* ScalarToChoiceTransformer to ChoiceToValueTransformer
to be consistent with the naming in `ChoiceListInterface`.
to be consistent with the naming in ChoiceListInterface.
* [BC BREAK] removed `FormUtil::toArrayKey()` and `FormUtil::toArrayKeys()`.
They were merged into `ChoiceList` and have no public equivalent anymore.
* added `ComplexChoiceList` and `ObjectChoiceList`. Both let you select amongst
* [BC BREAK] removed FormUtil::toArrayKey() and FormUtil::toArrayKeys().
They were merged into ChoiceList and have no public equivalent anymore.
* added ComplexChoiceList and ObjectChoiceList. Both let you select amongst
objects in a choice field, but feature different constructors.
* choice fields now throw a `FormException` if neither the "choices" nor the
* choice fields now throw a FormException if neither the "choices" nor the
"choice_list" option is set
* the radio field is now a child type of the checkbox field

View File

@ -78,6 +78,60 @@ UPGRADE FROM 2.0 to 2.1
enable BC behaviour by setting the option "cascade_validation" to `true` on
the parent form.
* Changed implementation of choice lists
ArrayChoiceList was replaced. If you have custom classes that extend
this class, you can now extend SimpleChoiceList.
Before:
class MyChoiceList extends ArrayChoiceList
{
protected function load()
{
parent::load();
// load choices
$this->choices = $choices;
}
}
After:
class MyChoiceList extends SimpleChoiceList
{
public function __construct()
{
// load choices
parent::__construct($choices);
}
}
If you need to load the choices lazily - that is, as soon as they are
accessed for the first time - you can extend LazyChoiceList instead.
class MyChoiceList extends LazyChoiceList
{
protected function loadChoiceList()
{
// load choices
return new SimpleChoiceList($choices);
}
}
PaddedChoiceList, MonthChoiceList and TimezoneChoiceList were removed.
Their functionality was merged into DateType, TimeType and
TimezoneType.
EntityChoiceList was adapted. The methods `getEntities`,
`getEntitiesByKeys`, `getIdentifier` and `getIdentifierValues` were
removed/made private. Instead of the first two, you can now use
`getChoices` and `getChoicesByValues`. For the latter two, no
replacement exists.
* The strategy for generating the HTML attributes "id" and "name"
of choices in a choice field has changed
@ -102,8 +156,8 @@ UPGRADE FROM 2.0 to 2.1
* In the template of the choice type, the structure of the "choices" variable
has changed
"choices" now contains ChoiceView objects with two getters `getValue()`
and `getLabel()` to access the choice data. The indices of the array
"choices" now contains ChoiceView objects with two getters `getValue`
and `getLabel` to access the choice data. The indices of the array
store an index whose generation is controlled by the "index_generation"
option of the choice field.

View File

@ -17,7 +17,16 @@ use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
/**
* Base class for choice list implementations.
* 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.
*
* <code>
* $choices = array(true, false);
* $labels = array('Agree', 'Disagree');
* $choiceList = new ChoiceList($choices, $labels);
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.<com>
*/
@ -145,11 +154,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the list of choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getChoices()
{
@ -157,11 +162,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the values for the choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getValues()
{
@ -169,12 +170,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the choice views of the preferred choices as nested array with
* the choice groups as top-level keys.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getPreferredViews()
{
@ -182,12 +178,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the choice views of the choices that are not preferred as nested
* array with the choice groups as top-level keys.
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getRemainingViews()
{
@ -195,13 +186,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the choices corresponding to the given values.
*
* @param array $values
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
@ -232,13 +217,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the values corresponding to the given choices.
*
* @param array $choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
@ -269,13 +248,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the indices corresponding to the given choices.
*
* @param array $choices
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getIndicesForChoices(array $choices)
{
@ -299,13 +272,7 @@ class ChoiceList implements ChoiceListInterface
}
/**
* Returns the indices corresponding to the given values.
*
* @param array $values
*
* @return array
*
* @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface
* {@inheritdoc}
*/
public function getIndicesForValues(array $values)
{

View File

@ -0,0 +1,149 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Exception\FormException;
/**
* A choice list that is loaded lazily
*
* This list loads itself as soon as any of the getters is accessed for the
* first time. You should implement loadChoiceList() in your child classes,
* which should return a ChoiceListInterface instance.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class LazyChoiceList implements ChoiceListInterface
{
/**
* The loaded choice list
*
* @var ChoiceListInterface
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function getChoices()
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getChoices();
}
/**
* {@inheritdoc}
*/
public function getValues()
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getValues();
}
/**
* {@inheritdoc}
*/
public function getPreferredViews()
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getPreferredViews();
}
/**
* {@inheritdoc}
*/
public function getRemainingViews()
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getRemainingViews();
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getValuesForChoices($choices);
}
/**
* {@inheritdoc}
*/
public function getIndicesForChoices(array $choices)
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getIndicesForChoices($choices);
}
/**
* {@inheritdoc}
*/
public function getIndicesForValues(array $values)
{
if (!$this->choiceList) {
$this->load();
}
return $this->choiceList->getIndicesForValues($values);
}
/**
* Loads the choice list
*
* Should be implemented by child classes.
*
* @return ChoiceListInterface The loaded choice list
*/
abstract protected function loadChoiceList();
private function load()
{
$choiceList = $this->loadChoiceList();
if (!$choiceList instanceof ChoiceListInterface) {
throw new FormException('loadChoiceList() should return a ChoiceListInterface instance. Got ' . gettype($choiceList));
}
$this->choiceList = $choiceList;
}
}

View File

@ -17,11 +17,17 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\InvalidPropertyException;
/**
* A choice list that can store object choices.
* A choice list for object choices.
*
* Supports generation of choice labels, choice groups, choice values and
* choice indices by introspecting the properties of the object (or
* associated objects).
* choice indices by calling getters of the object (or associated objects).
*
* <code>
* $choices = array($user1, $user2);
*
* // call getName() to determine the choice labels
* $choiceList = new ObjectChoiceList($choices, 'name');
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/

View File

@ -15,10 +15,39 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* A choice list that can store any choices that are allowed as PHP array keys.
* A choice list for choices of type string or integer.
*
* The value strategy of simple choice lists is fixed to ChoiceList::COPY_CHOICE,
* since array keys are always valid choice values.
* 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.
*
* <code>
* $choiceList = new SimpleChoiceList(array(
* 'creditcard' => 'Credit card payment',
* 'cash' => 'Cash payment',
* ));
* </code>
*
* The default value generation strategy is `ChoiceList::COPY_CHOICE`, because
* choice values must be scalar, and the choices passed to this choice list
* are guaranteed to be scalar.
*
* The default index generation strategy is `ChoiceList::GENERATE`, so that
* your choices can also contain values that are illegal as indices. If your
* choices are guaranteed to start with a letter, digit or underscore and only
* contain letters, digits, underscores, hyphens and colons, you can set the
* strategy to `ChoiceList::COPY_CHOICE` instead.
*
* <code>
* $choices = array(
* 'creditcard' => 'Credit card payment',
* 'cash' => 'Cash payment',
* );
*
* // value generation: COPY_CHOICE (the default)
* // index generation: COPY_CHOICE (custom)
* $choiceList = new SimpleChoiceList($choices, array(), ChoiceList::COPY_CHOICE, ChoiceList::COPY_CHOICE);
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/

View File

@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
class LazyChoiceListTest extends \PHPUnit_Framework_TestCase
{
private $list;
protected function setUp()
{
parent::setUp();
$this->list = new LazyChoiceListTest_Impl(new SimpleChoiceList(array(
'a' => 'A',
'b' => 'B',
'c' => 'C',
), array('b')));
}
protected function tearDown()
{
parent::tearDown();
$this->list = null;
}
public function testGetChoices()
{
$this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices());
}
public function testGetValues()
{
$this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getValues());
}
public function testGetPreferredViews()
{
$this->assertEquals(array(1 => new ChoiceView('b', 'B')), $this->list->getPreferredViews());
}
public function testGetRemainingViews()
{
$this->assertEquals(array(0 => new ChoiceView('a', 'A'), 2 => new ChoiceView('c', 'C')), $this->list->getRemainingViews());
}
public function testGetIndicesForChoices()
{
$choices = array('b', 'c');
$this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
}
public function testGetIndicesForValues()
{
$values = array('b', 'c');
$this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
}
public function testGetChoicesForValues()
{
$values = array('b', 'c');
$this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values));
}
public function testGetValuesForChoices()
{
$choices = array('b', 'c');
$this->assertSame(array('b', 'c'), $this->list->getValuesForChoices($choices));
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
*/
public function testLoadChoiceListShouldReturnChoiceList()
{
$list = new LazyChoiceListTest_InvalidImpl();
$list->getChoices();
}
}
class LazyChoiceListTest_Impl extends LazyChoiceList
{
private $choiceList;
public function __construct($choiceList)
{
$this->choiceList = $choiceList;
}
protected function loadChoiceList()
{
return $this->choiceList;
}
}
class LazyChoiceListTest_InvalidImpl extends LazyChoiceList
{
protected function loadChoiceList()
{
return new \stdClass();
}
}