Skip to content

[Form] Deprecated setting "choices_as_values" to "false" #16681

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

Closed
Closed
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
37 changes: 37 additions & 0 deletions src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ public function __construct($choices, $value = null)
$choices = iterator_to_array($choices);
}

if (null === $value && $this->castableToString($choices)) {
$value = function ($choice) {
return (string) $choice;
};
}

if (null !== $value) {
// If a deterministic value generator was passed, use it later
$this->valueCallback = $value;
Expand Down Expand Up @@ -207,4 +213,35 @@ protected function flatten(array $choices, $value, &$choicesByValues, &$keysByVa
$structuredValues[$key] = $choiceValue;
}
}

/**
* Checks whether the given choices can be cast to strings without
* generating duplicates.
*
* @param array $choices The choices.
* @param array|null $cache The cache for previously checked entries. Internal
*
* @return bool Returns true if the choices can be cast to strings and
* false otherwise.
*/
private function castableToString(array $choices, array &$cache = array())
{
foreach ($choices as $choice) {
if (is_array($choice)) {
if (!$this->castableToString($choice, $cache)) {
return false;
}

continue;
} elseif (!is_scalar($choice)) {
return false;
} elseif (isset($cache[(string) $choice])) {
return false;
}

$cache[(string) $choice] = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the cast is not required since it should happen automatically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast does make a difference for the value false. By default, false is converted to 0 when adding it as array key. Here, it is converted to an empty string. I don't have a strong preference, but I lean towards converting false to an empty string.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I didn't know that!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to #17292, could you take a look please?

}

return true;
}
}
9 changes: 9 additions & 0 deletions src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ public function configureOptions(OptionsResolver $resolver)
return $choiceListFactory->createListFromChoices($choices, $options['choice_value']);
};

$choicesAsValuesNormalizer = function (Options $options, $choicesAsValues) {
if (true !== $choicesAsValues) {
@trigger_error('The value "false" for the "choices_as_values" option is deprecated since version 2.8 and will not be supported anymore in 3.0. Set this option to "true" and flip the contents of the "choices" option instead.', E_USER_DEPRECATED);
}

return $choicesAsValues;
};

$placeholderNormalizer = function (Options $options, $placeholder) {
if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) {
@trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED);
Expand Down Expand Up @@ -343,6 +351,7 @@ public function configureOptions(OptionsResolver $resolver)
$resolver->setNormalizer('choice_list', $choiceListNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setNormalizer('choices_as_values', $choicesAsValuesNormalizer);

$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'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class CountryType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choices' => Intl::getRegionBundle()->getCountryNames(),
'choices' => array_flip(Intl::getRegionBundle()->getCountryNames()),
'choices_as_values' => true,
'choice_translation_domain' => false,
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class CurrencyType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choices' => Intl::getCurrencyBundle()->getCurrencyNames(),
'choices' => array_flip(Intl::getCurrencyBundle()->getCurrencyNames()),
'choices_as_values' => true,
'choice_translation_domain' => false,
));
}
Expand Down
16 changes: 10 additions & 6 deletions src/Symfony/Component/Form/Extension/Core/Type/DateType.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,13 @@ public function buildForm(FormBuilderInterface $builder, array $options)
if ('choice' === $options['widget']) {
// Only pass a subset of the options to children
$yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years']));
$yearOptions['choices_as_values'] = true;
$yearOptions['placeholder'] = $options['placeholder']['year'];
$monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months']));
$monthOptions['choices_as_values'] = true;
$monthOptions['placeholder'] = $options['placeholder']['month'];
$dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days']));
$dayOptions['choices_as_values'] = true;
$dayOptions['placeholder'] = $options['placeholder']['day'];
}

