Organizing Emails Logic into a Service
Keep on Learning!
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're sending two emails: one from a command and the other from src/Controller/SecurityController.php
. The logic for creating and sending these emails is fairly simple. But even still, I prefer to put all my email logic into one or more services. The real reason for this is that I like to have all my emails in one spot. That helps me remember which emails we're sending and what they contain. After all, emails are a strange part of your site: you send a lot of them... but rarely or never see them! Like, how often do you do a "password reset" on your own site to check out what that content looks like? Keeping things in one spot... at least helps with this.
Creating a Mailer Service
So what we're going to do is, in the Service/
directory, create a new class called FileThatWillSendAllTheEmails
... ah, or, maybe just Mailer
... it's shorter.
// ... lines 1 - 2 | |
namespace App\Service; | |
// ... lines 4 - 6 | |
class Mailer | |
{ | |
// ... lines 9 - 14 | |
} |
The idea is that this class will have one method for each email that our app sends. Now, if your app sends a lot of emails, instead of having just one Mailer
class, you could instead create a Mailer/
directory with a bunch of service classes inside - like one per email. In both cases, you're either organizing your email logic into a single service or multiple services in one directory.
Start by adding an __construct()
method. The one service that we know we're going to need is MailerInterface $mailer
... because we're going to send emails. I'll hit Alt + Enter and go to "Initialize fields" to create that property and set it.
// ... lines 1 - 2 | |
namespace App\Service; | |
use Symfony\Component\Mailer\MailerInterface; | |
class Mailer | |
{ | |
private $mailer; | |
public function __construct(MailerInterface $mailer) | |
{ | |
$this->mailer = $mailer; | |
} | |
} |
Ok, let's start with the registration email inside of SecurityController
. Ok... to send this email, the only info we need is the User
object. Create a new public function sendWelcomeMessage()
with a User $user
argument.
// ... lines 1 - 4 | |
use App\Entity\User; | |
// ... lines 6 - 12 | |
class Mailer | |
{ | |
// ... lines 15 - 27 | |
public function sendWelcomeMessage(User $user) | |
{ | |
// ... lines 30 - 40 | |
} | |
// ... lines 42 - 63 | |
} |
Then, grab the logic from the controller... everything from $email =
to the sending part... and paste that here. It looks like this class is missing a few use
statements... so I'll re-type the "L" on TemplatedEmail
and hit tab, then re-type the S
on NamedAddress
and hit tab once more... to add those use
statements to the top of this file. Then change $mailer
to $this->mailer
.
Tip
In Symfony 4.4 and higher, use new Address()
- it works the same way
as the old NamedAddress
.
// ... lines 1 - 6 | |
use Symfony\Bridge\Twig\Mime\TemplatedEmail; | |
// ... line 8 | |
use Symfony\Component\Mime\NamedAddress; | |
// ... lines 10 - 12 | |
class Mailer | |
{ | |
// ... lines 15 - 27 | |
public function sendWelcomeMessage(User $user) | |
{ | |
$email = (new TemplatedEmail()) | |
->from(new NamedAddress('alienmailcarrier@example.com', 'The Space Bar')) | |
->to(new NamedAddress($user->getEmail(), $user->getFirstName())) | |
->subject('Welcome to the Space Bar!') | |
->htmlTemplate('email/welcome.html.twig') | |
->context([ | |
// You can pass whatever data you want | |
//'user' => $user, | |
]); | |
$this->mailer->send($email); | |
} | |
// ... lines 42 - 63 | |
} |
I love it! This will simplify life dramatically in SecurityController
. Delete all the logic and then above... replace the MailerInterface
argument with our shiny new Mailer
class.
// ... lines 1 - 8 | |
use App\Service\Mailer; | |
// ... lines 10 - 20 | |
class SecurityController extends AbstractController | |
{ | |
// ... lines 23 - 50 | |
public function register(Mailer $mailer, Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator) | |
{ | |
// ... lines 53 - 89 | |
} | |
} |
Below, it's as simple as $mailer->sendWelcomeMessage($user)
.
// ... lines 1 - 8 | |
use App\Service\Mailer; | |
// ... lines 10 - 20 | |
class SecurityController extends AbstractController | |
{ | |
// ... lines 23 - 50 | |
public function register(Mailer $mailer, Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator) | |
{ | |
// ... lines 53 - 55 | |
if ($form->isSubmitted() && $form->isValid()) { | |
// ... lines 57 - 74 | |
$em->flush(); | |
$mailer->sendWelcomeMessage($user); | |
// ... lines 78 - 84 | |
} | |
// ... lines 86 - 89 | |
} | |
} |
That looks really nice! Our controller is now more readable.
Let's repeat the same thing for our weekly report email. In this case, the two things we need are the $author
that we're going to send to - which is a User
object - and the array of articles. Ok, over in our new Mailer
class, add a public function sendAuthorWeeklyReportMessage()
with a User
object argument called $author
and an array of Article
objects.
// ... lines 1 - 12 | |
class Mailer | |
{ | |
// ... lines 15 - 42 | |
public function sendAuthorWeeklyReportMessage(User $author, array $articles) | |
{ | |
// ... lines 45 - 62 | |
} | |
} |
Time to steal some code! Back in the command, copy everything related to sending the email... which in this case includes the entrypoint reset, Twig render, PDF code and the actual email logic. Paste that into Mailer
.
// ... lines 1 - 12 | |
class Mailer | |
{ | |
// ... lines 15 - 42 | |
public function sendAuthorWeeklyReportMessage(User $author, array $articles) | |
{ | |
$this->entrypointLookup->reset(); | |
$html = $this->twig->render('email/author-weekly-report-pdf.html.twig', [ | |
'articles' => $articles, | |
]); | |
$pdf = $this->pdf->getOutputFromHtml($html); | |
$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, | |
]) | |
->attach($pdf, sprintf('weekly-report-%s.pdf', date('Y-m-d'))); | |
$this->mailer->send($email); | |
} | |
} |
This time, we need to inject a few more services - for entrypointLookup
, twig
and pdf
. Let's add those on top: Environment $twig
, Pdf $pdf
and EntrypointLookupInterface $entrypointLookup
. I'll do my Alt + Enter shortcut and go to "Initialize fields" to create those three properties and set them.
// ... lines 1 - 5 | |
use Knp\Snappy\Pdf; | |
// ... lines 7 - 9 | |
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface; | |
use Twig\Environment; | |
// ... line 12 | |
class Mailer | |
{ | |
// ... line 15 | |
private $twig; | |
private $pdf; | |
private $entrypointLookup; | |
public function __construct(MailerInterface $mailer, Environment $twig, Pdf $pdf, EntrypointLookupInterface $entrypointLookup) | |
{ | |
$this->mailer = $mailer; | |
$this->twig = $twig; | |
$this->pdf = $pdf; | |
$this->entrypointLookup = $entrypointLookup; | |
} | |
// ... lines 27 - 63 | |
} |
Back in the method... oh... that's it! We're already using the properties... and everything looks happy! Oh, and it's minor, but I'm going to move the "entrypoint reset" code below the render. This is subtle... but it makes sure that the Encore stuff is reset after we render our template. If some other part of our app calls this methods and then renders its own template, Encore will now be ready to do work correctly for them.
// ... lines 1 - 12 | |
class Mailer | |
{ | |
// ... lines 15 - 42 | |
public function sendAuthorWeeklyReportMessage(User $author, array $articles) | |
{ | |
$html = $this->twig->render('email/author-weekly-report-pdf.html.twig', [ | |
'articles' => $articles, | |
]); | |
$this->entrypointLookup->reset(); | |
// ... lines 49 - 62 | |
} | |
} |
Anyways, let's use this in the command. Delete all of this logic and... in the constructor, change the $mailer
argument to Mailer $mailer
. Now we get to delete stuff! Take off the $twig
, $pdf
and $entrypointLookup
arguments, clear them from the constructor and remove their properties. If you really want to make things squeaky-clean, we now have a bunch of "unused" use
statements that are totally useless.
// ... lines 1 - 6 | |
use App\Service\Mailer; | |
// ... lines 8 - 14 | |
class AuthorWeeklyReportSendCommand extends Command | |
{ | |
// ... lines 17 - 20 | |
private $mailer; | |
public function __construct(UserRepository $userRepository, ArticleRepository $articleRepository, Mailer $mailer) | |
{ | |
parent::__construct(null); | |
$this->userRepository = $userRepository; | |
$this->articleRepository = $articleRepository; | |
$this->mailer = $mailer; | |
} | |
// ... lines 31 - 61 | |
} |
Back down, call the method with $this->mailer->sendWeeklyReportMessage()
passing $author
and $articles
.
// ... lines 1 - 2 | |
namespace App\Tests; | |
use PHPUnit\Framework\TestCase; | |
class MailerTest extends TestCase | |
{ | |
public function testSomething() | |
{ | |
$this->assertTrue(true); | |
} | |
} |
Phew! This really simplifies the controller & command... and now I know exactly where to look for all email-related code. Let's... just make sure I didn't break anything. Run:
php bin/console app:author-weekly-report:send
No errors... and in Mailtrap... yep! 2 emails... with an attachment!
Next, sending emails is scary! So let's add some tests. We'll start by adding a unit test and later, an integration test, functional test... and a final exam that will be worth 50% of your grade for the semester. Ok... no final exam - but we will do that other stuff.
Hello ! In case we have a .pdf file already ready in our assets, and which has been copied to the
public/build/files/manuel_utilisateur.pdf
(and is therefore versioned) with Webpack Encore, how can I attach this file to send it?I know how to attach a file that is directly in the assets. Like this
But if the file is versioned, in the public folder, what can I do?
Besides, is it a good solution to attach a file from the public folder, or is it better to leave it in the assets?
This is a user manual that will be sent to each new user