Symfony: [Form][2.7] Preselection of choice field with boolean value

Created on 21 May 2015  Â·  56Comments  Â·  Source: symfony/symfony

Hi,

I'm currently updating my code to symfony 2.7 and I saw a weird behavior with choice field form with boolean value.

Here is my form class:

<?php
namespace Icap\DropzoneBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DropzoneCommonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('peerReview', 'choice', array(
                'required' => true,
                'choices' => array(
                    'Standard evaluation' => false,
                    'Peer review evaluation' => true
                ),
                'choices_as_values' => true,
                'expanded' => true,
                'multiple' => false
            ));

    }

    public function getName()
    {
        return 'icap_dropzone_common_form';
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            array(
                'data_class' => 'Icap\DropzoneBundle\Entity\Dropzone',
                'language' => 'en',
                'translation_domain' => 'icap_dropzone',
                'date_format' => DateType::HTML5_FORMAT,
            )
        );
    }
}

And how the field is displayed, pretty classic:

{{ form_row(form.peerReview, {'label_attr': {'style': 'display: none;'}}) }}

But at the display of the form the field doesn't have its value checked, the radio button here.
The radio button is checked at the display of the form only after submitting it.

I checked in the debug bar to see what was in the form and I found what I think it's an error.
Before submitting the form the value, and the data, of the field peerReview is 1, and after submitting the value, and the data to, is true. I checked in my object and the attribute is a boolean with true as value.
After submitting the value is boolean, what I expect it to be, but before submission it's an integer.

Is it a bug or am I missing something?

Bug Form Needs Review

Most helpful comment

@MichaelMackus there are some works in progress, see https://github.com/symfony/symfony-docs/issues/6340 and https://github.com/symfony/symfony-docs/pull/6393.
Feedbacks are welcome!

All 56 comments

Yes maybe.
I have an other form with the same kind of field but as choices I have something like that:

array(
    'notStartedManualState' => 'notStarted',
    'allowDropManualState' => 'allowDrop',
    'peerReviewManualState' => 'peerReview',
    'allowDropAndPeerReviewManualState' => 'allowDropAndPeerReview',
    'finishedManualState' => 'finished',
)

And no problem with this array.
I just have issue with array whith boolean value.

I have a similar problem with boolean choices, this is my code https://gist.github.com/amsolucionesweb/1725c4959a1eb3200820 , when I load the form from doctrine entity with boolean type the radio button loaded in the view with none option selected.

How are you initializing the Dropzone object on which the form is bound for its peerReview property ? Are you setting 1 or true in it ?

My Dropzone object is initialize by Doctrine, object retrieve from ParamConverter.
This field is a boolean one as for Doctrine, so I think Doctrine initialize it with a boolean value.
And dumping this field show me true as a boolean value.

I have the same problem, and I was investigating and the problem seems to be in line https://github.com/symfony/symfony/blob/2.7/src/Symfony/Component/Form/Form.php#L347

The problem is that it is entering the if and becoming false to an empty string:

// dump($modelData)  ->  false
if (!$this->config->getViewTransformers() && !$this->config->getModelTransformers() && is_scalar($modelData)) {
       $modelData = (string) $modelData;
}
// dump($modelData)  ->  ''

In sf2.6 The ChoiceType add a ViewTransformer (ChoicesToBooleanArrayTransformer) so the condition is not met, and the $modelData is not converted into string. But in sf2.7 the addition of transformer was removed.

Possible solution

Verify if $modelData is not a boolean value like:

// dump($modelData)  ->  false
if (!$this->config->getViewTransformers() && !$this->config->getModelTransformers() && !is_bool($modelData) && is_scalar($modelData)) {
       $modelData = (string) $modelData;
}
// dump($modelData)  ->  false

Sorry for my bad English!!!

I apply the fix of @manuelj555 and now works as expected!

Same issue here, and @manuelj555 fix is working fine!
So I hope a PR based on that will be opened soon :)

Ping @webmozart @stof

Sadly it is not fixed in 2.7.1, the PR is planned for 2.8...

I have same problem. Temporally fix them with option choice_value as:

'choice_value' => function ($currentChoiceKey) {
    return $currentChoiceKey ? 'true' : 'false';
}

For anyone struggling with the same issue, this is the code that worked for me:

            'choices' => array(
                'Yes' => true,
                'No' => false
            ),
            'choices_as_values' => true,
            'choice_value' => function ($currentChoiceKey) {
                return $currentChoiceKey ? 'true' : 'false';
            },

Could we have a fix for the 2.7 branch ? I'm facing the same problem. When I go on an edit form, all false values are not preselected on radios.

True value (1) : "oui" is selected because 1 in bdd

image

False value (0) : "non" should be selected because 0 in bdd

image

Code :

->add('showIdentity', 'choice', array(
                'choices' => array(
                    true => 'Oui',
                    false => 'Non'
                ),
                'multiple' => false,
                'expanded' => true
            ))

This is happening since the upgrade to 2.7.0 (and is still an issue on 2.7.1), on all my projects.