Expand Down Expand Up @@ -262,6 +265,7 @@ private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $
{
$pattern = $formatter->getPattern();
$timezone = $formatter->getTimezoneId();
$formattedTimestamps = array();

if ($setTimeZone = PHP_VERSION_ID >= 50500 || method_exists($formatter, 'setTimeZone')) {
$formatter->setTimeZone('UTC');
Expand All @@ -272,8 +276,8 @@ private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $
if (preg_match($regex, $pattern, $matches)) {
$formatter->setPattern($matches[0]);

foreach ($timestamps as $key => $timestamp) {
$timestamps[$key] = $formatter->format($timestamp);
foreach ($timestamps as $timestamp => $choice) {
$formattedTimestamps[$formatter->format($timestamp)] = $choice;
}

// I'd like to clone the formatter above, but then we get a
Expand All @@ -287,7 +291,7 @@ private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $
$formatter->setTimeZoneId($timezone);
}

return $timestamps;
return $formattedTimestamps;
}

private function listYears(array $years)
Expand All @@ -296,7 +300,7 @@ private function listYears(array $years)

foreach ($years as $year) {
if (false !== $y = gmmktime(0, 0, 0, 6, 15, $year)) {
$result[$year] = $y;
$result[$y] = $year;
}
}

Expand All @@ -308,7 +312,7 @@ private function listMonths(array $months)
$result = array();

foreach ($months as $month) {
$result[$month] = gmmktime(0, 0, 0, $month, 15);
$result[gmmktime(0, 0, 0, $month, 15)] = $month;
}

return $result;
Expand All @@ -319,7 +323,7 @@ private function listDays(array $days)
$result = array();

foreach ($days as $day) {
$result[$day] = gmmktime(0, 0, 0, 5, $day);
$result[gmmktime(0, 0, 0, 5, $day)] = $day;
}

return $result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class LanguageType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choices' => Intl::getLanguageBundle()->getLanguageNames(),
'choices' => array_flip(Intl::getLanguageBundle()->getLanguageNames()),
'choices_as_values' => true,
'choice_translation_domain' => false,
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class LocaleType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choices' => Intl::getLocaleBundle()->getLocaleNames(),
'choices' => array_flip(Intl::getLocaleBundle()->getLocaleNames()),
'choices_as_values' => true,
'choice_translation_domain' => false,
));
}
Expand Down
9 changes: 6 additions & 3 deletions src/Symfony/Component/Form/Extension/Core/Type/TimeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,33 @@ public function buildForm(FormBuilderInterface $builder, array $options)
$hours = $minutes = array();

foreach ($options['hours'] as $hour) {
$hours[$hour] = str_pad($hour, 2, '0', STR_PAD_LEFT);
$hours[str_pad($hour, 2, '0', STR_PAD_LEFT)] = $hour;
}

// Only pass a subset of the options to children
$hourOptions['choices'] = $hours;
$hourOptions['choices_as_values'] = true;
$hourOptions['placeholder'] = $options['placeholder']['hour'];

if ($options['with_minutes']) {
foreach ($options['minutes'] as $minute) {
$minutes[$minute] = str_pad($minute, 2, '0', STR_PAD_LEFT);
$minutes[str_pad($minute, 2, '0', STR_PAD_LEFT)] = $minute;
}

$minuteOptions['choices'] = $minutes;
$minuteOptions['choices_as_values'] = true;
$minuteOptions['placeholder'] = $options['placeholder']['minute'];
}

if ($options['with_seconds']) {
$seconds = array();

foreach ($options['seconds'] as $second) {
$seconds[$second] = str_pad($second, 2, '0', STR_PAD_LEFT);
$seconds[str_pad($second, 2, '0', STR_PAD_LEFT)] = $second;
}

$secondOptions['choices'] = $seconds;
$secondOptions['choices_as_values'] = true;
$secondOptions['placeholder'] = $options['placeholder']['second'];
}

Expand Down
46 changes: 45 additions & 1 deletion src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,21 @@ class TimezoneType extends AbstractType
*/
private static $timezones;

/**
* Stores the available timezone choices.
*
* @var array
*/
private static $flippedTimezones;

/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choices' => self::getTimezones(),
'choices' => self::getFlippedTimezones(),
'choices_as_values' => true,
'choice_translation_domain' => false,
));
}
Expand Down Expand Up @@ -85,4 +93,40 @@ public static function getTimezones()

return static::$timezones;
}

/**
* Returns the timezone choices.
*
* The choices are generated from the ICU function
* \DateTimeZone::listIdentifiers(). They are cached during a single request,
* so multiple timezone fields on the same page don't lead to unnecessary
* overhead.
*
* @return array The timezone choices
*/
private static function getFlippedTimezones()
{
if (null === self::$timezones) {
self::$timezones = array();

foreach (\DateTimeZone::listIdentifiers() as $timezone) {
$parts = explode('/', $timezone);

if (count($parts) > 2) {
$region = $parts[0];
$name = $parts[1].' - '.$parts[2];
} elseif (count($parts) > 1) {
$region = $parts[0];
$name = $parts[1];
} else {
$region = 'Other';
$name = $parts[0];
}

self::$timezones[$region][str_replace('_', ' ', $name)] = $timezone;
}
}

return self::$timezones;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,56 @@ public function testCreateChoiceListWithValueCallback()
$this->assertSame(array(1 => ':foo', 2 => ':baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz')));
}

