bug #15061 [Form] Fixed handling of choices passed in choice groups (webmozart)

This PR was merged into the 2.7 branch.

Discussion
----------

[Form] Fixed handling of choices passed in choice groups

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | **yes**
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #14915
| License       | MIT
| Doc PR        | -

I introduced a bug in the 2.7 ChoiceList implementation when choices are passed as groups:

```
$form->add('response', 'choice', array(
    'choices' => array(
        'Decided' => array($yesObj, $noObj),
        'Undecided' => array($maybeObj),
    ),
    // use getName() for the labels
    'choice_label' => 'name',
    'choices_as_values' => true,
));
```

In this example, since the choices `$yesObj` and `$maybeObj` have the same array index `0`, the same label is displayed for the two options. The problem is that we rely on the keys passed in the "choices" option to identify choices in a choice list (which are, as you see, not guaranteed to be free of duplicates).

This PR changes the new choice list implementation to identify choices by values instead. We already have the guarantee that choices can be identified uniquely by their string values.

This PR should be included in 2.7.2 to fix the regression.

Unfortunately, a few BC breaks in the new implementation are necessary to make this fix:

* The legacy `ChoiceListInterface` was reverted to how it was in 2.6 and does *not* extend the new `ChoiceListInterface` anymore.
* As a consequence, legacy choice lists need to be wrapped into a `LegacyChoiceListAdapter` when they are passed to any place in the framework where a new choice list is expected.
* The new `ChoiceListInterface` has two additional methods `getStructuredValues()` and `getOriginalKeys()` now.
* `ArrayKeyChoiceList::toArrayKey()` was marked as internal.
* `ChoiceListFactoryInterface::createView()` does not accept arrays and Traversables anymore for the `$groupBy` parameter (for simplicity).

@fabpot Where should we document the upgrade path for 2.7.1 => 2.7.2?

Commits
-------

7623dc8 [Form] Fixed handling of choices passed in choice groups
This commit is contained in:
Fabien Potencier 2015-06-30 14:46:52 +02:00
commit b16fc6c2f6
20 changed files with 818 additions and 353 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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 <bschussek@gmail.com>
*/
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.
*

View File

@ -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])) {

View File

@ -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
*/

View File

@ -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,
@ -340,9 +245,9 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface
self::addChoiceView(
$choice,
$key,
$value,
$label,
$values,
$keys,
$index,
$attr,
$isPreferred,

View File

@ -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
*/

View File

@ -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}
*/

View File

@ -0,0 +1,144 @@
<?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\ChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface;
/**
* Adapts a legacy choice list implementation to {@link ChoiceListInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @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;
}
}
}

View File

@ -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 <bschussek@gmail.com>
*
* @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.
*

View File

@ -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');

View File

@ -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);

View File

@ -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) {

View File

@ -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')));
}

View File

@ -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();

View File

@ -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;
@ -206,7 +207,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()
@ -215,7 +216,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()
@ -254,7 +255,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase
)
);
$this->assertScalarListWithGeneratedValues($list);
$this->assertScalarListWithChoiceValues($list);
}
public function testCreateFromFlippedChoicesGroupedTraversable()
@ -266,7 +267,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase
))
);
$this->assertScalarListWithGeneratedValues($list);
$this->assertScalarListWithChoiceValues($list);
}
public function testCreateFromFlippedChoicesGroupedValuesAsCallable()
@ -530,33 +531,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);
@ -612,8 +596,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';
}
);
@ -769,78 +752,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)

View File

@ -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))

View File

@ -0,0 +1,110 @@
<?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\Tests\ChoiceList;
use Symfony\Component\Form\ChoiceList\LegacyChoiceListAdapter;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
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());
}
}

View File

@ -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');

View File

@ -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(