8000 [Form] Fixed handling of choices passed in choice groups by webmozart · Pull Request #15061 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Form] Fixed handling of choices passed in choice groups #15061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 96 additions & 44 deletions src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php
8000 8000
Original file line number Diff line number Diff line change
Expand Up @@ -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 $structuredValues;

/**
* The original keys of the choices array.
*
* @var int[]|string[]
*/
protected $values = array();
protected $originalKeys;

/**
* The callback for creating the value for a choice.
Expand All @@ -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 ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}

if (null === $value) {
$i = 0;
foreach ($this->choices as $key => $choice) {
$this->values[$key] = (string) $i++;
}
if (null !== $value) {
// If a deterministic value generator was passed, use it later
$this->valueCallback = $value;
} else {
foreach ($choices as $key => $choice) {
$this->values[$key] = (string) call_user_func($value, $choice);
}
// 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;
}

/**
Expand All @@ -91,7 +108,23 @@ public function getChoices()
*/
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;
}

/**
Expand All @@ -102,17 +135,8 @@ public function getChoicesForValues(array $values)
$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];
}
}

Expand All @@ -131,28 +155,56 @@ public function getValuesForChoices(array $choices)
$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;
}
}
}
56 changes: 48 additions & 8 deletions src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -89,23 +91,27 @@ public static function toArrayKey($choice)
* 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;
}

Expand All @@ -122,7 +128,7 @@ public function getChoicesForValues(array $values)

// 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);
Expand All @@ -143,4 +149,38 @@ public function getValuesForChoices(array $choices)

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;
}
}
}
64 changes: 52 additions & 12 deletions src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,80 @@
/**
* 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
{
/**
* 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'),
* )
*
* @re 89E5 turn 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.
*
Expand Down
Loading
0