Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Form] Add option widget to ChoiceType #17646

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
wants to merge 4 commits into from
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
36 changes: 36 additions & 0 deletions UPGRADE-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,42 @@ Form

* The `choices_as_values` option of the `ChoiceType` has been removed.

* The `expanded` option of the `ChoiceType` has been removed.
Use instead the `widget` option with `select`, `checkbox`, `radio`, `text` or `hidden`.

Before:

```php
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

$form = $this->createFormBuilder()
->add('isAttending', ChoiceType::class, array(
'choices' => array(
'Yes' => true,
'No' => false,
),
'expanded' => true,
'multiple' => false,
))
->getForm();
```

After:

```php
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

$form = $this->createFormBuilder()
->add('isAttending', ChoiceType::class, array(
'choices' => array(
'Yes' => true,
'No' => false,
),
'widget' => 'radio',
))
->getForm();
```

Serializer
----------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleAssociationToIntIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity;

/**
* @group legacy
*/
class EntityTypeTest extends TypeTestCase
{
const ITEM_GROUP_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
{%- endblock textarea_widget -%}

{%- block choice_widget -%}
{% if expanded %}
{% if widget is defined and 'hidden' == widget %}
{{- block('hidden_widget') -}}
{% elseif widget is defined and 'text' == widget %}
{{- block('form_widget_simple') -}}
Copy link
Contributor

Choose a reason for hiding this comment

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

hidden_widget and form_widget_simple never get a value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

{% elseif expanded %}
{{- block('choice_widget_expanded') -}}
{% else %}
{{- block('choice_widget_collapsed') -}}
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* deprecated the "choices_as_values" option of ChoiceType
* deprecated the "expanded" option of ChoiceType

3.0.0
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\Core\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

/**
* Converts an array of values to a string with multiple values separated by a delimiter.
*
* @author Bilal Amarni <[email protected]>
*/
class ValuesToStringTransformer implements DataTransformerInterface
{
/**
* @var string
*/
private $delimiter;

/**
* @var bool
*/
private $trim;

/**
* @param string $delimiter
* @param bool $trim
*/
public function __construct($delimiter, $trim)
{
$this->delimiter = $delimiter;
$this->trim = $trim;
}

/**
* @param array $array
*
* @return string
*
* @throws UnexpectedTypeException if the given value is not an array
*/
public function transform($array)
{
if (null === $array) {
return '';
}

if (!is_array($array)) {
throw new TransformationFailedException('Expected an array');
}

return implode($this->delimiter, $array);
}

/**
* @param string $string
*
* @return array
*
* @throws UnexpectedTypeException if the given value is not a string
*/
public function reverseTransform($string)
{
if (empty($string)) {
return array();
}

if (!is_string($string)) {
throw new TransformationFailedException('Expected a string');
}

$values = explode($this->delimiter, $string);

if ($this->trim) {
$values = array_map('trim', $values);
}

return $values;
}
}
49 changes: 44 additions & 5 deletions src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ValuesToStringTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

Expand Down Expand Up @@ -60,7 +61,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
$choiceList = $this->createChoiceList($options);
$builder->setAttribute('choice_list', $choiceList);

if ($options['expanded']) {
if ($options['expanded'] && !in_array($options['widget'], array('text', 'hidden'), true)) {
$builder->setDataMapper($options['multiple']
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be if ('expanded' === $options['widget'])

What is the point of having checkbox and radio value for widget option if you still have to deal with multiple option ?

widget might have few options like text, hidden, select and expanded

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For checkbox and radio, multiple option is ignored, I had made the same suggestion (see linked PR description) and @webmozart preferred to have more explicit names.

? new CheckboxListMapper($choiceList)
: new RadioListMapper($choiceList));
Expand Down Expand Up @@ -146,10 +147,15 @@ public function buildForm(FormBuilderInterface $builder, array $options)
});
}
} elseif ($options['multiple']) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What about widget set to text or hidden with multiple being false ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry I don't get your comment? it's handled in the next else condition

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you expect the html output to be when widget is set to text ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As expanded is deprecated for widget, it doesn't make sens to use them together.

// <select> tag with "multiple" option
// "select", "text" or "hidden" widget with "multiple" option
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));

// for "text" / "hidden" widget, view data uses a delimiter
if (in_array($options['widget'], array('text', 'hidden'), true)) {
$builder->addViewTransformer(new ValuesToStringTransformer($options['delimiter'], $options['trim']));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This implementation is wrong IMO since transformers may be executed in the right order for transformation but not for reversing transformation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure I get what you mean, Symfony will chain them in the correct order automatically depending on the direction (view to norm or norm to view).

} else {
// <select> tag without "multiple" option
// "select", "text" or "hidden" tag without "multiple" option
$builder->addViewTransformer(new ChoiceToValueTransformer($choiceList));
}

Expand Down Expand Up @@ -179,15 +185,20 @@ public function buildView(FormView $view, FormInterface $form, array $options)
: $this->createChoiceListView($choiceList, $options);

