From 0d09f16df5676306a35bc29d5bbac32a8b490815 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 30 Jun 2025 13:24:03 +0200 Subject: [PATCH] [Form][Validator] Merge all articles about using validation groups in forms --- _build/redirection_map | 3 + form/button_based_validation.rst | 36 ---- form/data_based_validation.rst | 72 -------- form/validation_group_service_resolver.rst | 58 ------- form/validation_groups.rst | 164 +++++++++++++++--- forms.rst | 2 - .../types/options/validation_groups.rst.inc | 57 +----- reference/forms/types/submit.rst | 24 +-- 8 files changed, 155 insertions(+), 261 deletions(-) delete mode 100644 form/button_based_validation.rst delete mode 100644 form/data_based_validation.rst delete mode 100644 form/validation_group_service_resolver.rst diff --git a/_build/redirection_map b/_build/redirection_map index 896c58a3022..ec088ad2209 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -570,3 +570,6 @@ /components/serializer /serializer /serializer/custom_encoder /serializer/encoders#serializer-custom-encoder /components/string /string +/form/button_based_validation /form/validation_groups +/form/data_based_validation /form/validation_groups +/form/validation_group_service_resolver /form/validation_groups diff --git a/form/button_based_validation.rst b/form/button_based_validation.rst deleted file mode 100644 index 47f2673b079..00000000000 --- a/form/button_based_validation.rst +++ /dev/null @@ -1,36 +0,0 @@ -How to Choose Validation Groups Based on the Clicked Button -=========================================================== - -When your form contains multiple submit buttons, you can change the validation -group depending on which button is used to submit the form. For example, -consider a form in a wizard that lets you advance to the next step or go back -to the previous step. Also assume that when returning to the previous step, -the data of the form should be saved, but not validated. - -First, we need to add the two buttons to the form:: - - $form = $this->createFormBuilder($task) - // ... - ->add('nextStep', SubmitType::class) - ->add('previousStep', SubmitType::class) - ->getForm(); - -Then, we configure the button for returning to the previous step to run -specific validation groups. In this example, we want it to suppress validation, -so we set its ``validation_groups`` option to false:: - - $form = $this->createFormBuilder($task) - // ... - ->add('previousStep', SubmitType::class, [ - 'validation_groups' => false, - ]) - ->getForm(); - -Now the form will skip your validation constraints. It will still validate -basic integrity constraints, such as checking whether an uploaded file was too -large or whether you tried to submit text in a number field. - -.. seealso:: - - To see how to use a service to resolve ``validation_groups`` dynamically - read the :doc:`/form/validation_group_service_resolver` article. diff --git a/form/data_based_validation.rst b/form/data_based_validation.rst deleted file mode 100644 index b01bea10b16..00000000000 --- a/form/data_based_validation.rst +++ /dev/null @@ -1,72 +0,0 @@ -How to Choose Validation Groups Based on the Submitted Data -=========================================================== - -If you need some advanced logic to determine the validation groups (e.g. -based on submitted data), you can set the ``validation_groups`` option -to an array callback:: - - use App\Entity\Client; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'validation_groups' => [ - Client::class, - 'determineValidationGroups', - ], - ]); - } - -This will call the static method ``determineValidationGroups()`` on the -``Client`` class after the form is submitted, but before validation is -invoked. The Form object is passed as an argument to that method (see next -example). You can also define whole logic inline by using a ``Closure``:: - - use App\Entity\Client; - use Symfony\Component\Form\FormInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'validation_groups' => function (FormInterface $form): array { - $data = $form->getData(); - - if (Client::TYPE_PERSON == $data->getType()) { - return ['person']; - } - - return ['company']; - }, - ]); - } - -Using the ``validation_groups`` option overrides the default validation -group which is being used. If you want to validate the default constraints -of the entity as well you have to adjust the option as follows:: - - use App\Entity\Client; - use Symfony\Component\Form\FormInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'validation_groups' => function (FormInterface $form): array { - $data = $form->getData(); - - if (Client::TYPE_PERSON == $data->getType()) { - return ['Default', 'person']; - } - - return ['Default', 'company']; - }, - ]); - } - -You can find more information about how the validation groups and the default constraints -work in the article about :doc:`validation groups `. diff --git a/form/validation_group_service_resolver.rst b/form/validation_group_service_resolver.rst deleted file mode 100644 index 82a6f65d6ec..00000000000 --- a/form/validation_group_service_resolver.rst +++ /dev/null @@ -1,58 +0,0 @@ -How to Dynamically Configure Form Validation Groups -=================================================== - -Sometimes you need advanced logic to determine the validation groups. If they -can't be determined by a callback, you can use a service. Create a service -that implements ``__invoke()`` which accepts a ``FormInterface`` as a -parameter:: - - // src/Validation/ValidationGroupResolver.php - namespace App\Validation; - - use Symfony\Component\Form\FormInterface; - - class ValidationGroupResolver - { - public function __construct( - private object $service1, - private object $service2, - ) { - } - - public function __invoke(FormInterface $form): array - { - $groups = []; - - // ... determine which groups to apply and return an array - - return $groups; - } - } - -Then in your form, inject the resolver and set it as the ``validation_groups``:: - - // src/Form/MyClassType.php; - namespace App\Form; - - use App\Validation\ValidationGroupResolver; - use Symfony\Component\Form\AbstractType; - use Symfony\Component\OptionsResolver\OptionsResolver; - - class MyClassType extends AbstractType - { - public function __construct( - private ValidationGroupResolver $groupResolver, - ) { - } - - // ... - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'validation_groups' => $this->groupResolver, - ]); - } - } - -This will result in the form validator invoking your group resolver to set the -validation groups returned when validating. diff --git a/form/validation_groups.rst b/form/validation_groups.rst index 4addc1ba1a7..3157ef7bc5b 100644 --- a/form/validation_groups.rst +++ b/form/validation_groups.rst @@ -1,39 +1,163 @@ -How to Define the Validation Groups to Use -========================================== +Configuring Validation Groups in Forms +====================================== -Validation Groups ------------------ +If the object handled in your form uses :doc:`validation groups `, +you need to specify which validation group(s) the form should apply. -If your object takes advantage of :doc:`validation groups `, -you'll need to specify which validation group(s) your form should use. Pass -this as an option when :ref:`creating forms in controllers `:: +To define them when :ref:`creating forms in classes `, +use the ``configureOptions()`` method:: + + use Symfony\Component\OptionsResolver\OptionsResolver; + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // ... + 'validation_groups' => ['registration'], + ]); + } + +When :ref:`creating forms in controllers `, pass +it as a form option:: $form = $this->createFormBuilder($user, [ 'validation_groups' => ['registration'], ])->add(/* ... */); -When :ref:`creating forms in classes `, add the -following to the ``configureOptions()`` method:: +In both cases, *only* the ``registration`` group will be used to validate the +object. To apply the ``registration`` group *and* all constraints not in any +other group, add the special ``Default`` group:: + + [ + // ... + 'validation_groups' => ['Default', 'registration'], + ] +.. note:: + + You can use any name for your validation groups. Symfony recommends using + "lower snake case" (e.g. ``foo_bar``), while automatically generated + groups use "UpperCamelCase" (e.g. ``Default``, ``SomeClassName``). + +Choosing Validation Groups Based on the Clicked Button +------------------------------------------------------ + +When your form has :doc:`multiple submit buttons `, you +can change the validation group based on the clicked button. For example, in a +multi-step form like the following, you might want to skip validation when +returning to a previous step:: + + $form = $this->createFormBuilder($task) + // ... + ->add('nextStep', SubmitType::class) + ->add('previousStep', SubmitType::class) + ->getForm(); + +To do so, configure the validation groups of the ``previousStep`` button to +``false``, which is a special value that skips validation:: + + $form = $this->createFormBuilder($task) + // ... + ->add('previousStep', SubmitType::class, [ + 'validation_groups' => false, + ]) + ->getForm(); + +Now the form will skip your validation constraints when that button is clicked. +It will still validate basic integrity constraints, such as checking whether an +uploaded file was too large or whether you tried to submit text in a number field. + +Choosing Validation Groups Based on Submitted Data +-------------------------------------------------- + +To determine validation groups dynamically based on submitted data, use a +callback. This is called after the form is submitted, but before validation is +invoked. The callback receives the form object as its first argument:: + + use App\Entity\Client; + use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - // ... - 'validation_groups' => ['registration'], + 'validation_groups' => function (FormInterface $form): array { + $data = $form->getData(); + + if (Client::TYPE_PERSON === $data->getType()) { + return ['Default', 'person']; + } + + return ['Default', 'company']; + }, ]); } -In both of these cases, *only* the ``registration`` validation group will -be used to validate the underlying object. To apply the ``registration`` -group *and* all constraints that are not in a group, use:: +.. note:: + + Adding ``Default`` to the list of validation groups is common but not mandatory. + See the main :doc:`article about validation groups ` to + learn more about validation groups and the default constraints. - 'validation_groups' => ['Default', 'registration'] +You can also pass a static class method callback:: -.. note:: + 'validation_groups' => [Client::class, 'determineValidationGroups'] + +Choosing Validation Groups via a Service +---------------------------------------- + +If validation group logic requires services or can't fit in a closure, use a +dedicated validation group resolver service. The class of this service must +be invokable and receives the form object as its first argument:: + + // src/Validation/ValidationGroupResolver.php + namespace App\Validation; + + use Symfony\Component\Form\FormInterface; + + class ValidationGroupResolver + { + public function __construct( + private object $service1, + private object $service2, + ) { + } + + public function __invoke(FormInterface $form): array + { + $groups = []; + + // ... determine which groups to return + + return $groups; + } + } + +Then use the service in your form type:: + + namespace App\Form; + + use App\Validation\ValidationGroupResolver; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class MyClassType extends AbstractType + { + public function __construct( + private ValidationGroupResolver $groupResolver, + ) { + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'validation_groups' => $this->groupResolver, + ]); + } + } + +Learn More +---------- - You can choose any name for your validation groups, but Symfony recommends - using "lower snake case" names (e.g. ``foo_bar``) in contrast with the - automatic validation groups created by Symfony, which use "upper camel case" - (e.g. ``Default``, ``SomeClassName``). +For more information about how validation groups work, see +:doc:`/validation/groups`. diff --git a/forms.rst b/forms.rst index 38006169cdb..bea93edb25d 100644 --- a/forms.rst +++ b/forms.rst @@ -1003,8 +1003,6 @@ Validation: :maxdepth: 1 /form/validation_groups - /form/validation_group_service_resolver - /form/button_based_validation /form/disabling_validation Misc.: diff --git a/reference/forms/types/options/validation_groups.rst.inc b/reference/forms/types/options/validation_groups.rst.inc index 1f5c9a597a3..6957bf203a3 100644 --- a/reference/forms/types/options/validation_groups.rst.inc +++ b/reference/forms/types/options/validation_groups.rst.inc @@ -1,59 +1,14 @@ ``validation_groups`` ~~~~~~~~~~~~~~~~~~~~~ -**type**: ``array``, ``string``, ``callable``, :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequence` or ``null`` **default**: ``null`` +**type**: ``array``, ``string``, ``callable``, :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequence`, or ``null`` **default**: ``null`` -This option is only valid on the root form and is used to specify which -groups will be used by the validator. +This option is only valid on the root form. It specifies which validation groups +will be used by the validator. -For ``null`` the validator will just use the ``Default`` group. - -If you specify the groups as an array or string they will be used by the -validator as they are:: - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'validation_groups' => 'Registration', - ]); - } - -This is equivalent to passing the group as array:: - - 'validation_groups' => ['Registration'], - -The form's data will be :doc:`validated against all given groups `. - -If the validation groups depend on the form's data a callable may be passed to -the option. Symfony will then pass the form when calling it:: - - use Symfony\Component\Form\FormInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - // ... - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'validation_groups' => function (FormInterface $form): array { - $entity = $form->getData(); - - return $entity->isUser() ? ['User'] : ['Company']; - }, - ]); - } - -.. seealso:: - - You can read more about this in :doc:`/form/data_based_validation`. - -.. note:: - - When your form contains multiple submit buttons, you can change the - validation group depending on :doc:`which button is used ` - to submit the form. - - If you need advanced logic to determine the validation groups have - a look at :doc:`/form/validation_group_service_resolver`. +If set to ``null``, the validator will use only the ``Default`` group. For the +other possible values, see the main article about +:doc:`using validation groups in Symfony forms ` In some cases, you want to validate your groups step by step. To do this, you can pass a :class:`Symfony\\Component\\Validator\\Constraints\\GroupSequence` diff --git a/reference/forms/types/submit.rst b/reference/forms/types/submit.rst index 70fa429685a..5d863fbe8b4 100644 --- a/reference/forms/types/submit.rst +++ b/reference/forms/types/submit.rst @@ -102,28 +102,8 @@ validation_groups **type**: ``array`` **default**: ``null`` When your form contains multiple submit buttons, you can change the validation -group based on the button which was used to submit the form. Imagine a registration -form wizard with buttons to go to the previous or the next step:: - - use Symfony\Component\Form\Extension\Core\Type\SubmitType; - // ... - - $form = $this->createFormBuilder($user) - ->add('previousStep', SubmitType::class, [ - 'validation_groups' => false, - ]) - ->add('nextStep', SubmitType::class, [ - 'validation_groups' => ['Registration'], - ]) - ->getForm(); - -The special ``false`` ensures that no validation is performed when the previous -step button is clicked. When the second button is clicked, all constraints -from the "Registration" are validated. - -.. seealso:: - - You can read more about this in :doc:`/form/data_based_validation`. +group based on the clicked button. Read the article about +:doc:`using validation groups in Symfony forms `. Form Variables --------------