Accessing and Debugging Symfony Form Errors
Written by weaverryan, Leannapelham, bocharsky-bw, and ilithium
I recently saw an old post on StackOverflow called How to get form validation errors after binding the request to the form. It has a lot of answers, most of them are only partially right and a lot are outdated. So, I wanted to look at the right answer, and why it's that way :).
Tip
Debugging
First, if you're trying to figure out what errors you have and which field
each is attached to, you should use the
Form::getErrorsAsString()
method that was introduced in Symfony 2.1 (so a long time ago!). Use it temporarily
in a controller to see what's going on::
public function formAction(Request $request)
{
$form = $this->createForm(new ProductType());
$form->handleRequest($request);
if ($form->isValid()) {
// ... form handling
}
var_dump($form->getErrorsAsString());die;
$this->render(
'DemoBundle:Product:form.html.twig',
array('form' => $form->createView())
);
}
That's it. To make things even simpler, you also have the Form panel of the web debug toolbar in Symfony 2.4. So, debugging form errors
Why $form->getErrors() doesn't Work
Tip
As of Symfony 2.5, we have a new tool! $form->getErrors(true)
will
return all of the errors from all of the fields on your form.
Raise your hand virtually if you've tried doing this to debug a form::
public function formAction(Request $request)
{
$form = $this->createForm(new ProductType());
$form->handleRequest($request);
if ($form->isValid()) {
// ... form handling
}
var_dump($form->getErrors());die;
$this->render(
'DemoBundle:Product:form.html.twig',
array('form' => $form->createView())
);
}
What do you get? Almost definitely an empty array, even when the form has lots of errors. Yea, I've been there too.
The reason is that a form is actually more than just one Form
object:
it's a tree of Form
objects. Each field is represented by its own Form
object and the errors for that field are stored on it.
Assuming the form has name
and price
fields, how can we get the errors
for each field?
$globalErrors = $form->getErrors()
$nameErrors = $form['name']->getErrors();
$priceErrors = $form['price']->getErrors();
By saying $form['name']
, we get the Form
object that represents just
the name
field, which gives us access to just the errors attached to
that field. This means that there's no one spot to get all of the errors
on your entire form. Saying $form->getErrors()
gives you only "global"
errors, i.e. errors that aren't attached to any specific field (e.g. a CSRF token
failure).
Render all the Errors at the Top of your Form
Tip
As of Symfony 2.5, you can use $form->getErrors(true)
to get an array
of all the errors in your form. Yay!
One common question is how you might render all of your errors in one big list at the top of your form. Again, there's no one spot where you can get a big array of all of the errors, so you'd need to build it yourself::
// a simple, but incomplete (see below) way to collect all errors
$errors = array();
foreach ($form as $fieldName => $formField) {
// each field has an array of errors
$errors[$fieldName] = $formField->getErrors();
}
We can iterate over $form
(a Form
object) to get all of its fields.
And again, remember that each field ($formField
here), is also a Form
object, which is why we can call
Form::getErrors()
on each.
In reality, since a form can be many-levels deep, this solution isn't good enough. Fortunately, someone already posted a more complete one on Stack Overflow (see the 2.1 version).
From here, you can pass these into your template and render each. Of course,
you'll need to make sure that you don't call {{ form_errors() }}
on any
of your fields, since you're printing the errors manually (and remember that
form_row
calls form_errors
automatically).
Tip
Each field also has an error_bubbling option. If this is set to true
(it defaults to false
for most fields), then the error will "bubble"
and attach itself to the parent form. This means that if you set this
option to true
for every field, all errors would be attached to
the top-level Form object and could be rendered by calling {{ form_errors(form) }}
in Twig.
Accessing Errors Inside Twig
We can also do some magic in Twig with errors using magical things called form variables. These guys are absolutely fundamental to customizing how your forms render.
Tip
If you're new to form theming and variables and need to master them, check out Episode 4 of our Symfony series.
Normally, field errors are rendered in Twig by calling form_errors
on
each individual field:
{{ form_errors(form) }}
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
Tip
The form_row
function calls form_errors
internally.
Just like in the controller, the errors are attached to the individual fields
themselves. Hopefully it make sense now why form_errors(form)
renders global
errors and form_errors(form.name)
renders the errors attached to the
name field.
Tip
Once you're in Twig, each field (e.g. form
, form.name
) is an instance
of :symfonyclass:Symfony\\Component\\Form\\FormView
.
If you need to customize how the errors are rendered, you can always use form theming. But you can also do it by leverage form variables:
{{ form_errors(form) }}
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
{% if form.name.vars.errors|length > 0 %}
<ul class="form-errors name">
{% for error in form.name.vars.errors %}
{{ error }}
{% endfor %}
</ul>
{% endif %}
The key here is form.name.vars
: a strange array of "variables" that you
have access to on every field. One of the variables you have access to
is errors
, but there are many others, like label
and required
.
Each variable is normally used internally to render the field, but you can
use them manually if you need to:
<label for="form.name.vars.id">
{{ form.name.vars.label }} {{ form.name.vars.required ? '*' : '' }}
</label>
To find out what variables a field has, just dump them:
{{ dump(form.price.vars) }}
Now that you know about the magic form.vars
, you could also use it to render
all validation errors at the top of your form. This will only work for simple forms
(i.e. forms without nested fields), which most forms are:
{% set messages = [] %}
{% for child in form.children %}
{% for error in child.vars.errors %}
{% set messages = messages|merge([error.message]) %}
{% endfor %}
{% endfor %}
{% if messages|length %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
Tip
When you are form theming, these variables become accessible in your
form theme template as local variables inside the form blocks (e.g.
simply label
or id
).
Takeaways
The key lesson is this: each form is a big tree. The top level Form
has
children, each which is also a Form
object (or a FormView
object
when you're in Twig). If you want to access information about a field (is
it required? what are its errors?), you need to first get access to the child
form and go from there.
Learn More
Stuck on other Symfony topics or want to learn Symfony from the context of an actual project? Check out our Getting Started with Symfony Series and cut down on your learning curve!
8 Comments
Nice, simple solution thanks.
The only error it will not get is when the anti-csrf token is not valid anymore.
Anyways it helped me to send errors in JSON response.
The csrf token should be stored on the top-level form - so you can get it (and any other global errors) by adding this to the "list" of errors: $form->getErrors().
Cheers!
I spent a bit of time working out how to compile all of the errors into a block which could be displayed outside of each form element, so I'm hoping sharing this might save someone a bit of time. It's not the most efficient way of doing things, but should work for simple forms:
{% block form_errors %}
{% spaceless %}
{% for child in form.children %}
{% if child.vars.errors|length > 0 %}
{% for error in child.vars.errors %}
{{ error.message }}
{% endfor %}
{% endif %}
{% if child.children|length >0 %}
{{ form_errors(child) }}
{% endif %}
{% endfor %}
{% endspaceless %}
{% endblock %}
To enable this, override the form_row:
{% block form_row %}
{% spaceless %}
{{ form_label(form) }}
{{ form_widget(form) }}
{% endspaceless %}
{% endblock form_row %}
>>> As of Symfony 2.5, you can use $form->getErrors(true) to get an array of all the errors in your form. Yay!
This is awesome! I can't believe.
Thank you for your article Form::getErrorsAsString() helps me a lot.
I have a form with a collection of gameType, my gameType has a collection of opponentType. With error_bubbling => false I can put the error of the opponents level but I can't go deeper. Is there a limitation with error_bubbling ?
games:
0:
ERROR: This value is too long. It should have 20 characters or less.
opponents:
0:
score:
No errors
Hey @disqus_mL8NmUKLb8!
I don't fully understand what you want to do, but I will say that "error_bubbling" defaults to false, on almost every field by default. That makes sense, because with error_bubbling false, it says "don't attach this error to the parent, attach it to this exact field". So, if you're setting error_bubbling to false, you're probably not doing anything - that's likely already the default for that field.
Even with the collection field type, the errors should show up attached to the field that caused them. So for example, if your opponentType has a "name" field that must have less than 20 characters, that error should show up next to *that* field - it won't (by default) show up at a higher level. From your code, it looks like you somehow have an error on an individual field (e.g. error on opponentType), but that this error is somehow being attached at the game level (instead of at the opponents.0.name level). If that's the case - it's not error_bubbling to blame - you have something weird going on. It could be that the validation error is occurring on a field that is *not* in your form (e.g. you have an Assert\Length on a name field, but name is not in this form). In that case, the form framework is forced to attach the error to the parent field. Check into this.
If I haven't answered your question (I couldn't *quite* understand the situation, it has some complexities!), feel free to add more details and I'll do my best :).
Cheers!
I've added a PR to the symfony cookbook to improve the docs. Feel free to vote for it here: github.com/symfony/symfony-...
Thank you. I get all fields errors on next code:
```
$errors = array();
foreach ($form as $fieldName => $formField) {
foreach ($formField->getErrors(true) as $error) {
$errors[$fieldName] = $error->getMessage();
}
}