@webmozart Looks like this is a BC break for 2.7. Is it something we can patch for 2.7?

I'm facing the same problem. I have many forms with 'yes'/'no' choice and cannot find a solution to fix this bug.

:+1: for 2.7 patch

:+1: for 2.7 patch. This should be documented as a BC break with a suggested solution at the very least.

:+1: for 2.7 patch. Unfortunately there wasn't fixed in version 2.7.2

for 2.7 patch!

@fabpot this BC break is there since 2.7.0 and it's getting annoying.
Nothing is really planned for 2.7? The PR was refused and postponed to 2.8.

I would be more than happy to merge a PR that fixes this regression.

I've been taking a look at this, and stof on his PR comment nails the point: PHP allow only integers or string as array keys. And eventually only strings/integers can be used as the view (HTML form) value.

Without going into all details, the model data and choices data will go through different paths, that will end up in different times for casting. No way it equals after, no matter you use choices_as_values or not (and that second part is definitely an issue way beyond the scope of this BC-break: you can't pre-set a checkbox with model data).

At the moment, at least a warning should be dropped into the Changelog, and then looking forward tackling the second issue.

This code posted by @MJBGO will not work as he want when having 0 or 1 in the DB, but this is totally expected:

->add('showIdentity', 'choice', array(
                'choices' => array(
                    true => 'Oui',
                    false => 'Non'
                ),
                'multiple' => false,
                'expanded' => true
            ))

This is because the actual code being executed is the following

->add('showIdentity', 'choice', array(
                'choices' => array(
                    '1' => 'Oui',
                    '' => 'Non'
                ),
                'multiple' => false,
                'expanded' => true
            ))

And so 0 would indeed not matched the casted false value.

What I cannot understand is how this code could have work on older versions, as @MJBGO seems to imply it did.

This workaround seems to be working as expected for a radio choice with a boolean property that can be null if unselected:

$builder->add('exampleProperty', 'choice', array(
    'choices' => array(
        'Positive choice' => true,
        'Negative choice' => false,
    ),
    'choices_as_values' => true,
    'choice_value' => function ($choiceKey) {
        if (null === $choiceKey) {
            return null;
        }

        // cast to string after testing for null,
        // as both null and false cast to an empty string
        $stringChoiceKey = (string) $choiceKey;

        // true casts to '1'
        if ('1' === $stringChoiceKey) {
            return 'true';
        }

        // false casts to an empty string
        if ('' === $stringChoiceKey) {
            return 'false';
        }

        throw new \Exception('Unexpected choice key: ' . $choiceKey);
    },
    'expanded' => true,
    'multiple' => false,
));

The issue was that the boolean values were being converted to strings: "1" for true and "" (empty) for both false and null.

:+1: for 2.7 patch

@webmozart do you have an idea to improve the way choices are dealt with to avoid this issue with false and string ?

i have the same problem :(

The only correct way (to my opinion) to tackle this issue is to use a Model Transformer to transform the data, an example:


// src/AppBundle/Form/DataTransformer/BooleanModelTransformer.php

namespace AppBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class BooleanModelTransformer implements DataTransformerInterface
{
    public function transform($boolVal)
    {
        if ($boolVal == null) {
            return '';
        }       
        return $boolVal === true ? '1' : '0';       
    }

    public function reverseTransform($textVal)
    {
        if ($textVal == '') {
            return null;
        }       
        return $textVal == '1' ? true : false;      
    }
}

// src/AppBundle/Form/Type/ChoiceTypeWithBooleanFix.php

namespace AppBundle\Form\Type;

use AppBundle\Form\DataTransformer\BooleanModelTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class ChoiceTypeWithBooleanFix extends AbstractType
{

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addModelTransformer(new BooleanModelTransformer());
        parent::buildForm($builder, $options);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'boolean_choice';
    }

    /**
     * {@inheritdoc}
     */
    public function getParent()
    {
        return 'choice';
    }

}

Now you can use '0' and '1' as keys in the 'choices' option.

For 2.7 patch ! :+1:
Thanks

:+1:

:+1: Same problem here. The label Bug should be added to this ticket. Using @ruudk easy-patch worked for me. Thanks man.

In my opinion, the string cast in Form class should be avoided if possible. It is really strange to have data transformed without a transformer; couldn't we just map the cases where cast to string is required?

Facing same problem on 2.7.1. @ruudk patch fixed problem for me. :+1:

We have news about it? 2.8 BETA 1 is out and this still doesn't work

ping @webmozart

There originally was a reason for the string cast: When you set a field without any transformers to a non-string value, submitting a value through the form _will_ updated it to a string. So the string cast is there to make the model format of the input and the output consistent.

I never considered back then that a data transformer could do the conversion. So this definitely does not make sense anymore, but we also cannot remove it. I think the best solution is to add an empty transformer.

I'll fix this once https://github.com/symfony/symfony/pull/16681 is merged. Otherwise there are too many merge conflicts.

@webmozart #16681 is now merged

My temporary fix based on @elzekool idea:

// your form
->add("example", "choice", [
    "choices" => ["no", "yes"],
    "expanded" => true,
    "multiple" => false
])

// src/AppBundle/Form/Extension/BooleanTypeExtension.php
namespace AppBundle\Form\Extension;

use AppBundle\Form\DataTransformer\BooleanTransformer;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

class BooleanTypeExtension extends AbstractTypeExtension
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        if($options["expanded"] && !$options["multiple"] && array_keys($options["choices"]) == [false, true]) {
            $builder->addModelTransformer(new BooleanTransformer());
        }
        parent::buildForm($builder, $options);
    }

    public function getExtendedType()
    {
        return ChoiceType::class;
    }
}

