Email Context & the Magic "email" Variable

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.

When you set the HTML part of an email, Mailer helps out by creating the "text" version for us! It's not perfect... and we'll fix that soon... but... it's a nice start! If you did want to control this manually, in SecurityController, you could set this the text by calling either the text() method or textTemplate() to render a template that would only contain text.

Passing Variables (context)

In both cases - htmlTemplate() and textTemplate() - you're probably going to want to pass some data into the template to make the mail dynamic. The way to do this is not via a second argument to htmlTemplate(). Nope, to pass variables into the templates, call context() and give this an array. Let's pass a user variable set to the $user that was just registered.

... lines 1 - 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 - 79
->context([
'user' => $user,
]);
... lines 83 - 91
}
... lines 93 - 96
}
}

As soon as we do this, in welcome.html.twig, we can replace that weird %name% placeholder with {{ user.firstName }}... because user is a instance of our User entity... and it has a getFirstName() method on it.

... line 1
<html lang="en">
... lines 3 - 55
<body>
<div class="body">
<div class="container">
... lines 59 - 63
<div class="content">
<h1 class="text-center">Nice to meet you {{ user.firstName }}!</h1>
... lines 66 - 83
</div>
... lines 85 - 93
</div>
</div>
</body>
</html>

Let's try it! In your browser, go back one page, tweak the email, type a password, hit enter and then... there it is! Nice to meet you "Fox".

The Built-in "app" and "email" Variables

But wait, there's more! In addition to whatever variables you pass via context(), you also have access to exactly two other variables... absolutely free. What a deal!

The first one... we already know: it's the app variable... which every Twig template in Symfony can access. It's useful if you need read info from the session, the request, get the current user or a few other things.

The other variable that you magically get access to in all email templates is more interesting. It's called... emu. I mean, email... and is not a large flightless bird from Australia... which would be awesome... but less useful. Nope, it's an an instance of WrappedTemplatedEmail.

Hello WrappedTemplatedEmail

I'll hit Shift+Shift and look for WrappedTemplatedEmail under "classes".

This is a super powerful class... full of tons of info. It gives us access to things like the name of who the email is being sent to - more about that in a minute - the subject, return path... and it even allows us to configure a few things on the email, like embedding an image right from Twig!

We're not going to talk about all of these methods... but basically, any information about the email itself can be found here... and it even allows you to change a few things about the email... all from inside Twig.

Go back to the welcome.html.twig email template. All the way at the top, we have a title tag set to

Welcome to the Space Bar!

Having a <title> tag in an email.... is usually not that important... but it doesn't hurt to have it and make it match the email's subject. Now that we know about the email variable, we can do this properly. Change the text to {{ email.subject }}.

... line 1
<html lang="en">
<head>
... lines 4 - 5
<title>{{ email.subject }}</title>
... lines 7 - 54
</head>
... lines 56 - 96
</html>

NamedAddress and email.toName()

Tip

In Symfony 4.4 and higher, you won't see NamedAddress mentioned here. But the idea is the same: an address can consist of an email and a "name".

Back inside WrappedTemplatedEmail, all the way on top, one of my favorite methods is toName(). When you're sending an email to just one person, this is a super nice way to get that person's name. It's interesting... if the "to" is an instance of NamedAddress, it returns $to->getName(). Otherwise it returns an empty string.

What is that NamedAddress? Go back to SecurityController. Hmm, for the to() address... we passed an email string... and that's a totally legal thing to do. But instead of a string, this method also accepts a NamedAddress object... or even an array of NamedAddress objects.

Tip

In Symfony 4.4 and higher, use new Address() - it works the same way as the NamedAddress we describe here.

Check this out: replace the email string with a new NamedAddress(). This takes two arguments: the address that we're sending to - $user->getEmail() - and the "name" that you want to identify this person as. Let's use $user->getFirstName().

... lines 1 - 13
use Symfony\Component\Mime\NamedAddress;
... lines 15 - 19
class SecurityController extends AbstractController
{
... lines 22 - 49
public function register(MailerInterface $mailer, Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 52 - 54
if ($form->isSubmitted() && $form->isValid()) {
... lines 56 - 75
$email = (new TemplatedEmail())
... line 77
->to(new NamedAddress($user->getEmail(), $user->getFirstName()))
... lines 79 - 80
->context([
... lines 82 - 83
]);
... lines 85 - 93
}
... lines 95 - 98
}
}

We can do the same thing with from. I'll copy the from email address and replace it with new NamedAddress(), alienmailer@example.com and for the name, we're sending as The Space Bar.

... lines 1 - 13
use Symfony\Component\Mime\NamedAddress;
... lines 15 - 19
class SecurityController extends AbstractController
{
... lines 22 - 49
public function register(MailerInterface $mailer, Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 52 - 54
if ($form->isSubmitted() && $form->isValid()) {
... lines 56 - 75
$email = (new TemplatedEmail())
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar'))
->to(new NamedAddress($user->getEmail(), $user->getFirstName()))
... lines 79 - 80
->context([
... lines 82 - 83
]);
... lines 85 - 93
}
... lines 95 - 98
}
}

This is actually even cooler than it looks... and helps us in two ways. First, in welcome.html.twig, we can use the email object to get the name of the person we're sending to instead of needing the user variable.

To prove it, let's get crazy and comment-out the user variable in context.

... lines 1 - 13
use Symfony\Component\Mime\NamedAddress;
... lines 15 - 19
class SecurityController extends AbstractController
{
... lines 22 - 49
public function register(MailerInterface $mailer, Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 52 - 54
if ($form->isSubmitted() && $form->isValid()) {
... lines 56 - 75
$email = (new TemplatedEmail())
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar'))
->to(new NamedAddress($user->getEmail(), $user->getFirstName()))
... lines 79 - 80
->context([
... line 82
//'user' => $user,
]);
... lines 85 - 93
}
... lines 95 - 98
}
}

In the template, use {{ email.toName }}. This will call the toName() method... which should give us the first name.

... line 1
<html lang="en">
... lines 3 - 55
<body>
<div class="body">
<div class="container">
... lines 59 - 63
<div class="content">
<h1 class="text-center">Nice to meet you {{ email.toName }}!</h1>
... lines 66 - 83
</div>
... lines 85 - 93
</div>
</div>
</body>
</html>

This is nice... but the real advantage of NamedAddress can be seen in the inbox.

Try the flow from the start: find your browser, go back, change the email again - we'll be doing this a lot - type a password, submit and... go check Mailtrap. There it is:

Nice to meet you Fox.

It's now getting that from the NamedAddress. The real beauty is on top: from "The Space Bar", then the email and to "Fox" next to that email. This is how pretty much all emails you receive appear to come from a specific "name", not just an address.

The "Check HTML"

By the way, one of the tabs in Mailtrap is "Check HTML"... which is kinda cool... well... only "kind of". There is a lot of variability on how different email clients render emails, like some apparently don't support using the background-color style attribute. Crazy!

If you really want to test how your emails looks, this "Check HTML" tab probably isn't going to help too much - there are other services like Litmus that can help you. But this does highlight one huge thing we're doing wrong. It says that some style thing on line 7 isn't supported. That's referring to the style tag. It turns out that Gmail doesn't support embedding CSS in your email: it doesn't let you do it with a style tag or with a CSS file. Nope, to make things look good in gmail, you must manually put all the styles as style attributes on every single element. Gross. Fortunately, Mailer will help us with this. We'll see how soon.

But first, let's perfect how our auto-generated text content looks... by running one command and high-fiving Mailer.

Leave a comment!

  • 2020-05-27 Brandon Peterson

    Ryan, Thing of beauty, works perfect, thank you so much!

  • 2020-05-27 weaverryan

    Yo Brandon Peterson !

    Bah, I was sloppy - my fault! The ->to() method on the Email object does (of course) accept an Address object, but it does not support an *array* of Address objects, which is what we created. We can simplify:


    foreach ($orders->getOrdersTo() as $orderTo) {
    // I'm guessing the entity has a getEmail() method on it
    $email->addTo(new Address($orderTo->getEmail()));
    }

    No need to create an array at all :). If you DO want to use the fancier way, here's how. Two things to know:

    1) The addTo() and to() methods actually allow for an unlimited number of arguments. So technically, you could say ->to(new Address(...), new Address(...), new Address(...)) - this is called a variadic argument.

    2) The problem with my original comment (and what caused the "Doctrine\Common\Collections\ArrayCollection given" error) was that I forgot that when you call ->map on a Doctrine collection, it returns another Doctrine collection (ArrayCollection specifically), not a normal PHP array.

    If you put these facts together, you can find a solution like this:


    $addresses = $orders->getOrdersTo()->map(function($orderTo) { return new Address($orderTo->getEmail()) };

    $email->to(... $addresses);

    The ... syntax "expands" the addresses ArrayCollection (this works also with normal arrays) so that it is equivalent to writing ->to(new Address(...), new Address(...), new Address(...)).

    Let me know if that works!

    Cheers!

  • 2020-05-25 Brandon Peterson

    Ryan,
    With your first suggestion I now get the error "An address can be an instance of Address or a string ("array") given)"
    Which confuses me because I thought in the foreach loop they were being converted to an instance of Address. Did I miss something?

    The fancier way I like, and I haven't seen it before but I get the error
    An address can be an instance of Address or a string ("Doctrine\Common\Collections\ArrayCollection") given).
    In my orders class I do have in a construct
    $this->orderstoo = new ArrayCollection();
    I didn't add that but Symfony must have when I made the entity from the command line. Any thoughts?

  • 2020-05-21 weaverryan

    Yo Brandon Peterson!

    Ah yes! So basically, $order->getOrdersTo() gives you an array (well, it's a PersistentCollection, which is an object, but acts like an array) of your related object (e.g. Email - I'm not sure what your's is really called). You need to "convert" these into Address objects. The simplest algorithm would be:


    $addresses = [];
    foreach ($orders->getOrdersTo() as $orderTo) {
    // I'm guessing the entity has a getEmail() method on it
    $addresses[] = new Address($orderTo->getEmail());
    }

    // ...
    ->to($addresses)

    You can also get fancier and use $addresses = $orders->getOrdersTo()->map(function($orderTo) { return new Address($orderTo->getEmail()) }... but that's up to you - that's a fancier syntax for the same thing. You could also add a helper method to your Order entity - e.g. $order->getOrdersToAddresses() and you would put the converting code inside of that.

    Let me know if that helps!

    Cheers!

  • 2020-05-21 Brandon Peterson

    On a form I have an EntityType, ManyToMany relationship where on the form the user can select multiple check boxes to e-mail an order too. How to I get that into the ->to() on the e-mail script?
    ->to($orders->getOrderstoo()) is giving me an error "An address can be an instance of Address or a string ("Doctrine\ORM\PersistentCollection") given)."

  • 2020-05-11 Vladimir Sadicov

    Hey Med Tun

    You are totally right! And of course we have a note about it in the script code, and it's mentioned in video!

    Cheers!

  • 2020-05-10 Med Tun

    The NamedAddress class was removed in V4.4. You can use Symfony\Component\Mime\Address instead
    https://github.com/symfony/...

  • 2019-12-02 Vladimir Sadicov

    Hey donchev,

    Thanks for the feedback! Video should be already fixed, it should be HD now! And we'll check out README file, this course is based on Upload course, so probably that's the reason!

    Cheers!

  • 2019-11-30 donchev

    For some reason this video does not have HD version. :)

    PS. The "project files" README md is a bit wrong in the beginning (copy-pasted from the Upload files tutorial)