Symfony 4 and Bootstrap 4


Towards the end of the previous video we had our basic Symfony 4 contact form displaying in our browser. Even though we have used Bootstrap 4, our form fields do not use any of the Bootstrap form field CSS classes, and so the actual output of our form is really ugly. Fortunately, Symfony yet again has us covered here.

As ever with programming, there are a couple of ways we can enable Bootstrap 4 form theming in our Symfony projects. We can either:

  • Enable a Bootstrap 4 form theme for a particular template
  • Enable Bootstrap 4 form theming for our entire project

This was a cool new feature added in Symfony 3.4. As a side note, it's always worth keeping a check on the Symfony blog as new and cool stuff is added to Symfony all the time, and aside from following the GitHub issues register, this is often the first place you will hear about new features.

Ok, I'm going to enable Bootstrap 4 form theming for my entire project. I've never had a situation where I only want form theming for a single form, so I'm leaning towards my personal experience here. Change up as you see fit.

# config/packages/twig.yaml

twig:
    # other stuff here
    form_themes: ['bootstrap_4_horizontal_layout.html.twig']

My preference is for the horizontal form layout. Use a variant if you prefer.

Here's what we had:

symfony-4-contact-form-unstyled

Here's the non-horizontal layout:

symfony-4-contact-form-bootstrap-4-layout

And this is what I'm going with - the horizontal layout:

symfony-4-contact-form-bootstrap-4-horizontal-layou

What you can see at the bottom of each screen in red here is that Symfony is complaining about some missing translations. I'm really not concerned about translations at this stage. Translations are a separate topic that aren't related to what we're doing here. So I'm going to disable the translator:

# config/packages/framework.yaml

framework:
    # other stuff here

    translator:
      enabled: false

It might be nice to make the Send button be all green and attractive, rather than the default dishwater gray.

We can do this easily enough by updating the submit button in our contact/index.html.twig template to use the styles provided by Bootstrap 4:

{% extends 'base.html.twig' %}

{% block title %}Our Contact Form{% endblock title %}

{% block body %}

    <div>
        {{ form_start(our_form) }}
        {{ form_widget(our_form) }}

-       <input type="submit" value="Send" />
+       <input type="submit" value="Send" class="btn btn-success" />

        {{ form_end(our_form) }}
    </div>

{% endblock %}

This makes the button itself look nice, but we still need to do a little more to 'properly' align the button with the rest of the input fields.

By applying the Bootstrap Twig form_themes configuration earlier, any fields on our form get nicely aligned and styled when they are rendered.

However, as we have added our own submit button into the template, this won't magically / automatically get the Bootstrap styling applied. This is why we had to add in the custom btn btn-success classes directly.

In order to keep this button aligned and looking nice, we also need to use the same structure of "two and ten" columns that Symfony is using as part of its bootstrap layout.

This is much easier to see / understand if we refresh the page, and take a peek at the generated HTML.

Notice how e.g. the "Name" field is transformed into the following HTML:

<div class="form-group row"><label class="col-form-label col-sm-2 form-control-label required" for="contact_name">Name</label><div class="col-sm-10"><input type="text" id="contact_name" name="contact[name]" required="required" class="form-control" /></div>

Yikes.

That's not so easy to read, for us humans at least.

I'm going to tidy this up a touch (watch the video to see how you can quickly tidy up HTML in PhpStorm):

<div class="form-group row">
    <label class="col-form-label col-sm-2 form-control-label required"
           for="contact_name">
       Name
   </label>

    <div class="col-sm-10">
        <input type="text"
               id="contact_name"
               name="contact[name]"
               required="required"
               class="form-control"
       />
   </div>
</div>

The interesting part in this instance is that the label has the CSS class of col-sm-2, and the input is wrapped in a div with a class of col-sm-10.

Bootstrap uses a 12 column layout.

Here the label is taking up two columns. And the input takes up the remaining 10 columns.

In order for our "Send" / submit button to align like all the other elements, we must replicate this layout. We don't need a label for our button, so we will have an empty two column div, and then put our submit button in the remaining ten column area:

<div class="form-group row">
    <div class="col-sm-2"></div>
    <div class="col-sm-10">
        <input type="submit" value="Send" class="btn btn-success" />
    </div>
</div>

Give your page a refresh and things look much nicer indeed.

Aren't You A Little -Short- Young To Be A Stormtrooper?

You may have noticed that our 'date of birth' drop down isn't that useful currently. Well, it may be if you're under five years old, or born in the future. For the rest of us, the available year range of +/- 5 years from today isn't so good.

When adding our form fields in the previous video we saw how the first argument is the field name. The second argument is the field type. And we didn't mention the third argument.

Let's quickly take a look at the add method signature (ctrl+click on add in PhpStorm):

    /**
     * Adds a new field to this group. A field must have a unique name within
     * the group. Otherwise the existing field is overwritten.
     *
     * If you add a nested group, this group should also be represented in the
     * object hierarchy.
     *
     * @param string|int|FormBuilderInterface $child
     * @param string|null                     $type
     * @param array                           $options
     *
     * @return self
     */
    public function add($child, $type = null, array $options = array());

The third argument is an array of options. In truth, when starting with Symfony way back when, the concept of 'options' confused the heck out of me. They shouldn't have. Options are just things we can use to add stuff to the output HTML.

You can see a list of the available options in the docs for each form field type. Different form fields have different available options.

Try It Yourself

Take a moment now to expand your knowledge. Try changing the form options for the DateTimeType to allow a wider range of years.

