Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

<!doctype html>
<html lang="en">
... lines 4 - 54
<div class="body">
<div class="container">
<div class="header text-center">
<a href="#homepage">
<img src="path/to/logo.png" class="logo" alt="SpaceBar Logo">
<div class="content">
<h1 class="text-center">Nice to meet you %name%!</h1>
<p class="block">
Welcome to <strong>the Space Bar</strong>, we can't wait to read what you have to write.
Get started on your first article and connect with the space bar community.
... lines 70 - 83
... lines 85 - 93

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!')
... 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!

Login or Register to join the conversation
Eric Avatar
Eric Avatar Eric | posted 6 months ago | edited

For some reason changes in my Twig template don't make it into the mail. Any guesses what might cause this issue? I already deleted the cache manually and with cache:clear but still the same. I'm pretty clueless why the template is not changing.

UPDATE: Ok. With this GitHub discussion I was able to answer the question myself... It is simply not supported in a worker context. So if templated mails are sent via a worker, one has to restart the worker to see the changed templates.


Hey @Eric

Yes, whenever you change any piece of code in your application, you have to restart the worker, otherwise it will execute the old code forever


Default user avatar

Pretty awesome!
For some reason, the HTML is coming up looking very weird and with lots of "=" in Outlook2013

Default user avatar

It looks like the Content-Transfer-Encoding is causing the issue. I changed it to 7bit (with swift mailer) and everything works. Is there a way to support 7zip in mailer? Or change something in my twig template so that quoted-printable works correctly?

Default user avatar
Default user avatar Dan | Dan | posted 4 years ago | edited

Andddd.... Another update! It looks like this is caused because MIME expects CRLF line endings and twig returns just \n.
This fixes it for me, but I am hoping there is a better method so that I can use htmlTemplate()

->html(preg_replace('/\R/', "\r\n", $this->twig->render('file', [])))

Default user avatar

Ok, the issue is will mailtrap, not symfony! I sent with Exim instead and works like a charm!
Hope this may help someone else.
Thanks for the wonderful guides!!


Lol - thanks for debugging yourself and posting the updates here Dan. I appreciate it :)

Cat in space

"Houston: no signs of life"
Start the conversation!

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
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "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.9", // v1.17.6
        "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