$view->vars = array_replace($view->vars, array(
'widget' => $options['widget'],
'multiple' => $options['multiple'],
'expanded' => $options['expanded'],
'expanded' => $options['expanded'], // BC
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like actual compatibility

'preferred_choices' => $choiceListView->preferredChoices,
'choices' => $choiceListView->choices,
'separator' => '-------------------',
'placeholder' => null,
'choice_translation_domain' => $choiceTranslationDomain,
));

if (in_array($options['widget'], array('text', 'hidden'), true)) {
return;
}

// The decision, whether a choice is selected, is potentially done
// thousand of times during the rendering of a template. Provide a
// closure here that is optimized for the value of the form, to
Expand Down Expand Up @@ -226,6 +237,10 @@ public function buildView(FormView $view, FormInterface $form, array $options)
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
if (in_array($options['widget'], array('text', 'hidden'), true)) {
return;
}

if ($options['expanded']) {
Copy link
Contributor

Choose a reason for hiding this comment

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

One last comment "for the road" if I may :

You did the change in https://github.com/bamarni/symfony/blob/9ba6a34f64fc063b3bfc8874d6f9300ba8f4c0be/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php#L145

Why not this PR ?

Handling this everywhere in the class (except for choice view vars) is how you can make $options['expanded'] deprecated otherwise it has to be marked internal.

// Radio buttons should have the same name as the parent
$childName = $view->vars['full_name'];
Expand Down Expand Up @@ -302,9 +317,30 @@ public function configureOptions(OptionsResolver $resolver)
return $choiceTranslationDomain;
};

$multipleNormalizer = function (Options $options, $multiple) {
if (in_array($options['widget'], array('radio', 'checkbox'), true)) {
return 'checkbox' === $options['widget'];
}

return $multiple;
};

$expandedNomalizer = function (Options $options, $expanded) {
if (null !== $expanded) {
@trigger_error('The "expanded" option is deprecated since version 3.1 and will be removed in 4.0. You should use "widget" instead.', E_USER_DEPRECATED);
} else {
$expanded = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

This line is not need if you cast return to bool :

return (bool) (in_array($options['widget'], array('radio', 'checkbox'), true) ?: $expanded);

anyway the cast would be done anywhere in this class as it's only a test variable. Then it has to remain internal and not to be removed in 4.0 with the actual implementation IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is so wrong about this normalizer for you to comment on it since days? :D
Handling everything in one line makes it less readable imo.

}

return in_array($options['widget'], array('radio', 'checkbox'), true) ?: $expanded;
};

Copy link
Contributor

Choose a reason for hiding this comment

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

You could have only one normalizer :

$expandedNormalizer = function (Options $options, $expanded) {
    // trigger error
    return 'expanded' === $options['widget'];
}

Anyway it should be :

$expandedNomalizer = function (Options $options, $expanded) {
    if (null !== $expanded) {
        @trigger_error('The "expanded" option is deprecated since version 3.1 for internal use only. You should use "widget" option with "radio" or "checkbox" instead.', E_USER_DEPRECATED);
     }

    return in_array($options['widget'], array('radio', 'checkbox'));
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as discussed previously, the expanded option from your first snippet hasn't been accepted, and the second snippet doesn't support $expanded set to true

Copy link
Contributor

Choose a reason for hiding this comment

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

There is no need to handle expanded value when it is deprecated by this PR... this is actually the meaning of this normalizer.

you could write :

$expandedNomalizer = function (Options $options, $expanded) {
    if (null !== $expanded) {
        @trigger_error('The "expanded" option is deprecated since version 3.1 for internal use only. You should use "widget" option with "radio" or "checkbox" instead.', E_USER_DEPRECATED);
     }

    return 'select' === $options['widget'] ? false : (in_array($options['widget'], array('radio', 'checkbox')) ?: $expanded);
};

only if text and hidden were indeed been handled differently when expanded is true and if it was not deprecated. Or neither options seems to be implemented properly in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no need to handle expanded value when it is deprecated by this PR... this is actually the meaning of this normalizer.

The goal of this normalizer is to handle expanded value (to provide BC) while overriding it if needed (when widget is defined).

$resolver->setDefaults(array(
'widget' => null,
'multiple' => false,
'expanded' => false,
'delimiter' => ',',
'expanded' => null, // deprecated
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO should be marked as internal since it's still used in the implementation of this class

'choice_list' => null, // deprecated
'choices' => array(),
'choices_as_values' => null, // deprecated since 3.1
'choice_loader' => null,
Expand All @@ -325,6 +361,8 @@ public function configureOptions(OptionsResolver $resolver)
'choice_translation_domain' => true,
));

$resolver->setNormalizer('expanded', $expandedNomalizer);
$resolver->setNormalizer('multiple', $multipleNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setNormalizer('choices_as_values', $choicesAsValuesNormalizer);
Copy link
Contributor

Choose a reason for hiding this comment

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

you should add :

$resolver->setAllowedTypes('widget', array('null', 'string'));
$resolver->setAllowedValues('widget', array('hidden', 'text', 'select', 'checkbox', 'radio'));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

Expand All @@ -338,6 +376,7 @@ public function configureOptions(OptionsResolver $resolver)
$resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('group_by', array('null', 'array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedValues('widget', array(null, 'hidden', 'text', 'select', 'checkbox', 'radio'));
}

/**
Expand Down
Loading