From 7623dc87e858d2d4b0cee0213386505924eb5f75 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 22 Jun 2015 14:14:00 +0200 Subject: [PATCH] [Form] Fixed handling of choices passed in choice groups --- .../Form/ChoiceList/ArrayChoiceList.php | 144 +++++++++++----- .../Form/ChoiceList/ArrayKeyChoiceList.php | 56 +++++- .../Form/ChoiceList/ChoiceListInterface.php | 64 +++++-- .../Factory/CachingFactoryDecorator.php | 29 +++- .../Factory/ChoiceListFactoryInterface.php | 27 ++- .../Factory/DefaultChoiceListFactory.php | 163 ++++-------------- .../Factory/PropertyAccessDecorator.php | 12 +- .../Form/ChoiceList/LazyChoiceList.php | 34 +++- .../ChoiceList/LegacyChoiceListAdapter.php | 144 ++++++++++++++++ .../Core/ChoiceList/ChoiceListInterface.php | 52 +++++- .../Form/Extension/Core/Type/ChoiceType.php | 8 +- .../ChoiceList/AbstractChoiceListTest.php | 49 +++++- .../Tests/ChoiceList/ArrayChoiceListTest.php | 24 ++- .../ChoiceList/ArrayKeyChoiceListTest.php | 53 ++---- .../Factory/CachingFactoryDecoratorTest.php | 8 +- .../Factory/DefaultChoiceListFactoryTest.php | 127 +++++++------- .../Tests/ChoiceList/LazyChoiceListTest.php | 30 ++++ .../LegacyChoiceListAdapterTest.php | 110 ++++++++++++ .../FixRadioInputListenerTest.php | 11 +- .../Extension/Core/Type/ChoiceTypeTest.php | 26 +++ 20 files changed, 818 insertions(+), 353 deletions(-) create mode 100644 src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/LegacyChoiceListAdapterTest.php diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index f55154b085..9996966f19 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -30,14 +30,21 @@ class ArrayChoiceList implements ChoiceListInterface * * @var array */ - protected $choices = array(); + protected $choices; /** - * The values of the choices. + * The values indexed by the original keys. * - * @var string[] + * @var array */ - protected $values = array(); + protected $structuredValues; + + /** + * The original keys of the choices array. + * + * @var int[]|string[] + */ + protected $originalKeys; /** * The callback for creating the value for a choice. @@ -51,31 +58,41 @@ class ArrayChoiceList implements ChoiceListInterface * * The given choice array must have the same array keys as the value array. * - * @param array $choices The selectable choices - * @param callable|null $value The callable for creating the value for a - * choice. If `null` is passed, incrementing - * integers are used as values + * @param array|\Traversable $choices The selectable choices + * @param callable|null $value The callable for creating the value + * for a choice. If `null` is passed, + * incrementing integers are used as + * values */ - public function __construct(array $choices, $value = null) + public function __construct($choices, $value = null) { if (null !== $value && !is_callable($value)) { throw new UnexpectedTypeException($value, 'null or callable'); } - $this->choices = $choices; - $this->values = array(); - $this->valueCallback = $value; - - if (null === $value) { - $i = 0; - foreach ($this->choices as $key => $choice) { - $this->values[$key] = (string) $i++; - } - } else { - foreach ($choices as $key => $choice) { - $this->values[$key] = (string) call_user_func($value, $choice); - } + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); } + + if (null !== $value) { + // If a deterministic value generator was passed, use it later + $this->valueCallback = $value; + } else { + // Otherwise simply generate incrementing integers as values + $i = 0; + $value = function () use (&$i) { + return $i++; + }; + } + + // If the choices are given as recursive array (i.e. with explicit + // choice groups), flatten the array. The grouping information is needed + // in the view only. + $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues); + + $this->choices = $choicesByValues; + $this->originalKeys = $keysByValues; + $this->structuredValues = $structuredValues; } /** @@ -91,7 +108,23 @@ class ArrayChoiceList implements ChoiceListInterface */ public function getValues() { - return $this->values; + return array_map('strval', array_keys($this->choices)); + } + + /** + * {@inheritdoc} + */ + public function getStructuredValues() + { + return $this->structuredValues; + } + + /** + * {@inheritdoc} + */ + public function getOriginalKeys() + { + return $this->originalKeys; } /** @@ -102,17 +135,8 @@ class ArrayChoiceList implements ChoiceListInterface $choices = array(); foreach ($values as $i => $givenValue) { - foreach ($this->values as $j => $value) { - if ($value !== (string) $givenValue) { - continue; - } - - $choices[$i] = $this->choices[$j]; - unset($values[$i]); - - if (0 === count($values)) { - break 2; - } + if (isset($this->choices[$givenValue])) { + $choices[$i] = $this->choices[$givenValue]; } } @@ -131,28 +155,56 @@ class ArrayChoiceList implements ChoiceListInterface $givenValues = array(); foreach ($choices as $i => $givenChoice) { - $givenValues[$i] = (string) call_user_func($this->valueCallback, $givenChoice); + $givenValues[$i] = call_user_func($this->valueCallback, $givenChoice); } - return array_intersect($givenValues, $this->values); + return array_intersect($givenValues, array_keys($this->choices)); } // Otherwise compare choices by identity foreach ($choices as $i => $givenChoice) { - foreach ($this->choices as $j => $choice) { - if ($choice !== $givenChoice) { - continue; - } - - $values[$i] = $this->values[$j]; - unset($choices[$i]); - - if (0 === count($choices)) { - break 2; + foreach ($this->choices as $value => $choice) { + if ($choice === $givenChoice) { + $values[$i] = (string) $value; + break; } } } return $values; } + + /** + * Flattens an array into the given output variables. + * + * @param array $choices The array to flatten + * @param callable $value The callable for generating choice values + * @param array $choicesByValues The flattened choices indexed by the + * corresponding values + * @param array $keysByValues The original keys indexed by the + * corresponding values + * + * @internal Must not be used by user-land code + */ + protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues) + { + if (null === $choicesByValues) { + $choicesByValues = array(); + $keysByValues = array(); + $structuredValues = array(); + } + + foreach ($choices as $key => $choice) { + if (is_array($choice)) { + $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]); + + continue; + } + + $choiceValue = (string) call_user_func($value, $choice); + $choicesByValues[$choiceValue] = $choice; + $keysByValues[$choiceValue] = $key; + $structuredValues[$key] = $choiceValue; + } + } } diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index 30709108e8..7c3c107d0f 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -62,6 +62,8 @@ class ArrayKeyChoiceList extends ArrayChoiceList * @return int|string The choice as PHP array key * * @throws InvalidArgumentException If the choice is not scalar + * + * @internal Must not be used outside this class */ public static function toArrayKey($choice) { @@ -89,23 +91,27 @@ class ArrayKeyChoiceList extends ArrayChoiceList * If no values are given, the choices are cast to strings and used as * values. * - * @param array $choices The selectable choices - * @param callable $value The callable for creating the value for a - * choice. If `null` is passed, the choices are - * cast to strings and used as values + * @param array|\Traversable $choices The selectable choices + * @param callable $value The callable for creating the value + * for a choice. If `null` is passed, the + * choices are cast to strings and used + * as values * * @throws InvalidArgumentException If the keys of the choices don't match * the keys of the values or if any of the * choices is not scalar */ - public function __construct(array $choices, $value = null) + public function __construct($choices, $value = null) { - $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); - + // If no values are given, use the choices as values + // Since the choices are stored in the collection keys, i.e. they are + // strings or integers, we are guaranteed to be able to convert them + // to strings if (null === $value) { $value = function ($choice) { return (string) $choice; }; + $this->useChoicesAsValues = true; } @@ -122,7 +128,7 @@ class ArrayKeyChoiceList extends ArrayChoiceList // If the values are identical to the choices, so we can just return // them to improve performance a little bit - return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values)); + return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, array_keys($this->choices))); } return parent::getChoicesForValues($values); @@ -143,4 +149,38 @@ class ArrayKeyChoiceList extends ArrayChoiceList return parent::getValuesForChoices($choices); } + + /** + * Flattens and flips an array into the given output variable. + * + * @param array $choices The array to flatten + * @param callable $value The callable for generating choice values + * @param array $choicesByValues The flattened choices indexed by the + * corresponding values + * @param array $keysByValues The original keys indexed by the + * corresponding values + * + * @internal Must not be used by user-land code + */ + protected function flatten(array $choices, $value, &$choicesByValues, &$keysByValues, &$structuredValues) + { + if (null === $choicesByValues) { + $choicesByValues = array(); + $keysByValues = array(); + $structuredValues = array(); + } + + foreach ($choices as $choice => $key) { + if (is_array($key)) { + $this->flatten($key, $value, $choicesByValues, $keysByValues, $structuredValues[$choice]); + + continue; + } + + $choiceValue = (string) call_user_func($value, $choice); + $choicesByValues[$choiceValue] = $choice; + $keysByValues[$choiceValue] = $key; + $structuredValues[$key] = $choiceValue; + } + } } diff --git a/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php index 62f3158646..b59e77bf79 100644 --- a/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php @@ -14,16 +14,13 @@ namespace Symfony\Component\Form\ChoiceList; /** * A list of choices that can be selected in a choice field. * - * A choice list assigns string values to each of a list of choices. These - * string values are displayed in the "value" attributes in HTML and submitted - * back to the server. + * A choice list assigns unique string values to each of a list of choices. + * These string values are displayed in the "value" attributes in HTML and + * submitted back to the server. * * The acceptable data types for the choices depend on the implementation. * Values must always be strings and (within the list) free of duplicates. * - * The choices returned by {@link getChoices()} and the values returned by - * {@link getValues()} must have the same array indices. - * * @author Bernhard Schussek */ interface ChoiceListInterface @@ -31,23 +28,66 @@ interface ChoiceListInterface /** * Returns all selectable choices. * - * The keys of the choices correspond to the keys of the values returned by - * {@link getValues()}. - * - * @return array The selectable choices + * @return array The selectable choices indexed by the corresponding values */ public function getChoices(); /** * Returns the values for the choices. * - * The keys of the values correspond to the keys of the choices returned by - * {@link getChoices()}. + * The values are strings that do not contain duplicates. * * @return string[] The choice values */ public function getValues(); + /** + * Returns the values in the structure originally passed to the list. + * + * Contrary to {@link getValues()}, the result is indexed by the original + * keys of the choices. If the original array contained nested arrays, these + * nested arrays are represented here as well: + * + * $form->add('field', 'choice', array( + * 'choices' => array( + * 'Decided' => array('Yes' => true, 'No' => false), + * 'Undecided' => array('Maybe' => null), + * ), + * )); + * + * In this example, the result of this method is: + * + * array( + * 'Decided' => array('Yes' => '0', 'No' => '1'), + * 'Undecided' => array('Maybe' => '2'), + * ) + * + * @return string[] The choice values + */ + public function getStructuredValues(); + + /** + * Returns the original keys of the choices. + * + * The original keys are the keys of the choice array that was passed in the + * "choice" option of the choice type. Note that this array may contain + * duplicates if the "choice" option contained choice groups: + * + * $form->add('field', 'choice', array( + * 'choices' => array( + * 'Decided' => array(true, false), + * 'Undecided' => array(null), + * ), + * )); + * + * In this example, the original key 0 appears twice, once for `true` and + * once for `null`. + * + * @return int[]|string[] The original choice keys indexed by the + * corresponding choice values + */ + public function getOriginalKeys(); + /** * Returns the choices corresponding to the given values. * diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php index f9848c2d0e..8796acfc4d 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -65,6 +65,30 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface return hash('sha256', $namespace.':'.json_encode($value)); } + /** + * Flattens an array into the given output variable. + * + * @param array $array The array to flatten + * @param array $output The flattened output + * + * @internal Should not be used by user-land code + */ + private static function flatten(array $array, &$output) + { + if (null === $output) { + $output = array(); + } + + foreach ($array as $key => $value) { + if (is_array($value)) { + self::flatten($value, $output); + continue; + } + + $output[$key] = $value; + } + } + /** * Decorates the given factory. * @@ -100,7 +124,7 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface // We ignore the choice groups for caching. If two choice lists are // requested with the same choices, but a different grouping, the same // choice list is returned. - DefaultChoiceListFactory::flatten($choices, $flatChoices); + self::flatten($choices, $flatChoices); $hash = self::generateHash(array($flatChoices, $value), 'fromChoices'); @@ -129,7 +153,7 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface // We ignore the choice groups for caching. If two choice lists are // requested with the same choices, but a different grouping, the same // choice list is returned. - DefaultChoiceListFactory::flattenFlipped($choices, $flatChoices); + self::flatten($choices, $flatChoices); $hash = self::generateHash(array($flatChoices, $value), 'fromFlippedChoices'); @@ -161,7 +185,6 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface { // The input is not validated on purpose. This way, the decorated // factory may decide which input to accept and which not. - $hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr)); if (!isset($this->views[$hash])) { diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php index 60239423f3..7933dd91d4 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php @@ -69,7 +69,7 @@ interface ChoiceListFactoryInterface * argument. * * @param ChoiceLoaderInterface $loader The choice loader - * @param null|callable $value The callable generating the choice + * @param null|callable $value The callable generating the choice * values * * @return ChoiceListInterface The choice list @@ -98,25 +98,20 @@ interface ChoiceListFactoryInterface * The preferred choices can also be passed as array. Each choice that is * contained in that array will be marked as preferred. * - * The groups can be passed as a multi-dimensional array. In that case, a - * group will be created for each array entry containing a nested array. - * For all other entries, the choice for the corresponding key will be - * inserted at that position. - * * The attributes can be passed as multi-dimensional array. The keys should * match the keys of the choices. The values should be arrays of HTML * attributes that should be added to the respective choice. * - * @param ChoiceListInterface $list The choice list - * @param null|array|callable $preferredChoices The preferred choices - * @param null|callable $label The callable generating - * the choice labels - * @param null|callable $index The callable generating - * the view indices - * @param null|array|\Traversable|callable $groupBy The callable generating - * the group names - * @param null|array|callable $attr The callable generating - * the HTML attributes + * @param ChoiceListInterface $list The choice list + * @param null|array|callable $preferredChoices The preferred choices + * @param null|callable $label The callable generating the + * choice labels + * @param null|callable $index The callable generating the + * view indices + * @param null|callable $groupBy The callable generating the + * group names + * @param null|array|callable $attr The callable generating the + * HTML attributes * * @return ChoiceListView The choice list view */ diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index b7f37137d8..cf88c6f581 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -15,11 +15,11 @@ use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Symfony\Component\Form\ChoiceList\LegacyChoiceListAdapter; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface; use Symfony\Component\Form\Extension\Core\View\ChoiceView as LegacyChoiceView; /** @@ -29,72 +29,12 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView as LegacyChoiceView; */ class DefaultChoiceListFactory implements ChoiceListFactoryInterface { - /** - * Flattens an array into the given output variable. - * - * @param array $array The array to flatten - * @param array $output The flattened output - * - * @internal Should not be used by user-land code - */ - public static function flatten(array $array, &$output) - { - if (null === $output) { - $output = array(); - } - - foreach ($array as $key => $value) { - if (is_array($value)) { - self::flatten($value, $output); - continue; - } - - $output[$key] = $value; - } - } - - /** - * Flattens and flips an array into the given output variable. - * - * During the flattening, the keys and values of the input array are - * flipped. - * - * @param array $array The array to flatten - * @param array $output The flattened output - * - * @internal Should not be used by user-land code - */ - public static function flattenFlipped(array $array, &$output) - { - if (null === $output) { - $output = array(); - } - - foreach ($array as $key => $value) { - if (is_array($value)) { - self::flattenFlipped($value, $output); - continue; - } - - $output[$value] = $key; - } - } - /** * {@inheritdoc} */ public function createListFromChoices($choices, $value = null) { - if ($choices instanceof \Traversable) { - $choices = iterator_to_array($choices); - } - - // If the choices are given as recursive array (i.e. with explicit - // choice groups), flatten the array. The grouping information is needed - // in the view only. - self::flatten($choices, $flatChoices); - - return new ArrayChoiceList($flatChoices, $value); + return new ArrayChoiceList($choices, $value); } /** @@ -105,26 +45,7 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ public function createListFromFlippedChoices($choices, $value = null) { - if ($choices instanceof \Traversable) { - $choices = iterator_to_array($choices); - } - - // If the choices are given as recursive array (i.e. with explicit - // choice groups), flatten the array. The grouping information is needed - // in the view only. - self::flattenFlipped($choices, $flatChoices); - - // If no values are given, use the choices as values - // Since the choices are stored in the collection keys, i.e. they are - // strings or integers, we are guaranteed to be able to convert them - // to strings - if (null === $value) { - $value = function ($choice) { - return (string) $choice; - }; - } - - return new ArrayKeyChoiceList($flatChoices, $value); + return new ArrayKeyChoiceList($choices, $value); } /** @@ -141,22 +62,24 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) { // Backwards compatibility - if ($list instanceof LegacyChoiceListInterface && empty($preferredChoices) + if ($list instanceof LegacyChoiceListAdapter && empty($preferredChoices) && null === $label && null === $index && null === $groupBy && null === $attr) { $mapToNonLegacyChoiceView = function (LegacyChoiceView $choiceView) { return new ChoiceView($choiceView->data, $choiceView->value, $choiceView->label); }; + $adaptedList = $list->getAdaptedList(); + return new ChoiceListView( - array_map($mapToNonLegacyChoiceView, $list->getRemainingViews()), - array_map($mapToNonLegacyChoiceView, $list->getPreferredViews()) + array_map($mapToNonLegacyChoiceView, $adaptedList->getRemainingViews()), + array_map($mapToNonLegacyChoiceView, $adaptedList->getPreferredViews()) ); } $preferredViews = array(); $otherViews = array(); $choices = $list->getChoices(); - $values = $list->getValues(); + $keys = $list->getOriginalKeys(); if (!is_callable($preferredChoices) && !empty($preferredChoices)) { $preferredChoices = function ($choice) use ($preferredChoices) { @@ -169,36 +92,17 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface $index = 0; } - // If $groupBy is not given, no grouping is done - if (empty($groupBy)) { - foreach ($choices as $key => $choice) { - self::addChoiceView( - $choice, - $key, - $label, - $values, - $index, - $attr, - $preferredChoices, - $preferredViews, - $otherViews - ); - } - - return new ChoiceListView($otherViews, $preferredViews); - } - // If $groupBy is a callable, choices are added to the group with the // name returned by the callable. If the callable returns null, the // choice is not added to any group if (is_callable($groupBy)) { - foreach ($choices as $key => $choice) { + foreach ($choices as $value => $choice) { self::addChoiceViewGroupedBy( $groupBy, $choice, - $key, + (string) $value, $label, - $values, + $keys, $index, $attr, $preferredChoices, @@ -207,13 +111,12 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface ); } } else { - // If $groupBy is passed as array, use that array as template for - // constructing the groups + // Otherwise use the original structure of the choices self::addChoiceViewsGroupedBy( - $groupBy, + $list->getStructuredValues(), $label, $choices, - $values, + $keys, $index, $attr, $preferredChoices, @@ -239,15 +142,17 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface return new ChoiceListView($otherViews, $preferredViews); } - private static function addChoiceView($choice, $key, $label, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceView($choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { - $value = $values[$key]; + // $value may be an integer or a string, since it's stored in the array + // keys. We want to guarantee it's a string though. + $key = $keys[$value]; $nextIndex = is_int($index) ? $index++ : call_user_func($index, $choice, $key, $value); $view = new ChoiceView( $choice, $value, - // If the labels are null, use the choice key by default + // If the labels are null, use the original choice key by default null === $label ? (string) $key : (string) call_user_func($label, $choice, $key, $value), // The attributes may be a callable or a mapping from choice indices // to nested arrays @@ -262,19 +167,19 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface } } - private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { - foreach ($groupBy as $key => $content) { + foreach ($groupBy as $key => $value) { // Add the contents of groups to new ChoiceGroupView instances - if (is_array($content)) { + if (is_array($value)) { $preferredViewsForGroup = array(); $otherViewsForGroup = array(); self::addChoiceViewsGroupedBy( - $content, + $value, $label, $choices, - $values, + $keys, $index, $attr, $isPreferred, @@ -295,10 +200,10 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface // Add ungrouped items directly self::addChoiceView( - $choices[$key], - $key, + $choices[$value], + $value, $label, - $values, + $keys, $index, $attr, $isPreferred, @@ -308,17 +213,17 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface } } - private static function addChoiceViewGroupedBy($groupBy, $choice, $key, $label, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { - $groupLabel = call_user_func($groupBy, $choice, $key, $values[$key]); + $groupLabel = call_user_func($groupBy, $choice, $keys[$value], $value); if (null === $groupLabel) { // If the callable returns null, don't group the choice self::addChoiceView( $choice, - $key, + $value, $label, - $values, + $keys, $index, $attr, $isPreferred, @@ -338,9 +243,9 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface self::addChoiceView( $choice, - $key, + $value, $label, - $values, + $keys, $index, $attr, $isPreferred, diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index 0f4bcaa14e..e86772fddc 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -54,8 +54,8 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface /** * Decorates the given factory. * - * @param ChoiceListFactoryInterface $decoratedFactory The decorated factory - * @param null|PropertyAccessorInterface $propertyAccessor The used property accessor + * @param ChoiceListFactoryInterface $decoratedFactory The decorated factory + * @param null|PropertyAccessorInterface $propertyAccessor The used property accessor */ public function __construct(ChoiceListFactoryInterface $decoratedFactory, PropertyAccessorInterface $propertyAccessor = null) { @@ -98,8 +98,6 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface if (is_object($choice) || is_array($choice)) { return $accessor->getValue($choice, $value); } - - return; }; } @@ -128,9 +126,9 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface /** * {@inheritdoc} * - * @param ChoiceLoaderInterface $loader The choice loader - * @param null|callable|string|PropertyPath $value The callable or path for - * generating the choice values + * @param ChoiceLoaderInterface $loader The choice loader + * @param null|callable|string|PropertyPath $value The callable or path for + * generating the choice values * * @return ChoiceListInterface The choice list */ diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php index 092e2c4644..f691d71330 100644 --- a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -43,13 +43,6 @@ class LazyChoiceList implements ChoiceListInterface */ private $value; - /** - * Whether to use the value callback to compare choices. - * - * @var bool - */ - private $compareByValue; - /** * @var ChoiceListInterface|null */ @@ -66,11 +59,10 @@ class LazyChoiceList implements ChoiceListInterface * @param null|callable $value The callable generating the choice * values */ - public function __construct(ChoiceLoaderInterface $loader, $value = null, $compareByValue = false) + public function __construct(ChoiceLoaderInterface $loader, $value = null) { $this->loader = $loader; $this->value = $value; - $this->compareByValue = $compareByValue; } /** @@ -97,6 +89,30 @@ class LazyChoiceList implements ChoiceListInterface return $this->loadedList->getValues(); } + /** + * {@inheritdoc} + */ + public function getStructuredValues() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getStructuredValues(); + } + + /** + * {@inheritdoc} + */ + public function getOriginalKeys() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getOriginalKeys(); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php b/src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php new file mode 100644 index 0000000000..929ef8c290 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/LegacyChoiceListAdapter.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface; + +/** + * Adapts a legacy choice list implementation to {@link ChoiceListInterface}. + * + * @author Bernhard Schussek + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ +class LegacyChoiceListAdapter implements ChoiceListInterface +{ + /** + * @var LegacyChoiceListInterface + */ + private $adaptedList; + + /** + * @var array|null + */ + private $choices; + + /** + * @var array|null + */ + private $values; + + /** + * @var array|null + */ + private $structuredValues; + + /** + * Adapts a legacy choice list to {@link ChoiceListInterface}. + * + * @param LegacyChoiceListInterface $adaptedList The adapted list + */ + public function __construct(LegacyChoiceListInterface $adaptedList) + { + $this->adaptedList = $adaptedList; + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + if (!$this->choices) { + $this->initialize(); + } + + return $this->choices; + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + if (!$this->values) { + $this->initialize(); + } + + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function getStructuredValues() + { + if (!$this->structuredValues) { + $this->initialize(); + } + + return $this->structuredValues; + } + + /** + * {@inheritdoc} + */ + public function getOriginalKeys() + { + if (!$this->structuredValues) { + $this->initialize(); + } + + return array_flip($this->structuredValues); + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + return $this->adaptedList->getChoicesForValues($values); + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + return $this->adaptedList->getValuesForChoices($choices); + } + + /** + * Returns the adapted choice list. + * + * @return LegacyChoiceListInterface The adapted list + */ + public function getAdaptedList() + { + return $this->adaptedList; + } + + private function initialize() + { + $this->choices = array(); + $this->values = array(); + $this->structuredValues = $this->adaptedList->getValues(); + + $innerChoices = $this->adaptedList->getChoices(); + + foreach ($innerChoices as $index => $choice) { + $value = $this->structuredValues[$index]; + $this->values[] = $value; + $this->choices[$value] = $choice; + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index ac5847ed32..f7f8acdfea 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -11,9 +11,6 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; -use Symfony\Component\Form\ChoiceList\ChoiceListInterface as BaseChoiceListInterface; -use Symfony\Component\Form\FormConfigBuilder; - /** * Contains choices that can be selected in a form field. * @@ -30,10 +27,24 @@ use Symfony\Component\Form\FormConfigBuilder; * @author Bernhard Schussek * * @deprecated since version 2.7, to be removed in 3.0. - * Use {@link BaseChoiceListInterface} instead. + * Use {@link \Symfony\Component\Form\ChoiceList\ChoiceListInterface} instead. */ -interface ChoiceListInterface extends BaseChoiceListInterface +interface ChoiceListInterface { + /** + * Returns the list of choices. + * + * @return array The choices with their indices as keys + */ + public function getChoices(); + + /** + * Returns the values for the choices. + * + * @return array The values with the corresponding choice indices as keys + */ + public function getValues(); + /** * Returns the choice views of the preferred choices as nested array with * the choice groups as top-level keys. @@ -84,6 +95,37 @@ interface ChoiceListInterface extends BaseChoiceListInterface */ public function getRemainingViews(); + /** + * Returns the choices corresponding to the given values. + * + * The choices can have any data type. + * + * The choices must be returned with the same keys and in the same order + * as the corresponding values in the given array. + * + * @param array $values An array of choice values. Not existing values in + * this array are ignored + * + * @return array An array of choices with ascending, 0-based numeric keys + */ + public function getChoicesForValues(array $values); + + /** + * Returns the values corresponding to the given choices. + * + * The values must be strings. + * + * The values must be returned with the same keys and in the same order + * as the corresponding choices in the given array. + * + * @param array $choices An array of choices. Not existing choices in this + * array are ignored + * + * @return array An array of choice values with ascending, 0-based numeric + * keys + */ + public function getValuesForChoices(array $choices); + /** * Returns the indices corresponding to the given choices. * diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index d9791b4739..386f27dbc9 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\ChoiceList\LegacyChoiceListAdapter; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; @@ -27,6 +28,7 @@ use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface; use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; @@ -259,6 +261,10 @@ class ChoiceType extends AbstractType if ($choiceList) { @trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED); + if ($choiceList instanceof LegacyChoiceListInterface) { + return new LegacyChoiceListAdapter($choiceList); + } + return $choiceList; } @@ -338,7 +344,7 @@ class ChoiceType extends AbstractType $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); - $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\ChoiceList\ChoiceListInterface')); + $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\ChoiceList\ChoiceListInterface', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface')); $resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable')); $resolver->setAllowedTypes('choice_translation_domain', array('null', 'bool', 'string')); $resolver->setAllowedTypes('choices_as_values', 'bool'); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php index 0805238f7f..ca244ebd69 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php @@ -31,6 +31,16 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase */ protected $values; + /** + * @var array + */ + protected $structuredValues; + + /** + * @var array + */ + protected $keys; + /** * @var mixed */ @@ -71,25 +81,52 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase */ protected $value4; + /** + * @var string + */ + protected $key1; + + /** + * @var string + */ + protected $key2; + + /** + * @var string + */ + protected $key3; + + /** + * @var string + */ + protected $key4; + protected function setUp() { parent::setUp(); $this->list = $this->createChoiceList(); - $this->choices = $this->getChoices(); + $choices = $this->getChoices(); + $this->values = $this->getValues(); + $this->structuredValues = array_combine(array_keys($choices), $this->values); + $this->choices = array_combine($this->values, $choices); + $this->keys = array_combine($this->values, array_keys($choices)); // allow access to the individual entries without relying on their indices reset($this->choices); reset($this->values); + reset($this->keys); for ($i = 1; $i <= 4; ++$i) { $this->{'choice'.$i} = current($this->choices); $this->{'value'.$i} = current($this->values); + $this->{'key'.$i} = current($this->keys); next($this->choices); next($this->values); + next($this->keys); } } @@ -103,6 +140,16 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame($this->values, $this->list->getValues()); } + public function testGetStructuredValues() + { + $this->assertSame($this->values, $this->list->getStructuredValues()); + } + + public function testGetOriginalKeys() + { + $this->assertSame($this->keys, $this->list->getOriginalKeys()); + } + public function testGetChoicesForValues() { $values = array($this->value1, $this->value2); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php index 129a093b89..50d4df8a9b 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -29,8 +29,6 @@ class ArrayChoiceListTest extends AbstractChoiceListTest protected function createChoiceList() { - $i = 0; - return new ArrayChoiceList($this->getChoices()); } @@ -60,11 +58,31 @@ class ArrayChoiceListTest extends AbstractChoiceListTest $choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); - $this->assertSame(array(2 => ':foo', 7 => ':bar', 10 => ':baz'), $choiceList->getValues()); + $this->assertSame(array(':foo', ':bar', ':baz'), $choiceList->getValues()); + $this->assertSame(array(':foo' => 'foo', ':bar' => 'bar', ':baz' => 'baz'), $choiceList->getChoices()); + $this->assertSame(array(':foo' => 2, ':bar' => 7, ':baz' => 10), $choiceList->getOriginalKeys()); $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => ':foo', 2 => ':baz'))); $this->assertSame(array(1 => ':foo', 2 => ':baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); } + public function testCreateChoiceListWithGroupedChoices() + { + $choiceList = new ArrayChoiceList(array( + 'Group 1' => array('A' => 'a', 'B' => 'b'), + 'Group 2' => array('C' => 'c', 'D' => 'd'), + )); + + $this->assertSame(array('0', '1', '2', '3'), $choiceList->getValues()); + $this->assertSame(array( + 'Group 1' => array('A' => '0', 'B' => '1'), + 'Group 2' => array('C' => '2', 'D' => '3'), + ), $choiceList->getStructuredValues()); + $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $choiceList->getChoices()); + $this->assertSame(array(0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D'), $choiceList->getOriginalKeys()); + $this->assertSame(array(1 => 'a', 2 => 'b'), $choiceList->getChoicesForValues(array(1 => '0', 2 => '1'))); + $this->assertSame(array(1 => '0', 2 => '1'), $choiceList->getValuesForChoices(array(1 => 'a', 2 => 'b'))); + } + public function testCompareChoicesByIdentityByDefault() { $callback = function ($choice) { diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php index 5024a60db7..5cbadf6e0f 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -29,7 +29,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest protected function createChoiceList() { - return new ArrayKeyChoiceList($this->getChoices()); + return new ArrayKeyChoiceList(array_flip($this->getChoices())); } protected function getChoices() @@ -44,9 +44,11 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest public function testUseChoicesAsValuesByDefault() { - $list = new ArrayKeyChoiceList(array(1 => '', 3 => 0, 7 => '1', 10 => 1.23)); + $list = new ArrayKeyChoiceList(array('' => 'Empty', 0 => 'Zero', 1 => 'One', '1.23' => 'Float')); - $this->assertSame(array(1 => '', 3 => '0', 7 => '1', 10 => '1.23'), $list->getValues()); + $this->assertSame(array('', '0', '1', '1.23'), $list->getValues()); + $this->assertSame(array('' => '', 0 => 0, 1 => 1, '1.23' => '1.23'), $list->getChoices()); + $this->assertSame(array('' => 'Empty', 0 => 'Zero', 1 => 'One', '1.23' => 'Float'), $list->getOriginalKeys()); } public function testNoChoices() @@ -102,33 +104,22 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest public function provideConvertibleChoices() { return array( - array(array(0), array(0)), - array(array(1), array(1)), - array(array('0'), array(0)), - array(array('1'), array(1)), - array(array('1.23'), array('1.23')), - array(array('foobar'), array('foobar')), + array(array(0 => 'Label'), array(0 => 0)), + array(array(1 => 'Label'), array(1 => 1)), + array(array('1.23' => 'Label'), array('1.23' => '1.23')), + array(array('foobar' => 'Label'), array('foobar' => 'foobar')), // The default value of choice fields is NULL. It should be treated // like the empty value for this choice list type - array(array(null), array('')), - array(array(1.23), array('1.23')), + array(array(null => 'Label'), array('' => '')), + array(array('1.23' => 'Label'), array('1.23' => '1.23')), // Always cast booleans to 0 and 1, because: // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean - array(array(true), array(1)), - array(array(false), array(0)), + array(array(true => 'Label'), array(1 => 1)), + array(array(false => 'Label'), array(0 => 0)), ); } - /** - * @dataProvider provideInvalidChoices - * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException - */ - public function testFailIfInvalidChoices(array $choices) - { - new ArrayKeyChoiceList($choices); - } - /** * @dataProvider provideInvalidChoices * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException @@ -155,7 +146,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest return $value; }; - $list = new ArrayKeyChoiceList(array('choice'), $callback); + $list = new ArrayKeyChoiceList(array('choice' => 'Label'), $callback); $this->assertSame(array($converted), $list->getValues()); } @@ -169,15 +160,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest array('1', '1'), array('1.23', '1.23'), array('foobar', 'foobar'), - // The default value of choice fields is NULL. It should be treated - // like the empty value for this choice list type - array(null, ''), - array(1.23, '1.23'), - // Always cast booleans to 0 and 1, because: - // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') - // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean - array(true, '1'), - array(false, ''), + array('', ''), ); } @@ -187,9 +170,11 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest return ':'.$choice; }; - $choiceList = new ArrayKeyChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); + $choiceList = new ArrayKeyChoiceList(array('foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz'), $callback); - $this->assertSame(array(2 => ':foo', 7 => ':bar', 10 => ':baz'), $choiceList->getValues()); + $this->assertSame(array(':foo', ':bar', ':baz'), $choiceList->getValues()); + $this->assertSame(array(':foo' => 'foo', ':bar' => 'bar', ':baz' => 'baz'), $choiceList->getChoices()); + $this->assertSame(array(':foo' => 'Foo', ':bar' => 'Bar', ':baz' => 'Baz'), $choiceList->getOriginalKeys()); $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => ':foo', 2 => ':baz'))); $this->assertSame(array(1 => ':foo', 2 => ':baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php index 716468276a..9855c4a708 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -204,8 +204,8 @@ class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase */ public function testCreateFromFlippedChoicesSameChoices($choice1, $choice2) { - $choices1 = array($choice1); - $choices2 = array($choice2); + $choices1 = array($choice1 => 'A'); + $choices2 = array($choice2 => 'A'); $list = new \stdClass(); $this->decoratedFactory->expects($this->once()) @@ -222,8 +222,8 @@ class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase */ public function testCreateFromFlippedChoicesDifferentChoices($choice1, $choice2) { - $choices1 = array($choice1); - $choices2 = array($choice2); + $choices1 = array($choice1 => 'A'); + $choices2 = array($choice2 => 'A'); $list1 = new \stdClass(); $list2 = new \stdClass(); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index e5112bf38f..16a116719c 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Symfony\Component\Form\ChoiceList\LegacyChoiceListAdapter; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; @@ -199,7 +200,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D') ); - $this->assertScalarListWithGeneratedValues($list); + $this->assertScalarListWithChoiceValues($list); } public function testCreateFromFlippedChoicesFlatTraversable() @@ -208,7 +209,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase new \ArrayIterator(array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D')) ); - $this->assertScalarListWithGeneratedValues($list); + $this->assertScalarListWithChoiceValues($list); } public function testCreateFromFlippedChoicesFlatValuesAsCallable() @@ -247,7 +248,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase ) ); - $this->assertScalarListWithGeneratedValues($list); + $this->assertScalarListWithChoiceValues($list); } public function testCreateFromFlippedChoicesGroupedTraversable() @@ -259,7 +260,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase )) ); - $this->assertScalarListWithGeneratedValues($list); + $this->assertScalarListWithChoiceValues($list); } public function testCreateFromFlippedChoicesGroupedValuesAsCallable() @@ -523,33 +524,16 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatViewWithCustomIndices($view); } - public function testCreateViewFlatGroupByAsArray() + public function testCreateViewFlatGroupByOriginalStructure() { - $view = $this->factory->createView( - $this->list, - array($this->obj2, $this->obj3), - null, // label - null, // index - array( - 'Group 1' => array('A' => true, 'B' => true), - 'Group 2' => array('C' => true, 'D' => true), - ) - ); + $list = new ArrayChoiceList(array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + )); - $this->assertGroupedView($view); - } - - public function testCreateViewFlatGroupByAsTraversable() - { $view = $this->factory->createView( - $this->list, - array($this->obj2, $this->obj3), - null, // label - null, // index - new \ArrayIterator(array( - 'Group 1' => array('A' => true, 'B' => true), - 'Group 2' => array('C' => true, 'D' => true), - )) + $list, + array($this->obj2, $this->obj3) ); $this->assertGroupedView($view); @@ -592,8 +576,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase null, // label null, // index function ($object) use ($obj1, $obj2) { - return $obj1 === $object || $obj2 === $object ? 'Group 1' - : 'Group 2'; + return $obj1 === $object || $obj2 === $object ? 'Group 1' : 'Group 2'; } ); @@ -749,78 +732,86 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase ->method('getRemainingViews') ->will($this->returnValue($other)); - $view = $this->factory->createView($list); + $view = $this->factory->createView(new LegacyChoiceListAdapter($list)); $this->assertEquals(array(new ChoiceView('y', 'y', 'Other')), $view->choices); $this->assertEquals(array(new ChoiceView('x', 'x', 'Preferred')), $view->preferredChoices); } - private function assertScalarListWithGeneratedValues(ChoiceListInterface $list) + private function assertScalarListWithChoiceValues(ChoiceListInterface $list) { + $this->assertSame(array('a', 'b', 'c', 'd'), $list->getValues()); + $this->assertSame(array( - 'A' => 'a', - 'B' => 'b', - 'C' => 'c', - 'D' => 'd', + 'a' => 'a', + 'b' => 'b', + 'c' => 'c', + 'd' => 'd', ), $list->getChoices()); $this->assertSame(array( - 'A' => 'a', - 'B' => 'b', - 'C' => 'c', - 'D' => 'd', - ), $list->getValues()); + 'a' => 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + ), $list->getOriginalKeys()); } private function assertObjectListWithGeneratedValues(ChoiceListInterface $list) { + $this->assertSame(array('0', '1', '2', '3'), $list->getValues()); + $this->assertSame(array( - 'A' => $this->obj1, - 'B' => $this->obj2, - 'C' => $this->obj3, - 'D' => $this->obj4, + 0 => $this->obj1, + 1 => $this->obj2, + 2 => $this->obj3, + 3 => $this->obj4, ), $list->getChoices()); $this->assertSame(array( - 'A' => '0', - 'B' => '1', - 'C' => '2', - 'D' => '3', - ), $list->getValues()); + 0 => 'A', + 1 => 'B', + 2 => 'C', + 3 => 'D', + ), $list->getOriginalKeys()); } private function assertScalarListWithCustomValues(ChoiceListInterface $list) { + $this->assertSame(array('a', 'b', '1', '2'), $list->getValues()); + $this->assertSame(array( - 'A' => 'a', - 'B' => 'b', - 'C' => 'c', - 'D' => 'd', + 'a' => 'a', + 'b' => 'b', + 1 => 'c', + 2 => 'd', ), $list->getChoices()); $this->assertSame(array( - 'A' => 'a', - 'B' => 'b', - 'C' => '1', - 'D' => '2', - ), $list->getValues()); + 'a' => 'A', + 'b' => 'B', + 1 => 'C', + 2 => 'D', + ), $list->getOriginalKeys()); } private function assertObjectListWithCustomValues(ChoiceListInterface $list) { + $this->assertSame(array('a', 'b', '1', '2'), $list->getValues()); + $this->assertSame(array( - 'A' => $this->obj1, - 'B' => $this->obj2, - 'C' => $this->obj3, - 'D' => $this->obj4, + 'a' => $this->obj1, + 'b' => $this->obj2, + 1 => $this->obj3, + 2 => $this->obj4, ), $list->getChoices()); $this->assertSame(array( - 'A' => 'a', - 'B' => 'b', - 'C' => '1', - 'D' => '2', - ), $list->getValues()); + 'a' => 'A', + 'b' => 'B', + 1 => 'C', + 2 => 'D', + ), $list->getOriginalKeys()); } private function assertFlatView($view) diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php index 2993721c82..5db96e6a7d 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php @@ -73,6 +73,36 @@ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame('RESULT', $this->list->getValues()); } + public function testGetStructuredValuesLoadsInnerListOnFirstCall() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->innerList->expects($this->exactly(2)) + ->method('getStructuredValues') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getStructuredValues()); + $this->assertSame('RESULT', $this->list->getStructuredValues()); + } + + public function testGetOriginalKeysLoadsInnerListOnFirstCall() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->innerList->expects($this->exactly(2)) + ->method('getOriginalKeys') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getOriginalKeys()); + $this->assertSame('RESULT', $this->list->getOriginalKeys()); + } + public function testGetChoicesForValuesForwardsCallIfListNotLoaded() { $this->loader->expects($this->exactly(2)) diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/LegacyChoiceListAdapterTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/LegacyChoiceListAdapterTest.php new file mode 100644 index 0000000000..521c950331 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/LegacyChoiceListAdapterTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +use Symfony\Component\Form\ChoiceList\LegacyChoiceListAdapter; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; + +/** + * @author Bernhard Schussek + */ +class LegacyChoiceListAdapterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var LegacyChoiceListAdapter + */ + private $list; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ChoiceListInterface + */ + private $adaptedList; + + protected function setUp() + { + $this->adaptedList = $this->getMock('Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'); + $this->list = new LegacyChoiceListAdapter($this->adaptedList); + } + + public function testGetChoices() + { + $this->adaptedList->expects($this->once()) + ->method('getChoices') + ->willReturn(array(1 => 'a', 4 => 'b', 7 => 'c')); + $this->adaptedList->expects($this->once()) + ->method('getValues') + ->willReturn(array(1 => ':a', 4 => ':b', 7 => ':c')); + + $this->assertSame(array(':a' => 'a', ':b' => 'b', ':c' => 'c'), $this->list->getChoices()); + } + + public function testGetValues() + { + $this->adaptedList->expects($this->once()) + ->method('getChoices') + ->willReturn(array(1 => 'a', 4 => 'b', 7 => 'c')); + $this->adaptedList->expects($this->once()) + ->method('getValues') + ->willReturn(array(1 => ':a', 4 => ':b', 7 => ':c')); + + $this->assertSame(array(':a', ':b', ':c'), $this->list->getValues()); + } + + public function testGetStructuredValues() + { + $this->adaptedList->expects($this->once()) + ->method('getChoices') + ->willReturn(array(1 => 'a', 4 => 'b', 7 => 'c')); + $this->adaptedList->expects($this->once()) + ->method('getValues') + ->willReturn(array(1 => ':a', 4 => ':b', 7 => ':c')); + + $this->assertSame(array(1 => ':a', 4 => ':b', 7 => ':c'), $this->list->getStructuredValues()); + } + + public function testGetOriginalKeys() + { + $this->adaptedList->expects($this->once()) + ->method('getChoices') + ->willReturn(array(1 => 'a', 4 => 'b', 7 => 'c')); + $this->adaptedList->expects($this->once()) + ->method('getValues') + ->willReturn(array(1 => ':a', 4 => ':b', 7 => ':c')); + + $this->assertSame(array(':a' => 1, ':b' => 4, ':c' => 7), $this->list->getOriginalKeys()); + } + + public function testGetChoicesForValues() + { + $this->adaptedList->expects($this->once()) + ->method('getChoicesForValues') + ->with(array(1 => ':a', 4 => ':b', 7 => ':c')) + ->willReturn(array(1 => 'a', 4 => 'b', 7 => 'c')); + + $this->assertSame(array(1 => 'a', 4 => 'b', 7 => 'c'), $this->list->getChoicesForValues(array(1 => ':a', 4 => ':b', 7 => ':c'))); + } + + public function testGetValuesForChoices() + { + $this->adaptedList->expects($this->once()) + ->method('getValuesForChoices') + ->with(array(1 => 'a', 4 => 'b', 7 => 'c')) + ->willReturn(array(1 => ':a', 4 => ':b', 7 => ':c')); + + $this->assertSame(array(1 => ':a', 4 => ':b', 7 => ':c'), $this->list->getValuesForChoices(array(1 => 'a', 4 => 'b', 7 => 'c'))); + } + + public function testGetAdaptedList() + { + $this->assertSame($this->adaptedList, $this->list->getAdaptedList()); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php index cb0b34b41f..b936ea35cc 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Form\Tests\Extension\Core\EventListener; +use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; /** * @group legacy @@ -26,7 +26,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase { parent::setUp(); - $this->choiceList = new SimpleChoiceList(array('' => 'Empty', 0 => 'A', 1 => 'B')); + $this->choiceList = new ArrayKeyChoiceList(array('' => 'Empty', 0 => 'A', 1 => 'B')); } protected function tearDown() @@ -45,7 +45,6 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $listener = new FixRadioInputListener($this->choiceList, true); $listener->preSubmit($event); - // Indices in SimpleChoiceList are zero-based generated integers $this->assertEquals(array(2 => '1'), $event->getData()); } @@ -58,7 +57,6 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $listener = new FixRadioInputListener($this->choiceList, true); $listener->preSubmit($event); - // Indices in SimpleChoiceList are zero-based generated integers $this->assertEquals(array(1 => '0'), $event->getData()); } @@ -71,13 +69,12 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $listener = new FixRadioInputListener($this->choiceList, true); $listener->preSubmit($event); - // Indices in SimpleChoiceList are zero-based generated integers $this->assertEquals(array(0 => ''), $event->getData()); } public function testConvertEmptyStringToPlaceholderIfNotFound() { - $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B')); + $list = new ArrayKeyChoiceList(array(0 => 'A', 1 => 'B')); $data = ''; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -91,7 +88,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase public function testDontConvertEmptyStringToPlaceholderIfNoPlaceholderUsed() { - $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B')); + $list = new ArrayKeyChoiceList(array(0 => 'A', 1 => 'B')); $data = ''; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 8e79b72199..32fa8b1af7 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -186,6 +186,32 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase } } + public function testExpandedChoicesOptionsAreFlattenedObjectChoices() + { + $obj1 = (object) array('id' => 1, 'name' => 'Bernhard'); + $obj2 = (object) array('id' => 2, 'name' => 'Fabien'); + $obj3 = (object) array('id' => 3, 'name' => 'Kris'); + $obj4 = (object) array('id' => 4, 'name' => 'Jon'); + $obj5 = (object) array('id' => 5, 'name' => 'Roman'); + + $form = $this->factory->create('choice', null, array( + 'expanded' => true, + 'choices' => array( + 'Symfony' => array($obj1, $obj2, $obj3), + 'Doctrine' => array($obj4, $obj5), + ), + 'choices_as_values' => true, + 'choice_name' => 'id', + )); + + $this->assertSame(5, $form->count(), 'Each nested choice should become a new field, not the groups'); + $this->assertTrue($form->has(1)); + $this->assertTrue($form->has(2)); + $this->assertTrue($form->has(3)); + $this->assertTrue($form->has(4)); + $this->assertTrue($form->has(5)); + } + public function testExpandedCheckboxesAreNeverRequired() { $form = $this->factory->create('choice', null, array(