Symfony Forms For Designers


In this video we are going to look at how we can render our Symfony form fields by hand, and along the way, understand how we can replicate the Bootstrap 3 layout look and feel that comes with a fresh install of Symfony 3.

The form is one of the most useful components in all of Symfony. However, because it needs to be so flexible, it has a steeper learning curve than if there was only one set way to do any particular thing.

An example of this flexibility being a double edged sword would be in how we can apply styles to a form. But this doesn't just apply to styles, it also applies to form labels, custom html attributes, button text, and more.

Imagine that we need to add a custom style (making the field use Bootstrap 3's input-lg class) to our email form field input. Let's quickly look at two possible ways to achieve this, and then discuss when and why each might be appropriate.

Styling Using The Form Builder

We could add the required class right inside the form builder:

<?php

// src/AppBundle/Form/Type/ProductType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email', EmailType::class, [
                'attr' => [
                    'class' => 'input-lg'
                ]
            ])
            ->add('save', SubmitType::class)
        ;
    }

This works really nicely if we have a simple form widget rendering out our form, e.g.:

<!-- your twig form template -->

{% block body %}

    <h2>Symfony 3 Form Example</h2>

    {{ form(myForm) }}

{% endblock %}

Assuming we are using the Bootstrap 3 layout as covered in a previous video, we don't need to do anything extra to our form template to see the class appear, and the email input visually change as expected.

However, whilst this is really easy for us as developers, it has some drawbacks to consider.

Firstly, assuming our project grows and we have to start working with non-technical folk, we now need to have our designer(s) start updating .php files to make changes to the HTML. That's no so good, Al.

Secondly, what if we want to re-use this form somewhere else in our project? What if in that particular instance, the we need to make the field the default size, or some other size? What if we don't want or need any styling information at all? Yuck. We have tied our form to our styling, and it turns out, that's not so good after all.

In my experience, adding styles directly to the form is only useful if you have a very small project - maybe less than 5 pages in total... a brochure-style site.

Other than that, move your styles to a better place, such as...

Styling Inside a Twig Template

It makes more sense to put your styles inside a HTML-like environment. I say an environment like HTML because Twig templates aren't raw HTML. Twig gives us a templating language that compiles our HTML-like syntax down to PHP.

Personally, I like the Twig syntax, and it turns out that this syntax is extremely popular across multiple languages - see Jinja for Python's version, and Swig for the Node equivalent. This is nice as it means your designers have less of an argument when being told that there is still a little extra syntax that they must still learn. You can sell them on the fact that learning Twig also benefits if they work on Node, Python, or other projects. Well... possibly :)

Unfortunately, as mentioned, there is some unavoidable additional syntax for the designer(s) to learn.

Let's recreate our form output without using the form builder:

<!-- your twig form template -->

{% block body %}

    <h2>Symfony 3 Form Example</h2>

    {{ form_start(myForm) }}

    {{ form_errors(myForm) }}

    <div class="form-group">
        {{ form_label(myForm.email) }}
        <div class="col-sm-10">
            {{ form_widget(myForm.email, { 'attr': { 'class': 'input-lg' } }) }}
            {{ form_errors(myForm.email) }}
        </div>
    </div>

    {{ form_end(myForm) }}

{% endblock %}

Now, I wouldn't expect a designer to be able to write this form definition out from scratch. You, as a Symfony developer, will need to get them some of the way there.

The syntax is still tricky. It matches up just as we write it in the form builder version - the attr key is passed using parentheses instead of brackets, but it is still an array, which contains another array, with one key - class - with the input-lg value.

In other words, from a styling point of view:

{{ form_widget(myForm.email, { 'attr': { 'class': 'input-lg' } }) }}

Is equal too:

$builder->add('email', EmailType::class, [ 'attr' => [ 'class' => 'input-lg' ] ])

It absolutely will still confuse designers - to begin with - but from experience, it is much easier for designers to 'get' HTML-like templates, versus the "omg it's code, abort, abort!" viewpoint that some designers take. Of course, not all designers are created equal, your mileage may vary, etc etc.

Anyway, the point being here that we have now moved the styling, labels, button text elements, and whatever else that needs to be customised OUT of the code, and IN to the template. This means that individual instances of a form can define whatever styling, text, labels, etc that they need, all independantly, and all without mucking about with the form code. Good stuff.

The downside to this is that we need to manually render out each field of the form. But honestly, on any real project that uses Twig, this is pretty much an unavoidable necessity anyway.

Handling Form Errors

Inevitably, things will go wrong. Especially during development. I mean, we anticipate our end user's doing crazy stuff - that's why we use Form Validation.

Symfony is really helpful to us here. It will map our form errors back on to the respective form fields, and we can use the little form_errors helper to render out the error text so our end user can make the appropriate corrective action.

This is quite an easy one to miss if you are rendering out a field by hand. So make sure to include the form_errors widget for your particular field instance:

<div class="col-sm-10">
    {{ form_widget(myForm.email, { 'attr': { 'class': 'input-lg' } }) }}
    {{ form_errors(myForm.email) }}
</div>

Given that we are using the Bootstrap 3 horizontal layout in this series, it makes sense for me to keep my manually rendered form fields as closely in line with the original template as possible. If you are unsure on this, be sure to watch this video where we covered this in more detail.

We can also make use of a property from the Symfony form to determine if both the form in general, or a particular field is invalid, and add in an appropriate Bootstrap validation state CSS:

<div class="form-group {% if not myForm.email.vars.valid %}has-error{% endif %}">
    {{ form_label(myForm.email) }}
    <div class="col-sm-10">
        {{ form_widget(myForm.email, { 'attr': { 'class': 'input-lg' } }) }}
        {{ form_errors(myForm.email) }}
    </div>
</div>

This is nice, as it shows specific form field error right below / next to the invalid field, making it super easy for the end user to figure out what went wrong.

Sometimes, however - and especially during development - things might go a little more pear-shaped with your form. In this case, the inclusion of the catch-all {{ form_errors(myForm) }} helper is essential. Be sure to add this near the top of your form - just below form_start is a good place.

What this will do is catch any errors that don't map to a particular field. This might be that you have inadvertantly submitted a form with extra fields - particularly frustrating as your forms get larger / more dynamic.

With each passing version of Symfony, we have seen progressive improvements to the web debug toolbar, and now you can get an extremely useful debug output of your particular form submission whenever you are working in development mode. If you submit an invalid form, the little 'form' icon on the toolbar will change to red as the page re-renders, making it much easier to see something went wrong, and allow you to dig deeper into exactly what went wrong.

It can also be useful to dump out the entire form view by adding dump($form->createView()); inside your controller, something like:

    public function formAddExampleAction(Request $request)
    {
        $form = $this->createForm(ProductType::class, new Product());

        $form->handleRequest($request);

        dump($form->createView());

        if ($form->isSubmitted() && $form->isValid()) {
        // etc

Then when using the form, you can see a dump of what is happening internally with the form. This can be useful for picking out particular form vars, error states, and so on. Imagine how hard this used to be before the form helper on the web debug toolbar, and the easy use of the dump statement :)

Code For This Course

Get the code for this course.

Episodes