06/11/2015
Adding a form error from a controller in Symfony
Every field in a Symfony form is a form object in its own right, so an error can be attached to one of them directly:
use Symfony\Component\Form\FormError;
$form->get('email')->addError(new FormError('That address is already registered.'));
It renders wherever that field's form_errors() is, which for a
form_row() is immediately under the input. The user sees it against the
thing that is wrong, which is the entire point of doing it this way rather
than with a flash message.
Attaching it to the form rather than a field
Some failures are not any one field's fault:
$form->addError(new FormError('Those dates overlap a booking you already have.'));
That one lands in form_errors(form) — the call with the form itself
rather than a field. If your template does not have that call, the error
is added and never appears anywhere, which is a confusing half-hour.
Where this goes in the controller
After handleRequest(), and inside the valid branch, because a form that
already failed validation does not need more errors on it:
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($userRepository->findOneByEmail($form->get('email')->getData())) {
$form->get('email')->addError(new FormError('That address is already registered.'));
} else {
// save, redirect
}
}
return $this->render('signup.html.twig', ['form' => $form]);
Adding an error inside that branch does not retroactively make
isValid() return false — it has already returned. The else is what
keeps the save from happening, and forgetting it is how you get a form
that shows an error and saves the record anyway.
When a constraint is the better answer
Reach for addError() when the check needs something the form has no
access to — the current user, an external service, another record. For
anything that is a property of the data itself, a constraint belongs on
the entity, where it also applies to the API endpoint and the console
command that touch the same object:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[UniqueEntity(fields: ['email'], message: 'That address is already registered.')]
class User
{
#[Assert\NotBlank]
#[Assert\Email]
private string $email;
}
The uniqueness example above is the honest case for this: UniqueEntity
does that job properly, including the race the manual version loses. The
controller version is what you write when the rule is genuinely about the
request rather than about the data.
The one that catches people
An error added to a field that the template never renders is invisible,
and so is a form-level error in a template without form_errors(form).
Both submit, both fail, and the page comes back looking like nothing
happened. {{ form_rest(form) }} covers the first case; only an explicit
form_errors(form) covers the second.