Likewise from as a label isn't the most obvious that we expect a person's email address here.

Try changing the from field's label text to something a little more descriptive.

Leave a comment below sharing your experiences.

We'll look at a solution to these problems in the next video.

Handling Contact Form Submission

If you've tried to submit the contact form then you will have found out that ... not a lot appears to happen.

Well, some interesting things do happen, even if they aren't immediately obvious.

Firstly, all of the form fields are required. Kind of.

Try submitting the form with empty fields. What do you see?

html-5-validation-errors

That's right. You can't submit the form with empty fields. You'll get a validation error.

These are HTML5 validations. They are happening in the visitors / your browser. They are not server side / happening in PHP. This is a very important concept: validation is nice to have on the front end, but essential on the back end. In other words, you need to validate what the visitor submitted, and you cannot simply rely on HTML5 / front end / browser validation to guarantee the submitted data is valid.

We'll covered validation in more detail in the next series.

For now, so long as you fill in all the fields with something - anything - then you can submit the form.

Only, once you submit the form, the page just reloads and the fields are empty again.

This shouldn't be that surprising.

We haven't told Symfony how to handle our form submission.

Let's add this functionality in, and then cover what we just did:

<?php

namespace App\Controller;

use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+ use Symfony\Component\HttpFoundation\Request;

class ContactController extends AbstractController
{
    /**
     * @Route("/contact", name="contact")
     */
-   public function index()
+   public function index(Request $request)
    {
        $form = $this->createForm(ContactType::class);

+       $form->handleRequest($request);
+
+       if ($form->isSubmitted() && $form->isValid()) {
+
+           $contactFormData = $form->getData();
+
+           dump($contactFormData);
+
+           // do something interesting here
+       }

        return $this->render('contact/index.html.twig', [
            'our_form' => $form->createView(),
        ]);
    }
}

Before we look at what is happening here, as soon as we have added this, when we submit the form we get some new output on the web debug toolbar - the cross hair icon.

When we hover over this we can see the data that just got submitted. Notice how the dateOfBirth isn't a string, it's been transformed into an instance of PHP's \DateTime object? That's quite nice.

symfony-4-contact-form-submission-dump.

You can click the cross hair icon, or any of the other icons, and be taken to the full feature profiler. I recommend doing that right now, and having a nosy about. There's loads of good stuff in there, and this is a hugely useful tool during development.

Dissecting The Form Submission

In order to work with a form submission, we need to know what information got sent (technically POSTed) in via the current request.

Fortunately, as we have already covered, Symfony offers us the Request object, and this object comes with all kinds of helpful methods and properties for understanding, and working with our site visitor's current request.

To get access to this Request object we can inject it into our current controller method. In order to make this work, don't forget the use statement at the top of your controller class.

This Request object is injected whether we are loading the "contact" page for the first time, or refreshing the page, or POSTing / submitting the form.

It's worth pointing out that when we do submit our form, the form's default action is to send any form data back into this same controller method. You could submit your form to a different controller method. This is how some other languages and frameworks handle this process. Symfony, and most PHP frameworks tend to use one method for displaying, and processing the form submission.

Simply injecting the Request isn't going to be enough for Symfony to determine that the form has been submitted.

We must explicitly tell Symfony to look at the incoming request.

This is why we start by calling $form->handleRequest($request);.

If the page has simply been loaded (i.e. a GET request), then Symfony understands that no, the form has not been submitted.

In this case the outcome of $form->isSubmitted() would return false.

If the form was determined to have been submitted (i.e. a POST request) then the internal process of form submission takes place. This is the responsibility of Symfony's form component. It's not something you need to overly concern yourself with on a day-to-day basis.

In this case the outcome of $form->isSubmitted() would return true.

Array vs Entity

What's interesting to note here is that depending on whether we're using a form with an entity, or a 'standalone' form as in our case, the process here is slightly different.

Think of an entity as simply a PHP class that contains properties that match up with the fields we have set on our form. There's more to it than this, but from a high level this is good enough for the moment.

If we were using a form with an entity then the internal process of form submission will set the submitted data into the appropriate properties on that entity. In other words, we end up with a populated PHP class / entity that we can, for example, save off to our database.

As we aren't using a form with an entity, then we will instead be working with an array.

You can see this for yourself either by running the dump statement in your own code, or examining the screenshot above. The dump output does indeed show that our $contactFormData variable contains a 4 key array, where each key is the name of the associated form field from our ContactType.

Do It Yourself

Add in a dump($request); statement at the very top of your index method. Then refresh your page.

After examining the dumped output, complete the form and submit the data. Now examine the output of your dump statement again.

What's changed?

Validation

Validation is a powerful and extremely useful part of using Symfony.

We're going to cover validation in much greater detail in the next series when we start working with entities / the database.

For the moment, our form is always going to be considered valid.

This means $form->isValid() is always going to be true.

As a reminder: the validation we saw earlier where we had to fill in some data to submit the form was front end / HTML5 validation. This is completely unrelated to this PHP / back end validation check we do here. This check is far more important in the real world.

Inside our conditional (if ($form->isSubmitted() && $form->isValid()) {) right now all we are doing is pulling the data back as an array, and then dumping the data to our debug toolbar output.

Can We Send An Email Already?

We have all the data we need at this point. The next step is to send off an email to ourselves with the info we just captured.

Also, it would be nice to let the site visitor / end user know their message was sent.

We will get on to that in the very next video.

Code For This Course

Get the code for this course.

Episodes