public function testCreateChoiceListWithoutValueCallbackAndDuplicateFreeToStringChoices()
{
$choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 123));

$this->assertSame(array('foo', 'bar', '123'), $choiceList->getValues());
$this->assertSame(array('foo' => 'foo', 'bar' => 'bar', '123' => 123), $choiceList->getChoices());
$this->assertSame(array('foo' => 2, 'bar' => 7, '123' => 10), $choiceList->getOriginalKeys());
$this->assertSame(array(1 => 'foo', 2 => 123), $choiceList->getChoicesForValues(array(1 => 'foo', 2 => '123')));
$this->assertSame(array(1 => 'foo', 2 => '123'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 123)));
}

public function testCreateChoiceListWithoutValueCallbackAndToStringDuplicates()
{
$choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => '123', 10 => 123));

$this->assertSame(array('0', '1', '2'), $choiceList->getValues());
$this->assertSame(array('0' => 'foo', '1' => '123', '2' => 123), $choiceList->getChoices());
$this->assertSame(array('0' => 2, '1' => 7, '2' => 10), $choiceList->getOriginalKeys());
$this->assertSame(array(1 => 'foo', 2 => 123), $choiceList->getChoicesForValues(array(1 => '0', 2 => '2')));
$this->assertSame(array(1 => '0', 2 => '2'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 123)));
}

public function testCreateChoiceListWithoutValueCallbackAndMixedChoices()
{
$object = new \stdClass();
$choiceList = new ArrayChoiceList(array(2 => 'foo', 5 => array(7 => '123'), 10 => $object));

$this->assertSame(array('0', '1', '2'), $choiceList->getValues());
$this->assertSame(array('0' => 'foo', '1' => '123', '2' => $object), $choiceList->getChoices());
$this->assertSame(array('0' => 2, '1' => 7, '2' => 10), $choiceList->getOriginalKeys());
$this->assertSame(array(1 => 'foo', 2 => $object), $choiceList->getChoicesForValues(array(1 => '0', 2 => '2')));
$this->assertSame(array(1 => '0', 2 => '2'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => $object)));
}

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('a', 'b', 'c', 'd'), $choiceList->getValues());
$this->assertSame(array(
'Group 1' => array('A' => '0', 'B' => '1'),
'Group 2' => array('C' => '2', 'D' => '3'),
'Group 1' => array('A' => 'a', 'B' => 'b'),
'Group 2' => array('C' => 'c', 'D' => 'd'),
), $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')));
$this->assertSame(array('a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => 'd'), $choiceList->getChoices());
$this->assertSame(array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), $choiceList->getOriginalKeys());
$this->assertSame(array(1 => 'a', 2 => 'b'), $choiceList->getChoicesForValues(array(1 => 'a', 2 => 'b')));
$this->assertSame(array(1 => 'a', 2 => 'b'), $choiceList->getValuesForChoices(array(1 => 'a', 2 => 'b')));
}

public function testCompareChoicesByIdentityByDefault()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase

protected function setUp()
{
$list = new ArrayChoiceList(array('', 0, 'X'));
$list = new ArrayChoiceList(array('', false, 'X'));

$this->transformer = new ChoiceToValueTransformer($list);
}
Expand All @@ -35,7 +35,7 @@ public function transformProvider()
return array(
// more extensive test set can be found in FormUtilTest
array('', '0'),
array(0, '1'),
array(false, '1'),
);
}

Expand All @@ -53,7 +53,7 @@ public function reverseTransformProvider()
// values are expected to be valid choice keys already and stay
// the same
array('0', ''),
array('1', 0),
array('1', false),
array('2', 'X'),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase

protected function setUp()
{
$list = new ArrayChoiceList(array('A', 'B', 'C'));
$list = new ArrayChoiceList(array('', false, 'X'));
$this->transformer = new ChoicesToValuesTransformer($list);
}

Expand All @@ -31,7 +31,7 @@ protected function tearDown()

public function testTransform()
{
$in = array('A', 'B', 'C');
$in = array('', false, 'X');
$out = array('0', '1', '2');

$this->assertSame($out, $this->transformer->transform($in));
Expand All @@ -54,7 +54,7 @@ public function testReverseTransform()
{
// values are expected to be valid choices and stay the same
$in = array('0', '1', '2');
$out = array('A', 'B', 'C');
$out = array('', false, 'X');

$this->assertSame($out, $this->transformer->reverseTransform($in));
}
Expand Down
Loading