diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index f51c9827ea..b74f2b228a 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -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 diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 9b79eb32ae..120e66e933 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -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. diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index c8cd44ccd8..c9d09a4904 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -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. + * + * + * $choices = array(true, false); + * $labels = array('Agree', 'Disagree'); + * $choiceList = new ChoiceList($choices, $labels); + * * * @author Bernhard Schussek */ @@ -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) { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php new file mode 100644 index 0000000000..29551de560 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php @@ -0,0 +1,149 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index 3533326be1..daf13efcd3 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -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). + * + * + * $choices = array($user1, $user2); + * + * // call getName() to determine the choice labels + * $choiceList = new ObjectChoiceList($choices, 'name'); + * * * @author Bernhard Schussek */ diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 748f8397c0..27a7382c03 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -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. + * + * + * $choiceList = new SimpleChoiceList(array( + * 'creditcard' => 'Credit card payment', + * 'cash' => 'Cash payment', + * )); + * + * + * 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. + * + * + * $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); + * * * @author Bernhard Schussek */ diff --git a/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/LazyChoiceListTest.php b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/LazyChoiceListTest.php new file mode 100644 index 0000000000..6e6bb927dc --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Extension/Core/ChoiceList/LazyChoiceListTest.php @@ -0,0 +1,116 @@ + + * + * 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(); + } +} \ No newline at end of file