// src/AppBundle/Form/DataTransformer/BooleanTransformer.php
namespace AppBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class BooleanTransformer implements DataTransformerInterface
{
    public function transform($value)
    {
        return $value === null ? '' : $value;
    }

    public function reverseTransform($value)
    {
        return $value;
    }
}

+1.

I think #17760 should fix this one.

To be sure it will, you can try this workaround :

'choice_value' => function ($choice) {
    return false === $choice ? '0' : (string) $choice;
}

Fix works! :+1:

ping @core23 can you please check the fix?

Ps.: I'm not sure if this has to be considered. When using the PRE_SUBMIT event and checking the value for the boolean field you will still get 0 or 1, not the boolean strings:

$builder->addEventListener(
    FormEvents::PRE_SUBMIT,
    function(FormEvent $event) {
        $data = $event->getData();
        $form = $event->getForm();

        if (isset($data['peerReview']) && $data['peerReview'] == '1') {

@webdevilopers actually without the fix, you get an empty string for false and null but '1' for true.

So this is kind of a fix expected too IMO.

:+1:

I find this a serious BC break, and the title doesn't do the issue justice. It seems to break for me when setting the "data" of a choice field, for any choice value other than a string. See this comment.

I've got a regression on this issue after updating to 2.8.6 when submitting a form with a choice field.

Expected argument of type "null or string", "boolean" given

@rvanlaak Using empty_data? If so, you should use the string representation of the submitted choice (numeric index, or "0" or "1" for strict boolean).
Examples:

'choices' => array(
    'Yes' => true,
    'No' => false,
),
'choices_as_values' => true,
'empty_data' => '0' // false by default string

or

'choices' => array(
    'Yes' => true,
    'No' => false,
    'Undecided' => null,
),
'choices_as_values' => true,
'empty_data' => '1' // false by numeric index

or

'choices' => array(
    'Yes' => true,
    'No' => false,
    'Undecided' => null,
),
'choices_as_values' => true,
'choice_value' => function ($choice) {
    if (null === $choice) {
        return 'null';
    }

    return $choice ? 'true' : 'false';
},
'empty_data' => 'false' // false by custom value

Hope that helps.

I'll try to validate if that's the case, because that would mean I've missed one of the upgrade notices on that? :wink:

@rvanlaak see https://github.com/symfony/symfony/pull/18462 for a similar topic. Cheers!

@rvanlaak unfortunately Symfony docs are not very helpful in this area - its noted once and the docs say this is "only necessary for view layer" (or some other nonsense).

@MichaelMackus there are some works in progress, see https://github.com/symfony/symfony-docs/issues/6340 and https://github.com/symfony/symfony-docs/pull/6393.
Feedbacks are welcome!

Thank you @HeahDude - much appreciated and the docs will be much better after merge. This threw me off for hours before trying to figure out the intended functionality, and not to mention the hours of implications involved throughout our symfony apps after upgrading to 2.8. Good docs will help a lot going forward.

Run into this error with SF2.8.6
In my CustomRadioType I had to change the empty_data to null instead of false:

        $resolver->setDefaults(array(
            'expanded' => true,
            'multiple' => false,
            'horizontal' => false,
            'empty_value' => false,
            'empty_data' => false, // caused exception
            'required' => true,
            'by_reference' => false,
            'inline'  => true,
            'choices' => array(
                1 => 'Yes',
                0 => 'No'
            )
        ));

to

        $resolver->setDefaults(array(
            'expanded' => true,
            'multiple' => false,
            'horizontal' => false,
            'empty_value' => false,
            'empty_data' => null, // working with 2.8.6
            'required' => true,
            'by_reference' => false,
            'inline'  => true,
            'choices' => array(
                1 => 'Yes',
                0 => 'No'
            )
        ));

@kimwue You may try the choices_as_values option.

My form field with 0/1 values does not work too on symfony 2.7.20:

            ->add('refresh', 'choice', [
                'choices'           => [
                    'yes' => 1,
                    'no'  => 0,
                ],
                'choices_as_values' => true,
                'empty_data'        => 0,
                'required'          => false,
            ])

Returns this:

{"errors":{"refresh":["This value is not valid."]}}

But if I replace 0 with the string '0' – everything works fine. Magic!

@Aliance The empty_data expects a value that is of the same format as the view data. The view data however is treating the number as strings. Please take a look at symfony/symfony-docs#6265 which is an attempt to make this more clear in the documentation and feel free to leave a comment there.

Was this page helpful?
0 / 5 - 0 ratings