Sending Email with Symfony 4


We've got out contact form in code, and we can see our form on the /contact page. When we submit our contact form we can see the submitted data dumped out on the web debug toolbar.

Now, let's turn this into an email, and display a Flash Message to let the visitor know their message was sent.

Sending An Email With Symfony 4

There's a great page in the documentation that describes how to send an email both using Gmail, and with a "cloud service". Everyone loves a good cloud, don't they? I know I sure do.

Sending an email with Symfony 4 is actually remarkably easy.

And like I say, there's a good page in the docs that walks you through it step by step.

What we're going to do is use this as an opportunity to go a little geekier. And by geekier, I of course mean a little more real world. To the nerd cave!

<?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(Request $request)
+   public function index(Request $request, \Swift_Mailer $mailer)
    {
        $form = $this->createForm(ContactType::class);

        $form->handleRequest($request);

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

            $contactFormData = $form->getData();

+            $message = (new \Swift_Message('You Got Mail!'))
+               ->setFrom($contactFormData['from'])
+               ->setTo('our.own.real@email.address')
+               ->setBody(
+                   $contactFormData['message'],
+                   'text/plain'
+               )
+           ;
+
+           $mailer->send($message);
+
+           return $this->redirectToRoute('contact');
        }

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

A fair amount of change.

We've had to inject the \Swift_Mailer service inside our index function arguments, as this is the service that Symfony uses (out of the box, at least) to send emails.

We then created a new $message object by new'ing up an instance of \Swift_Message, filled in all the appropriate fields, and used that $mailer to send our $message.

We return $this->redirectToRoute('contact'); to force a full page refresh, upon successfully sending a mail. This clears off our form by forcing a 'hard reload' of the page.

As we saw in the previous video, our $contactFormData is an array containing any / all submitted form data. Given that we're inside the $form->isSubmitted() conditional, we can access key / values from that array and use them to populate our email.

We haven't used all the information we submitted from the form. We will do though, don't worry.

We're almost ready to send. We just need to hook up our Gmail credentials. In your .env file in your project root:

- MAILER_URL=null://localhost
+ MAILER_URL=gmail://username:password@localhost

Depending on how Gmail is configured for you, you may either immediately get the email you expect, or a warning saying you're trying to do "bad guy stuff". I paraphrase, but that's the gist. If so, follow the appropriate guidance / precautions and work around the problem. Again, the official docs will guide you through here.

Getting Flashy

Whilst I'm not yet happy with the implementation we just added, we're going to keep on trucking and get another quick win.

Immediately after sending an email I want to show the visitor a nice Flash Message to say: "hey, that went well".

We can even use some nice Bootstrap styling to add extra levels of style to proceedings. How sweet.

A flash message is a one time message that lasts for the duration of the current request. An example might be to show a failure message if the previous login attempt was unsuccessful. Or a success message if the user successfully updated their password. They shouldn't see this message every time they browse to a page, just this one time when they attempted a task, and it succeeded or failed.

I've done a previous video on a Beginners Guide To Symfony Flash Messages before, so will go quickly here. That video is for Symfony 3, but this is largely the same for Symfony 4.

We will create a new template to store our Flash Message display block.

First, I'll create a new empty template file:

cd {project_root}
touch templates/flash_messages.html.twig

I put this new empty template file in the root of the templates directory as there's no controller in our project that directly uses this template. You can, of course, store it anywhere. You could even merge it with your existing base.html.twig, if you wish.

The contents of templates/flash_messages.html.twig are as follows:

{% block flash_messages %}
    {% for type, messages in app.session.flashbag.all() %}
        {% for message in messages %}
            <div class="alert alert-{{ type }} alert-dismissible" role="alert">
                <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
                {{ message | raw }}
            </div>
        {% endfor %}
    {% endfor %}
{% endblock %}

This can be copy / pasted directly, between any project that uses Bootstrap.

The nice thing about this template is it works generically for any 'level' of flash message: success, warning,error`, and so on.

Next, we do need to include this flash_messages.html.twig template in our base.html.twig template. I'm going to include this inside the main tag, but above the block body tag:

<main role="main" class="container main">
    {% include 'flash_messages.html.twig' %}

    {% block body %}{% endblock %}
</main>

The nice part about of flash messages template is that if we have no flash message set (and we'll get on to how to do that in a second), there is no extra HTML added to our page. Only if we have one or more flash messages set do we see the output.

With that in mind, let's add our first flash message right now.

We typically add flash messages from our controller actions. Much like how we have used $this->render(...) and $this->createForm(...) throughout this series, so too can we use $this->addFlash(...).

A quick look at the method signature for addFlash:

    /**
     * Adds a flash message to the current session for type.
     *
     * @throws \LogicException
     *
     * @final since version 3.4
     */
    protected function addFlash(string $type, string $message)

We see that addFlash takes two mandatory parameters.

The $type and $message can be any string. Anything you like.

That said, if you want the Bootstrap styles to apply, the $type needs to be one of the available alert levels:

  • primary
  • success
  • warning
  • dark

And so on.

Let's add this into our controller method:

<?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(Request $request, \Swift_Mailer $mailer)
    {
        $form = $this->createForm(ContactType::class);

        $form->handleRequest($request);

+       $this->addFlash('info', 'Some useful info');

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

            $contactFormData = $form->getData();

            $message = (new \Swift_Message('You Got Mail!'))
                ->setFrom($contactFormData['from'])
                ->setTo('christopherdmoss@gmail.com')
                ->setBody(
                    $contactFormData['message'],
                    'text/plain'
                )
            ;

            $mailer->send($message);

+           $this->addFlash('success', 'It sent!');

            return $this->redirectToRoute('contact');
        }

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

We've added two flash messages.

One will always display:

$this->addFlash('info', 'Some useful info');

The other will only display if we successfully submit the form:

$this->addFlash('success', 'It sent!');

Refresh your /contact page and try it out.

Notice that even though we used return $this->redirectToRoute('contact'); to force a full page reload after a successful form submission, our flash message survived and displays as expected.

Towards A Better Implementation

Even though what we have is working, I'm already thinking the index controller method is doing a little too much.

We have all the form handling, then mailing, and the flash messages. And that's before we've worried about displaying a page.

It might be nice to extract out the details of how we create this specific email, and also how we send emails, in to a separate service.

Knowing how to do this is really useful, and it's something you'll do quite a lot when working with Symfony.

We're starting to stray into murky waters. Advice given here, and in the Best Practices guidelines are (in my opinion) completely dependent on the size and scale of your project. If you're working on a small, 5 page largely static website, your requirements are going to be significantly different to those of a large JSON API, or complex internal business system.

Typically Symfony would be more likely used in larger, more dynamic applications, so I'm going to steer towards the sort of approach you may find on a bigger system. Please understand that the changes we are about to make in the next video are overkill for the real size and complexity of this tutorial application.

With that in mind, let's get on to that in the next video in this short series.

Code For This Course

Get the code for this course.

Episodes