HTML Emails with Twig

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Every email can contain content in two formats, or "parts": a "text" part and an HTML part. And an email can contain just the text part, just the HTML part or both. Of course, these days, most email clients support HTML, so that's the format you really need to focus on. But there are still some situations where having a text version is useful - so we won't completely forget about text. You'll see what I mean.

The email we just sent did not contain the HTML "part" - only the text version. How do we also include an HTML version of the content? Back in the controller, you can almost guess how: copy the ->text(...) line, delete the semicolon, paste and change the method to html(). It's that simple! To make it fancier, put an <h1> around this.

... lines 1 - 17
class SecurityController extends AbstractController
{
... lines 20 - 47
public function register(MailerInterface $mailer, Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 50 - 52
if ($form->isSubmitted() && $form->isValid()) {
... lines 54 - 73
$email = (new Email())
... lines 75 - 77
->text("Nice to meet you {$user->getFirstName()}! ❤️")
->html("<h1>Nice to meet you {$user->getFirstName()}! ❤️</h1>");
... lines 80 - 88
}
... lines 90 - 93
}
}

This email now has two "parts": a text part and an HTML part. The user's email client will choose which to show, usually HTML. Let's see what this looks like in Mailtrap. Click back to get to the registration form again, change the email address, add a password and... register! No errors! Check out Mailtrap.

Yeah! This time we have an HTML version! One of the things I love about Mailtrap is how easily we can see the original HTML source, the text or the rendered HTML.

MIME: The "Multipart" Behind Emails

Or, you can check what the "Raw" message looks like. Ooooo, nerdy. It turns out that what an email looks like under-the-hood is almost exactly what an HTTP response looks like that's returned from our app: it has some headers on top, like To, From and Subject, and content below. But, the content is a bit different. Normally, our app returns an HTTP response whose content is probably HTML or JSON. But this email's content contains two formats all at once: HTML and text.

Check out the Content-Type header: it's multipart/alternative and then has this weird boundary string - _=_symfony - then some random numbers and letters. Below, we can see the content: the plain-text version of the email on top and the text/html version below that. That weird boundary string is placed between these two... and literally acts as a separator: it's how the email client knows where the "text" content stops and the next "part" of the message - the HTML part - begins. Isn't that cool? I mean, if this isn't a hot topic for your next dinner party, I don't know what is.

This is what the Symfony's Mime component helps us build. I mean, sheesh, this is ugly. But all we had to do was use the text() method to add text content and the html() method to add HTML content.

Using Twig

So... as simple as this Email was to build, we're not really going to put HTML right inside of our controller. We have our standards! Normally, when we need to write some HTML, we put that in a Twig template. When you need HTML for an email, we'll do the exact same thing. Mailer's integration with Twig is awesome.

First, if you downloaded the course code, you should have a tutorial/ directory with a welcome.html.twig template file inside. Open up the templates/ directory. To organize our email-related templates, let's create a new sub-directory called email/. Then, paste the welcome.html.twig template inside.

Say hello to our fancy new templates/email/welcome.html.twig file. This is a full HTML page with embedded styling via a <style> tag... and... nothing else interesting: it's 100% static. This %name% thing I added here isn't a variable: it's just a reminder of something that we need to make dynamic later.

But first, let's use this! As soon as your email needs to leverage a Twig template, you need to change from the Email class to TemplatedEmail.

Hold Command or Ctrl and click that class to jump into it. Ah, this TemplatedEmail class extends the normal Email: we're really still using the same class as before, but with a few extra methods related to templates. Let's use one of these. Remove both the html() and text() calls - you'll see why in a minute - and replace them with ->htmlTemplate() and then the normal path to the template: email/welcome.html.twig.

... lines 1 - 8
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
... lines 10 - 18
class SecurityController extends AbstractController
{
... lines 21 - 48
public function register(MailerInterface $mailer, Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 51 - 53
if ($form->isSubmitted() && $form->isValid()) {
... lines 55 - 74
$email = (new TemplatedEmail())
... lines 76 - 77
->subject('Welcome to the Space Bar!')
->htmlTemplate('email/welcome.html.twig');
... lines 80 - 88
}
... lines 90 - 93
}
}

And... that's it! Before we try this, let's make a few things in the template dynamic, like the URLs and the image path. But, there's an important thing to remember with emails: paths must always be absolute. That's next.

Leave a comment!

This tutorial is built on Symfony 4.3, but will work well with Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.110.11
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
        "knplabs/knp-time-bundle": "^1.8", // v1.9.1
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.8.2
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.1.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.4.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.3.4
        "symfony/console": "^4.0", // v4.3.4
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "^4.0", // v4.3.4
        "symfony/framework-bundle": "^4.0", // v4.3.4
        "symfony/mailer": "4.3.*", // v4.3.4
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.3.4
        "symfony/sendgrid-mailer": "4.3.*", // v4.3.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.3.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "^4.0", // v4.3.4
        "symfony/web-server-bundle": "^4.0", // v4.3.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.6.2
        "symfony/yaml": "^4.0", // v4.3.4
        "twig/cssinliner-extra": "^2.12", // v2.12.0
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/browser-kit": "4.3.*", // v4.3.5
        "symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
        "symfony/dotenv": "^4.0", // v4.3.4
        "symfony/maker-bundle": "^1.0", // v1.13.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.3.4
    }
}