Simple Symfony 4 Contact Form


Let's create a simple contact form using Symfony 4.

We'll capture the visitors name, email address, date of birth, and also their message.

We're going to take whatever information the visitor provides, and we will email a copy to ourselves using Gmail. You can use a different email provider, and we'll cover how to do so along the way.

Lastly we'll show a nice message to the user to let them know their message was sent.

There's lots to cover, so let's get started.

Generation Game

First up we are going to generate a new Controller class.

We've already covered how to do this, so simply here's the command we will need:

bin/console make:controller ContactController

 created: src/Controller/ContactController.php

  Success! 

 Next: Open your new controller class and add some pages!

That gets us a controller class.

We could create our form right inside that controller.

I'm going to advise against this. Even though you can, in the real world, I can't think of any time I actually would.

It's a better choice (in my opinion) to create your forms in dedicated form classes. These form classes always end with the suffix of Type. I've never found out why. If you know why we use the Type suffix when working with forms in Symfony then do please let me know.

As our form is for the purposes of "contact", I'm going with the imaginative ContactType for my form class name.

I'll make use of the bin/console make:form command to save myself a bit of typing:

bin/console make:form

 The name of the form class (e.g. GrumpyGnomeType):
 > ContactType

 created: src/Form/ContactType.php

  Success! 

 Next: Add fields to your form and start using it.
 Find the documentation at https://symfony.com/doc/current/forms.html

Jolly good, that's all we need to generate.

Now, let's start the real work.

Building Our Contact Form

I'd be lying if I said I found Symfony's Forms to be 'easy' for the first several months of my time working with them.

I don't want to put you off. But I also want to be completely truthful and say that I struggled to learn how to use forms in Symfony for a good long time. My struggles with things like forms, and security, and some of the more complex parts of Symfony are what led me to create this site in the first place.

But, all that said, we are going to ease in gently, and I'm going to help you every step of the way.

First, let's open up the src/Form/ContactType.php class that we just generated, and see what we have to work with:

<?php

namespace App\Form;

use App\Entity\Contact;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('field_name')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            // uncomment if you want to bind to a class
            //'data_class' => Contact::class,
        ]);
    }
}

Ok, firstly, the generator expects us to be working with an entity. In this case, a Contact entity. We haven't covered entities just yet.

From a very high level an entity is an object with some form of unique identifier. We'll get onto to working with entities in the next series. For now, we don't need to worry about them, so you can remove the line:

use App\Entity\Contact;

Probably the most confusing part of working with Symfony forms for me was in the fact that forms are classes. They are not HTML forms that I knew, and I'd like to have say I loved, but maybe tolerated would be a better description.

The important thing to understand is that we define forms in code. When we use a form in a Twig template, behind the scenes Symfony's Form Component will take care of turning the form into HTML for us.

This means that we need to add all our form's fields to the buildForm method, and the Form Component will take care of the rest.

If we want a form field to be simple text input then all we need to do is provide the field's name.

If we wanted a textarea, however, or a select box, then things get a little more interesting.

That's exactly why we're adding a simple text input for our visitor's name.

And we'll have a more involved input type="datetime-local" for our visitor's birth date.

And lastly a textarea for the visitor's message.

You can see the large range of form field types that Symfony provides for our use. From that list we will use TextType, TextareaType, and DateTimeType.

Let's add these to our buildForm implementation now, by removing the existing add('field_name') call, and adding three of our own:

<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
+ use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
+ use Symfony\Component\Form\Extension\Core\Type\EmailType;
+ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
-           ->add('field_name')
+           ->add('name')
+           ->add('from', EmailType::class)
+           ->add('dateOfBirth', DateTimeType::class)
+           ->add('message', TextareaType::class)
        ;
    }

Notice how the call to add('name') omits the second argument, whereas dateOfBirth, from, and message both explicitly specify the form field type?

If we don't specify a second argument, the form field type is inferred to be of type TextType::class. This means that it will be rendered out as a standard HTML input.

