Skip to content
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

Constraints on unmapped fields are not validated as expected when using validation groups #58407

Closed
pauljura opened this issue Sep 27, 2024 · 5 comments

Comments

@pauljura
Copy link

Symfony version(s) affected

7.1.5

Description

I have a user registration form, that is bound to a Doctrine user entity. In addition to the usual fields like first name, email, etc, I also have an unmapped field for a verification code. This code is not persisted with the user, but I want it to be validated when the form is submitted. I am also using a validation group with this form, because the user entity has many other fields that can be validated later, but are initially blank during registration.

I noticed that the constraints on the unmapped verification code field do not get checked as expected, even if I set the 'validation_groups' attribute to match the 'validation_groups' on the form. They will only get checked if I set 'validation_groups' to 'Default'.

This is unexpected and seems like a bug to me.

How to reproduce

User entity

class User
{
    #[Assert\NotBlank(groups: ['Default', 'Registration'])]
    private string $firstName;

    #[Assert\NotBlank(groups: ['Default', 'Registration'])]
    #[Assert\Email(groups: ['Default', 'Registration'])]
    private string $email;

    // also other properties with other validation groups...
}

My registration form:

class RegistrationType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => User::class,
            'validation_groups' => ['Registration'], // Note validation group set here
        ]);
    }

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('firstName', TextType::class, [
                'required' => true,
                'label' => 'First Name',
            ])
            ->add('email', TextType::class, [
                'required' => true,
                'label' => 'Email',
            ])
            ->add('code', TextType::class, [
                'required' => true,
                'label' => 'Verification Code',
                'mapped' => false,

                // This MUST be 'Default' or else no constraints get validated.
                // If this is ['Registration'] (to match what is in configureOptions), then no constraints are validated.
                // If 'validation_groups' is omitted, then no constraints are validated.
                'validation_groups' => ['Default'],
                'constraints' => [
                    new Assert\NotBlank(),
                    new Assert\Regex(...),
                    new Assert\Callback(...),
                    // and other constraints, etc
                ],
            ])
        ;
    }
}

Possible Solution

No response

Additional Context

No response

@xabbuh
Copy link
Member

xabbuh commented Sep 27, 2024

Can you create a small example application that allows to reproduce your issue?

@eliasfernandez
Copy link

Looks like it works fine to me:

IndexController

<?php
namespace App\Controller;

use App\Entity\User;
use App\Form\Type\UserType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class IndexController extends AbstractController
{
    #[Route('/')]
    public function index(Request $request): Response
    {
        $user = new User();
        $user->setFirstName($request->get('firstName'));
        $user->setEmail($request->get('email'));
        $user->setTest($request->get('test'));
        $form = $this->createForm(UserType::class, $user);
        if ($request->isMethod('POST')) {
            $form->submit($request->get('user'));
        }
        return $this->render('index.html.twig', [
            'form' => $form,
        ]);
    }
}

User

<?php

namespace App\Entity;

use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[Assert\NotBlank(groups: ['Default', 'Registration'])]
    #[ORM\Column(length: 255)]
    private ?string $firstName = null;

    #[Assert\NotBlank(groups: ['Default', 'Registration'])]
    #[Assert\Email(groups: ['Default', 'Registration'])]
    #[ORM\Column(length: 255)]
    private ?string $email = null;
    #[Assert\NotBlank(groups: ['Registration'])]
    #[Assert\Length(min: 3, groups: ['Registration'])]
    private ?string $test = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getFirstName(): ?string
    {
        return $this->firstName;
    }

    public function setFirstName(?string $firstName): static
    {
        $this->firstName = $firstName;

        return $this;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(?string $email): static
    {
        $this->email = $email;

        return $this;
    }

    public function getTest(): ?string
    {
        return $this->test;
    }

    public function setTest(?string $test): static
    {
        $this->test = $test;

        return $this;
    }
}

UserType

<?php
namespace App\Form\Type;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;

class UserType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => User::class,
            'validation_groups' => ['Registration'], // Note validation group set here
        ]);
    }

    /**
     * @param array<string,mixed> $options
     */
    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        $view->vars['attr'] = array_merge($view->vars['attr'], ['novalidate' => 'novalidate']);
    }
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('firstName', TextType::class, )
            ->add('email', EmailType::class, ['constraints' => new Email()])
            ->add('test', TextType::class)
            ->add('save', SubmitType::class)
        ;
    }
}

index.html.twig

{{ form_start(form) }}
    <div class="my-custom-class-for-errors">
        {{ form_errors(form) }}
    </div>

    <div class="row">
        <div class="col">
            {{ form_row(form.firstName) }}
        </div>
        <div class="col" id="some-custom-id">
            {{ form_row(form.email) }}
        </div>
        <div class="col" id="some-custom-id">
            {{ form_row(form.test) }}
        </div>
    </div>
{{ form_end(form) }}

You can check it here eliasfernandez@4d75337 ``

@pauljura
Copy link
Author

pauljura commented Oct 8, 2024

Here's a small example application that demonstrates the issue:
pauljura/issue-58407@34d9473

The form should validate that the code is not blank, but if you submit a blank code then there is no error.

Then if you set validation_groups => ['Default'] and try to submit a blank code, then the validator works and you get an error.

@xabbuh
Copy link
Member

xabbuh commented Oct 8, 2024

This is the expected behaviour. The validation_groups option is only evaluated for the root form:

This option is only valid on the root form and is used to specify which groups will be used by the validator.

If the form is validated in the Registration group, you will have to make sure that your constraints are part of this group like this:

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
        ->add('code', TextType::class, [
            'required' => false,
            'label' => 'Verification Code',
            'mapped' => false,
            'constraints' => [
                new Assert\NotBlank(groups: ['Registration']),
            ],
        ])
    ;
}

Closing here as there is no bug to fix. Thank you for understanding.

@xabbuh xabbuh closed this as not planned Won't fix, can't repro, duplicate, stale Oct 8, 2024
@pauljura
Copy link
Author

pauljura commented Oct 8, 2024

Right, ok, my bad. Thanks for the explanation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants