If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe found all the authors that want to receive an update about the articles they wrote during the last 7 days. Now, let's send them that update as an email.
If you downloaded the course code, you should have a tutorial/
directory with an inky/
directory and a file inside called author-weekly-report.html.twig
. Copy that and throw it into templates/email/
.
... line 1 | |
<container> | |
{# Header #} | |
<hr> | |
<spacer size="20"></spacer> | |
<row> | |
<columns> | |
<p> | |
What a week {{ email.toName }}! Here's a quick review of what you've been up to on the Space Bar this week | |
</p> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<table> | |
<tr> | |
<th>#</th> | |
<th>Title</th> | |
<th>Comments</th> | |
</tr> | |
<tr> | |
<td>1</td> | |
<td>Article Title</td> | |
<td>99</td> | |
</tr> | |
</table> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<center> | |
<spacer size="20"></spacer> | |
<button href="{{ url('app_homepage') }}">Check on the Space Bar</button> | |
<spacer size="20"></spacer> | |
</center> | |
</columns> | |
</row> | |
{# Footer #} | |
</container> | |
... lines 40 - 41 |
Nice! This template is already written using the Inky markup: the markup that Inky will translate into HTML that will work in any email client. But mostly, other than a link to the homepage and the user's name, this is a boring, empty email: we still need to print the core content of the email.
Let's open up welcome.html.twig
, steal the apply
line from here, and paste it on top of the new template. This will translate the markup to Inky and inline our CSS. At the bottom, add endapply
... and I'll indent everything to satisfy my burning inner need for order in the universe!
{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css'), source('@styles/email.css')) %} | |
<container> | |
... lines 3 - 38 | |
</container> | |
{% endapply %} |
To send this email, we know the drill! In the command, start with $email = (new TemplatedEmail())
, ->from()
and... ah: let's cheat a little.
... lines 1 - 6 | |
use Symfony\Bridge\Twig\Mime\TemplatedEmail; | |
... lines 8 - 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 40 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
... lines 43 - 47 | |
foreach ($authors as $author) { | |
... lines 49 - 53 | |
if (count($articles) === 0) { | |
continue; | |
} | |
$email = (new TemplatedEmail()) | |
... lines 59 - 67 | |
} | |
... lines 69 - 71 | |
} | |
} |
Go back to src/Controller/SecurityController.php
, find the register()
method and copy its from()
line: we'll probably always send from the same user. And yes, we'll learn how not to duplicate this later. I'll re-type the "S" on NamedAddress
and hit tab to add the missing use
statement on top.
... lines 1 - 14 | |
use Symfony\Component\Mime\NamedAddress; | |
... line 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 40 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
... lines 43 - 47 | |
foreach ($authors as $author) { | |
... lines 49 - 57 | |
$email = (new TemplatedEmail()) | |
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar')) | |
... lines 60 - 67 | |
} | |
... lines 69 - 71 | |
} | |
} |
Tip
In Symfony 4.4 and higher, use new Address()
- it works the same way
as the old NamedAddress
.
Ok, let's finish the rest: ->to()
with new NamedAddress()
$author->getEmail()
and $author->getFirstName()
,
... lines 1 - 14 | |
use Symfony\Component\Mime\NamedAddress; | |
... line 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 40 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
... lines 43 - 47 | |
foreach ($authors as $author) { | |
... lines 49 - 57 | |
$email = (new TemplatedEmail()) | |
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar')) | |
->to(new NamedAddress($author->getEmail(), $author->getFirstName())) | |
... lines 61 - 67 | |
} | |
... lines 69 - 71 | |
} | |
} |
->subject('Your weekly report on The Space Bar!')
and
... lines 1 - 14 | |
use Symfony\Component\Mime\NamedAddress; | |
... line 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 40 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
... lines 43 - 47 | |
foreach ($authors as $author) { | |
... lines 49 - 57 | |
$email = (new TemplatedEmail()) | |
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar')) | |
->to(new NamedAddress($author->getEmail(), $author->getFirstName())) | |
->subject('Your weekly report on the Space Bar!') | |
... lines 62 - 67 | |
} | |
... lines 69 - 71 | |
} | |
} |
->htmlTemplate()
to render email/author-weekly-report.html.twig
.
... lines 1 - 14 | |
use Symfony\Component\Mime\NamedAddress; | |
... line 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 40 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
... lines 43 - 47 | |
foreach ($authors as $author) { | |
... lines 49 - 57 | |
$email = (new TemplatedEmail()) | |
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar')) | |
->to(new NamedAddress($author->getEmail(), $author->getFirstName())) | |
->subject('Your weekly report on the Space Bar!') | |
->htmlTemplate('email/author-weekly-report.html.twig') | |
... lines 63 - 67 | |
} | |
... lines 69 - 71 | |
} | |
} |
Do we need to pass any variables to the template? Technically... no: the only variable we're using so far is the built-in email
variable. But we will need the articles, so let's call ->context([])
. Pass this an author
variable... I'm not sure if we'll actually need that... and the $articles
that this author recently wrote.
... lines 1 - 14 | |
use Symfony\Component\Mime\NamedAddress; | |
... line 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 40 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
... lines 43 - 47 | |
foreach ($authors as $author) { | |
... lines 49 - 57 | |
$email = (new TemplatedEmail()) | |
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar')) | |
->to(new NamedAddress($author->getEmail(), $author->getFirstName())) | |
->subject('Your weekly report on the Space Bar!') | |
->htmlTemplate('email/author-weekly-report.html.twig') | |
->context([ | |
'author' => $author, | |
'articles' => $articles, | |
]); | |
... line 67 | |
} | |
... lines 69 - 71 | |
} | |
} |
Done! Another beautiful Email
object. We're a machine! How do we send it? Oh, we know that too: we need the mailer service. Add a third argument to the constructor: MailerInterface $mailer
. I'll do our usual Alt+Enter trick and select "Initialize Fields" to create that property and set it.
... lines 1 - 13 | |
use Symfony\Component\Mailer\MailerInterface; | |
... lines 15 - 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 22 | |
private $mailer; | |
public function __construct(UserRepository $userRepository, ArticleRepository $articleRepository, MailerInterface $mailer) | |
{ | |
... lines 27 - 30 | |
$this->mailer = $mailer; | |
} | |
... lines 33 - 72 | |
} |
Back down below, give a co-worker a serious "nod"... as if you're about to take on a task of great gravity... but instead, send an email: $this->mailer->send($email)
.
... lines 1 - 16 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
... lines 19 - 40 | |
protected function execute(InputInterface $input, OutputInterface $output) | |
{ | |
... lines 43 - 47 | |
foreach ($authors as $author) { | |
... lines 49 - 57 | |
$email = (new TemplatedEmail()) | |
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar')) | |
->to(new NamedAddress($author->getEmail(), $author->getFirstName())) | |
->subject('Your weekly report on the Space Bar!') | |
->htmlTemplate('email/author-weekly-report.html.twig') | |
->context([ | |
'author' => $author, | |
'articles' => $articles, | |
]); | |
$this->mailer->send($email); | |
} | |
... lines 69 - 71 | |
} | |
} |
Love that. In our fixtures, thanks to some randomness we're using, about 75% of users will be subscribed to the newsletter. Before we run the command, let's make sure the data is fresh... with recent article created dates. Run:
php bin/console doctrine:fixtures:load
This should add enough users and articles that about 1-2 authors will be subscribed to the newsletter and have recent articles. Try that command:
php bin/console app:author-weekly-report:send
Ha! It didn't explode! It found 6 authors... or really, 6 users that are subscribed to the newsletter... but anywhere from 0 to 6 of these might actually have recent articles. Spin over to Mailtrap. If you don't see any emails - try reloading the fixtures again... just in case you got some bad random data, then re-run the command. Oh, and if you got an error when running the command about too many emails being sent, you've hit a limit on Mailtrap. The free plan only allows sending 2 emails each 10 seconds. In that case, ignore the error - because two emails did send - or reload your fixtures to hopefully send less emails.
We have exactly one email: phew! So... we rock! Or do we?
I see a few problems. First, the link to the homepage is broken: it links to localhost
. Not localhost:8000
- or whatever our real domain is - just localhost
. When you send emails from a console command... your paths break. More on that later.
The second problem is more obvious... and it's my fault: this email is missing the cool header and footer we had in the other email! Why? Simple: in welcome.html.twig
, we have a header with a logo on top and a footer at the bottom. In author-weekly-report.html.twig
? I forgot to put that stuff!
Ok, I really did it on purpose: we probably do want a consistent layout for every email... but we definitely do not want to duplicate that layout in every email template.
We know the fix! We do it all the time in normal twig: create a base template, a base email template. In the templates/email
directory, add a new file called, how about emailBase.html.twig
.
And... I'll close a few files. In welcome.html.twig
, copy that entire template and paste in emailBase
. Then... select the middle of the template and delete! We basically want the header, the footer and, in the middle, a block for the content. Add {% block content %}{% endblock %}
.
{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css'), source('@styles/email.css')) %} | |
<container> | |
<row class="header"> | |
<columns> | |
<a href="{{ url('app_homepage') }}"> | |
<img src="{{ email.image('@images/email/logo.png') }}" class="logo" alt="SpaceBar Logo"> | |
</a> | |
</columns> | |
</row> | |
{% block content %} | |
{% endblock %} | |
<row class="footer"> | |
<columns> | |
<p>Cheers,</p> | |
<p>Your friendly <em>Space Bar Team</em></p> | |
</columns> | |
</row> | |
<row class="bottom"> | |
<columns> | |
<center> | |
<spacer size="20"></spacer> | |
<div> | |
Sent with ❤️ from the friendly folks at The Space Bar | |
</div> | |
</center> | |
</columns> | |
</row> | |
</container> | |
{% endapply %} |
That block name could be anything. Now that we have this nifty template, back in welcome.html.twig
, life gets simpler. On top, start with {% extends 'email/emailBase.html.twig' %}
. Then, delete the apply
and endapply
, and replace it with {% block content %}
... and {% endblock %}
.
{% extends 'email/emailBase.html.twig' %} | |
{% block content %} | |
<row class="welcome"> | |
<columns> | |
<spacer size="35"></spacer> | |
<h1> | |
<center> | |
Nice to meet you {{ email.toName }}! | |
</center> | |
</h1> | |
<spacer size="10"></spacer> | |
</columns> | |
</row> | |
<spacer size="30"></spacer> | |
<row> | |
<columns> | |
<p> | |
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. | |
</p> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<center> | |
<button href="{{ url('admin_article_new') }}">Get writing!</button> | |
</center> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<p> | |
Check out our existing articles and share your thoughts in the comments! | |
</p> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<center> | |
<button href="{{ url('app_homepage') }}">Get reading!</button> | |
</center> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<p> | |
We're so excited that you've decided to join us in our corner of the universe, | |
it's a friendly one with other creative and insightful writers just like you! | |
Need help from a friend? We're always just a message away. | |
</p> | |
</columns> | |
</row> | |
{% endblock %} |
If you're wondering why we don't need the inky_to_html
and inline_css
filter stuff anymore, it's because the contents of this template will be put into a block that is inside of those same filters. The content will go through those filters... but we don't need to worry about adding them in every template.
Now we can delete most of the content: all we really need is the welcome row... and down below, we can get rid of the bottom and footer stuff. Celebrate your inner desire for order by un-indenting this.
Perfecto! Repeat this beautiful code in author-weekly-report.html.twig
: {% extends 'email/emailBase.html.twig' %}
, {% block content %}
and all the way at the bottom, {% endblock %}
. We can also remove the container
element... and unindent.
{% extends 'email/emailBase.html.twig' %} | |
{% block content %} | |
<hr> | |
<spacer size="20"></spacer> | |
<row> | |
<columns> | |
<p> | |
What a week {{ email.toName }}! Here's a quick review of what you've been up to on the Space Bar this week | |
</p> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<table> | |
<tr> | |
<th>#</th> | |
<th>Title</th> | |
<th>Comments</th> | |
</tr> | |
<tr> | |
<td>1</td> | |
<td>Article Title</td> | |
<td>99</td> | |
</tr> | |
</table> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<center> | |
<spacer size="20"></spacer> | |
<button href="{{ url('app_homepage') }}">Check on the Space Bar</button> | |
<spacer size="20"></spacer> | |
</center> | |
</columns> | |
</row> | |
{% endblock %} |
That felt great! Let's see how it looks: run our weekly report:
php bin/console app:author-weekly-report:send
And... move back over! Woo! Now every email can easily share the same "look".
Next, let's finish the email by making it dynamic. And, most importantly, let's figure out why our link paths are broken. You need to be extra careful when you send an email from the command line.
Hi @Ryan, shouldn't inky & foundation get us responsive email templates out of the box? i always end up scrolling horizontal on mobile (mailtrap is an awesome helper, thanks!!).. checked out the course project and realized, it's having problems with mobile devices too.. would you mind taking a look into responsive email templates with inky in the course?
When the command explodes as of Mailtrap free accounts limitations:
<blockquote>Expected response code "354" but got code "550", with message "550 5.7.0 Requested action not taken: too many emails per second".</blockquote>
If you want to make it work regardless of the amount of emails you send you can add sleep(5)
at the end of the loop, which makes it slow but working.
Hey AymDev,
Thank you for this tip! Yeah, adding a 5 seconds delay should be a workaround. Or just use messenger that will send not more than 1 email each 5 seconds - and this way you even don't need to tweak any code. We cover the integration with Messenger in the end of this course :)
Cheers!
I thought about Messenger too :)
Yes I just proposed a lazy workaround but there's better real solutions as you mentioned.
Cheers !
Yep. No HD version for this video as well. The same applies to "Email Context & the Magic 'email' Variable" video.
// 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
}
}
Nice post !
Thanks for your share !
I did the same before and it's the good way!
See you later :)
Thomas de Smart-Infos