We could be explicit:

+ use Symfony\Component\Form\Extension\Core\Type\TextType;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
-            ->add('name')
+            ->add('name', TextType::class)

The outcome is the same.

One other point to note: I used the field name of dateOfBirth, not date_of_birth, or some other combination. This is the typical convention. And if your form is backed by an entity then the properties on your entities would likely follow this naming convention also. As a heads up, your form fields tend to map 1:1 to your entity property names. You can change this setup using the property_path option, which is a bit more advanced and typically not needed. One example where I have needed this is when accepting snake_case form data on a JSON API. Again, more advanced, just interesting trivia at this point.

The field names set will be used as the names of the generated HTML input field's name, and id and label properties. We'll take a look at this when we render out the form view.

That's actually enough to get our form to where we need it to be.

At this point we've specified the field names we'd like, and what each of those fields should render as when output as HTML. We don't need to worry ourselves about the way this ContactType form class is converted to HTML, this will be taken care of for us.

Creating The Form

We've created our ContactType form class, although currently it isn't used anywhere.

Earlier we created a new controller class (ContactController) with a auto-generated method: index. We are going to use our ContactType in that controller method, and see how Symfony converts the form class into HTML.

Here's our starting point:

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class ContactController extends Controller
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
    }
}

I'm going to make the same two changes we made to our WelcomeController:

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
-use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\HttpFoundation\Response;

-class ContactController extends Controller
+class ContactController extends AbstractController
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
    }
}

As our ContactType is a plain old PHP class, in order to use that class we will need a new instance of that class. However, we won't directly instantiate a new ContactType() in our code.

When first looking at Twig a few videos ago, we saw how we could call $this->render(...) in order to convert a Twig template into HTML that can be sent back as the body of the Response to our site visitor. We gain access to the $this->render(...) method because render is defined on Symfony's ControllerTrait.php, and our controllers - ContactController, and WelcomeController - both extend Controller. This is standard inheritance at work, nothing specific to Symfony. This is much easier to demonstrate in the video, so please watch if at all unsure.

One of the other methods available to us aside from render is createForm. We can call this method like $this->createForm(...). Let's quickly look at the method signature:

    /**
     * Creates and returns a Form instance from the type of the form.
     *
     * @final since version 3.4
     */
    protected function createForm(string $type, $data = null, array $options = array()): FormInterface

Again, much like render, the only mandatory argument is the form $type that we wish to use. We need to pass in a class name here. We already know that class name, because we just generated our form class - it's ContactType::class. By adding ::class as a suffix to any class name, behind the scenes PHP will expand out the class name to its fully qualified name automatically. In other words, when we type ContactType::class, what PHP really sees is App\Form\ContactType, which is the namespace and the class name in one.

The second and third arguments to createForm are optional. We won't need them here. As a heads up, you would use the second argument to populate your form with data before it renders. This is useful in situations where you have existing data that you'd like to show back to your visitors so they can edit / update - maybe their user profile, or similar. We will get to this and more in the next series.

Ok, let's create our form:

<?php

namespace App\Controller;

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

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

If you were to visit /contact now, not much would happen. We aren't rendering anything just yet.

But even so, we have technically created our form. Make sure to include the use statement or this won't work.

To make our lives easier, let's add this new route in to our Navbar (inside templates/base.html.twig):

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>{% block title %}Let's Explore Symfony 4{% endblock %}</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css"
          integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy"
          crossorigin="anonymous">
    <link rel="stylesheet" href="{{ asset('css/custom-style.css') }}" />
    {% block stylesheets %}{% endblock %}
</head>

<body>

<header>
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <div class="container">

            <a class="navbar-brand" href="{{ path('welcome') }}">Home</a>

            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ path('hello_page') }}">Hello Page</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ path('contact') }}">Contact</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
</header>

<main role="main" class="container main">
    {% block body %}{% endblock %}
</main><!-- /.container -->

{% block javascripts %}{% endblock %}
</body>
</html>

