Villermen Villermen - 1 year ago 54
PHP Question

Turn off validation for form type if checkbox is checked in Symfony

I'm trying to create an address form with a payment and shipping address. When a checkbox on the shipping address is checked, I want to skip form validation from occurring on that address.

I have created the form type below with a toggle option that will display and deal with the checkbox, however the form is still validated even when checked.

Symfony has documentation on how to implement such a form, and even though I almost have the exact same code, validation is not turned off when checked. I am not using validation groups, so I just disable the default group to disable validation on the entity.

The

AddressType
building a form for the
Address
class (which has annotation constraints on certain fields like
NotBlank
and
Callback
).

class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options["toggle"]) {
$builder->add("toggle", CheckboxType::class, [
"mapped" => false,
"required" => false,
"label" => $options["toggle"]
]);
}

$builder
->add("name", TextType::class, [
"required" => !$options["toggle"]
])
->add("address", TextType::class, [
"required" => !$options["toggle"]
])
->add("zipcode", TextType::class, [
"label" => "Postcode",
"required" => !$options["toggle"]
])
->add("city", TextType::class, [
"required" => !$options["toggle"]
])
->add("countryCode", ChoiceType::class, [
"choices" => Address::COUNTRY_CODES
]);
}

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
"toggle" => false,
"data_class" => Address::class,
"validation_groups" => function(FormInterface $form) {
if ($form->has("toggle") && $form->get("toggle")->getData() === true) {
return [];
}

return ["Default"];
}
]);

$resolver->setAllowedTypes("toggle", ["bool", "string"]);
}
}


I use the type like this:

$addressForm = $this
->createFormBuilder([
"paymentAddress" => $paymentAddress,
"shippingAddress" => $shippingAddress
])
->add("paymentAddress", AddressType::class, [
"label" => false
])
->add("shippingAddress", AddressType::class, [
"label" => false,
"toggle" => "Use payment address"
])
->add("submit", SubmitType::class, [
])
->getForm();


I've been going over this for a few hours now but I am not able to deduce why validation is not being turned off, and I am not willing to botch up a form over this little detail.

Why is validation for the
AddressType
not turned off by the closure in configureOptions? If this is just not how it works, what would be a better solution to accomplish partially turning off validation in a tidy way?

EDIT: Even if setting
"validation_groups" => false
in the defaults, in the children created in the builder, or in the usage of the form, validation will still happen. It does not have to do with the closure. Every online resource, including Symfony's own resource states that it should work though...

Answer Source

[...] so I just disable the default group to disable validation on the entity.

Assuming that Address properties & constraints look like this:

/**
 * @Assert\NotBlank()
 */
private $name;

// etc.

The "validator" assumes that these properties will be evaluated with Default group, because he always considers empty validation_groups (e.g. return [];) as ['Default'] (hence validation is not turned off when checked):

https://symfony.com/doc/current/validation/groups.html: If no groups are specified, all constraints that belong to the group Default will be applied.

A solution to accomplish partially turning off validation in a tidy way

Should there be many ways to achieve it, but I show you two of them:

  1. If none data_class is set to the root form, then Form group is available to validate this level only:

    $addressForm = $this
        ->createFormBuilder([...], [
            'validation_groups' => 'Form', // <--- set
        ])
    

    Next, in configureOptions method, set Address group as default and Form group if "toggle" is checked, also adds Valid() constraint for cascade validation:

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            // ...
            "validation_groups" => function(FormInterface $form) {
                if ($form->has("toggle") && $form->get("toggle")->getData() === true) {
                    return ['Form']; // <--- set
                }
    
                return ['Address']; // <--- set
            },
            'constraints' => [new Assert\Valid()], // <--- set
        ]);
    }
    

    That means, on submit with toggle off: Form and Address groups are applied to address fields, else, only Form group is applied.

  2. (Another way) In Address class add "Required" group to all constraints, it avoids validate these properties with Default group:

    /**
     * @Assert\NotBlank(groups={"Required"}) // <--- set
     */
    private $name;
    
    // etc.
    

    Next, in configureOptions method set Required as default group and Default if toggle is checked:

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            // ...
            "validation_groups" => function(FormInterface $form) {
                if ($form->has("toggle") && $form->get("toggle")->getData() === true) {
                    return ['Default']; // <--- set
                }
    
                return ['Required']; // <--- set
            },
            'constraints' => [new Assert\Valid()], // <--- set
        ]);
    }
    

    In this case, on submit with toggle off: Default and Required groups are applied to address fields, else only Default group, skipping thus the required fields.

Nested forms that contain objects disconnected from the root object can be validated by setting the constraints option to new Valid().

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download