Note the addition under the ul section in the navbar.

Ok, let's get this form displayed.

Displaying The Contact Form

In order to display the form we need to do two things:

  • Create a 'view' of the form
  • Render the form view from a Twig template

Neither of these are too taxing from our point of view. However, behind the scenes there is a lot of 'stuff' happening to make this process work.

For any of this to work, we will need new a Twig template. I'm going to suggest following what we've already covered and create a new contact subdirectory under templates, into which I will create a index.html.twig file:

mkdir templates/contact
touch templates/contact/index.html.twig

In to this new template file I'm adding:

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

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

{% block body %}

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

{% endblock %}

The only new part of this is the use of the form tags.

These are special Twig functions that take our form view and create a fully working HTML form representation. You can read more about what each function does in the official docs.

There's one tiny tweak I've made from how the official docs display this:

I've changed form to our_form.

I'll explain why in just a moment. For now, that's the Twig side of things (almost) completed.

Hooking Up Our ContactController

As we've covered already, Twig doesn't just magically know what we're up too. We must explicitly tell it.

It stands that in order for Twig to know about our Form, we must tell Twig about our Form.

We do this by passing in our form as a variable.

This is just like passing in any other variable to our template.

Let's start by adding in the return statement with a call to $this->render(...):

<?php

namespace App\Controller;

use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

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

        return $this->render('contact/index.html.twig');
    }
}

So far, nothing new.

Next, we will pass in our form to the contact/index.html.twig template.

I mentioned above that the Symfony docs use the following example for their Twig template form_start / form_widget / form_end functions:

{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

If we used this same code, we would need to pass in our form's view as a variable named form.

Instead, we're using e.g. { form_start(our_form) }}. This means we need to use the variable named our_form.

I do this to illustrate that variable names can be anything you like. And if you have multiple forms per page, they can't all be called form :)

<?php

namespace App\Controller;

use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

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

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

Head to your site on http://127.0.0.1:8000/contact, and what happens?

"Type error: Argument 1 passed to Symfony\Component\Form\FormRenderer::renderBlock() must be an instance of Symfony\Component\Form\FormView, instance of Symfony\Component\Form\Form given, called in /home/chris/Development/lets-explore-symfony-4/var/cache/dev/twig/27/277e6d2e85202798cec1fe0a745133d5c7f3925b92540e36b8d1e3395807f520.php on line 72"

You're going to have a slightly different path, but the gist is the same: This doesn't work.

What? Why am I even showing you this?

Because this mistake is super easy to make!

We've passed in a Form object, and these form_... functions all expect a FormView! But of course :)

Ok, the fix here is easy, and essential:

<?php

namespace App\Controller;

use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

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

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

Now refresh the page and hoorah, we see a form. It's an ugly form, but it is a form.

There's one problem though:

We don't have a submit button.

It is possible to add a submit button to our form type. However, in doing so, we impose some restrictions on our form that limit its re-usability. This becomes more evident on more complex forms.

Imagine we have a form that allows us to add a new Product. And because this form is defined as a separate, standalone class - much like our ContactType form, we should be able to re-use that very same form for Editing / Updating a Product, too. All the form fields are the same in either case, right?

Yes. Except if we use the SubmitType form field type to add a button to our form class.

If we add a button then we need to give it a label. Maybe we label the button as "Create".

But then in our Edit / Update form variant, we'd like to label the button as "Update", instead.

What do we do now? Well, as ever there's a bunch of solutions. And it's super easy to over think this.

If we go a little higher level here, we may conclude that the problem we face is presentational.

Presentation = Twig.

Wouldn't it be better to put the specific tweaks in the presentation layer - e.g. the relevant Twig template - rather than tie a form to a specific type of action ('Create', 'Update')?

I think so. And so do the best practices.

Let's update our template to add in a simple submit button:

{% 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" />

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

{% endblock %}

We're half way there, let's continue on with our Symfony 4 Contact form implementation in the very next video.

Code For This Course

Get the code for this course.

Episodes