Introduction to Sending Emails in PHP
It's time for us to learn how to send emails the right way in PHP. So in this lesson, we'll learn how to send emails, a little bit about email protocols, setting up a local mailbox for testing emails, and so on. Please drop a like and subscribe to the channel if you enjoy my content. It really helps YouTube's algorithm recommend my videos to more developers like you.
So how do we send emails in PHP? There is, of course, the native way using the built-in function called mail, but we are not going to be learning that due to its limited functionality with attachments, deliverability, and so on. Another way to send emails is to use a third-party package. SwiftMailer was a good option until it was retired, so Symfony Mailer is the one that we'll be covering today.
Understanding Email Protocols: SMTP, POP3, and IMAP
Before we actually install the Symfony Mailer, we need to know a little bit about email protocols like SMTP, POP3, and IMAP. SMTP stands for Simple Mail Transfer Protocol and is used to deliver email from an email client like Outlook to an email server, or from one email server to another email server. So when you type up an email and hit the send button, what typically happens when using SMTP is that the client, or the sender, opens up a TCP connection to the SMTP server and uses some commands to communicate. The email server does a bunch of stuff and then sends the message to the recipient's or target's email server. The recipient's email server will hold the email until the user or the recipient opens up their inbox to check the email.
Now, SMTP is used along with other protocols because SMTP is for sending or delivering email. IMAP or POP3 are protocols that are used for retrieving or receiving emails. POP3 stands for Post Office Protocol version 3, and IMAP stands for Internet Message Access Protocol. The difference is that IMAP offers more features than POP3. For example, POP3 by default deletes the email from the server after it retrieves it, while IMAP on the other hand does not delete the email on the server and allows it to be synced across multiple clients. So the emails with IMAP are stored on the server.
There are ways to configure POP3 to keep a copy of the email on the server as well, but IMAP still offers more features. We're not going to go in a lot of detail about this, but you can read up on them on your own if you want to know more. I just wanted to cover the basics in this lesson.
Installing Symfony Mailer and Project Setup
So let's go back to the Symfony Mailer. I have the Symfony Mailer documentation open here at the installation step. So we're going to copy this composer require command and run it in our terminal. So we'll open the code, we'll open the terminal, run composer require and it's going to pull in all the necessary packages including the Symfony Mailer itself.
Now, while it's doing its own thing, I'm going to close out the terminal and explain what I have set up here. I have the User controller with two routes or two methods where one is to create the user, which is the user registration route, which simply renders the register form. And then we have a register method which is a POST request to the user's endpoint which is supposed to register the user. Now, we're not actually going to register the user, we're just going to send a welcome email and assume that the user was registered.
Constructing and Sending a Basic Email
To send an email using Symfony Mailer we need a few things. We need the transport or transporter object, a mailer service, and the actual email message object. Let's create the email message object first. So we'll create an email object here, and we'll set that to a new Email from the Symfony\Component\Mime package, which is a dependency to the Symfony Mailer.
We can use this class to build up an email by chaining the method calls. So we need to set the from email, we can set the to email where the email is being delivered to, and we can use the email that we get from the form. So we'll set that to email. We'll set the subject to 'Welcome', and we'll set the text to the email body text that I have created here. This is a very simple email object. There are of course more methods available on the email object, but I'm going to leave that for you to explore and experiment.
Now that we got the email object, we need to send this out using the mailer service. We can create the mailer object using the Mailer class. If we click into the Mailer class, we see that it just has a single method called send which accepts a message as an argument. So we just need to call the send method and pass the message as an argument. So we'll go back to the controller and we'll simply do mailer->send($email).
As you probably noticed the Mailer class has a required constructor parameter which we are not passing, and that is the transport object which implements a Transport interface. The transporter is actually responsible for sending the email message using a protocol, hence why we talked about SMTP and other email protocols.
Configuring the Transport with DSN
So if we inspect into this interface, we see that it just has a single method, send. If we check the send method on the Mailer class, we see that it just calls the send method on the transport object. The reason this is an interface is because there are many transports that can be used to send an email. If we open up the documentation and scroll down here within the transport setup, we see that there are a bunch of other transports that we could use like native transports, or third-party transports like Gmail, Mailchimp, Postmark, SendGrid and so on. If we scroll up a little bit here, we see that there are three built-in transports. We have SMTP, sendmail, and native. We're going to be using the SMTP for this lesson.
So we need to create a transport object and pass it into the constructor of the mailer class. There is a Transport class that we can use which has a static method called fromDsn that can create a transport object for us. And we need to pass some kind of DSN in here and then we'll take this transport object and pass it into the mailer. Let's inspect the Transport class here. So now we need the proper DSN string to create the proper transport object.
DSN stands for Data Source Name. It is a string that represents the location where there is a database connection, a file system location, or in this case, email transport information. We can see the various DSNs in the Symfony Mailer documentation. For example, this is a DSN for the SMTP, this is a DSN for sendmail, this is a DSN for native. And if we keep scrolling down, we'll see DSNs for the third-party packages like Gmail and so on.
Now, of course, we could use Gmail SMTP to send our emails, but we don't actually want to send emails from our local environment. We want to test that our emails are being sent, but we don't want to deliver them to actual email inboxes. So we're not going to use Gmail SMTP. We need our own local SMTP server.
Setting Up a Local SMTP Server with MailHog
So our DSN will be something like this. So we're going to copy this and we're going to create a DSN variable here and put that in. So now we need an SMTP server, username, password, and the port because this would not work since this is not a valid SMTP server.
Now as I mentioned before, for an SMTP server, we could of course use our own Gmail SMTP, but we're not going to be doing that because we don't want to send actual emails from our local environment. We need some sort of local inbox. There are some free and some paid services for that, like Mailtrap, but there are also open source packages like MailHog. We're going to use MailHog and install it via Docker for our local environment and configure it.
So I'm going to open the docker-compose.yml file, and I have already pasted in this snippet here for the MailHog container. We have the container name, the image is mailhog/mailhog, and we're exposing two ports. 8025 is for the web UI where we can access our inbox, and 1025 is for the SMTP server. So let's exit out of the container here. Let's cd into the docker directory and run docker-compose up -d. And we wait for it to build our MailHog container.
Now while that's installing MailHog, what we need to do is that we need to update our SMTP DSN here. So we don't really need the username/password here, but our SMTP server host is the mailhog because that's the container the MailHog will be running on. So we'll do mailhog and the port will be 1025. Now we'll pass in the DSN variable here and we should be good to go. Let's open up the browser here, and we'll go to localhost:8025 because that's where the web UI runs, and as you can see, it opened MailHog here and we have our inbox, which has no emails at the moment.
Sending HTML Content and Attachments
So let's test this out and send some email. We hit register. It takes a second, and we get a blank page, which means that the email should have been sent and we don't get any errors, so that's good news. If we open the MailHog here and refresh the inbox, we see that an email has been received. So everything looks good.
Now in addition to sending email as a text format, we can send HTML as well because we don't want to just send plain text like that. We might want to use HTML templates and so on. So let's create the HTML version of this text. We'll copy the same text here and maybe we want to add some kind of welcome message right before in blue color and center it. We want to add a break line here, and that should be good enough. Then we'll chain another method here called html and pass in the HTML body. Let's test this out. I'm going to refresh this page and it's just going to resubmit the same form we submitted before with the same information. So it's just going to send the email to the same user. So if we refresh the inbox, we got the email, and if we open it up, we see that now we have a new tab here called HTML and we get the HTML email message. If we switch over to the plain text, we still have the plain text available.
The reason why we are using both HTML and text version in code is that if an email client does not support HTML for whatever reason, it will fall back to the text version. We could also move this into a view file, of course, because we don't want to clutter our controllers with this kind of data here. We don't want HTML in our controllers. So the proper way would be to move this into a view and then render it into a string and pass that string as the HTML. You could have two view files, one for the text version of the email and one for the HTML version, so you could send both. Or you could simply parse the HTML version and extract only the text out of it and then send that. We haven't covered templating engines and Twig here, so I'm not going to be covering templated emails, but it could be a good exercise for you to look into sending templated emails and what Twig is and what the templating engines are and so on. We might cover some templating engines like Blade in this series later in the course. So we'll leave this as is for now.
We could send emails with attachments as well. We could attach content or attach files from a given path. We could chain another method here called attach, and as you can see, we can attach from path and we can attach part, or we can simply just attach from a resource or a content. In this case, we'll just attach some kind of content. So, we'll say "Hello World" and we'll call it welcome.txt. And let's test this out quick. We'll refresh the page again. It's going to resubmit the same form with the same values. We go here, refresh the inbox, we got the email, and if we switch over to the Mime, we have that attachment right here.
Refactoring to Use Environment Variables
Now let's clean this up a little bit. As I mentioned, the proper way would be to move this into the view files and then render the view files into strings and use that to pass in here. Now I'm not going to do that in this lesson, but you can do that as an exercise if you want to. What I want to clean up is this part right here. We're hard-coding the DSN string in here and that's not the right way. We want to get that from the environment variable. We need to take this and put it into an environment variable in our .env file. So we'll open the .env file and we'll create MAILER_DSN and set that to that value. So then we can load the mailer dsn from the $_ENV superglobal, right? So we'll do $_ENV['MAILER_DSN'] and we can get rid of that. And let's make sure that everything still works. So we'll refresh the page here, resubmitting the same form, and we got the email. Everything is looking good.
Uh, that one thing that is annoying me, it's missing a space here, so let's do that quick, refresh the page, and looks much better.
The next improvement that I want to do is that this transport object here and the mailer service class kind of looks out of place. We talked about dependency injection and dependency injection containers in this series, and this kind of doesn't follow that. Wouldn't it be awesome if we were able to get rid of this and instead do something like this mailer->send and accept the mailer object or mailer service through the constructor? So something like __construct and accept the mailer object here. If you need a refresher on the dependency injection and dependency injection containers, I'm going to leave the links in the description to those lessons that you can watch which we covered in this series.
Implementing Dependency Injection for the Mailer Service
Now, if we inspect the Mailer class, we see that it implements a MailerInterface, so we don't need to be specific here and we don't need to use a concrete class. We can simply expect some sort of implementation of MailerInterface, and we covered that as well in this series. So if you've been following along, this should be familiar.
Now this of course is not going to work because we haven't told our app how to resolve MailerInterface. We need to bind it to a concrete implementation. So if we open the app.php class here, we see that we are binding a PaymentGatewayServiceInterface to a concrete PaymentGatewayService class. So we need to do the same, but for the MailerInterface, and we'll bind it to some kind of CustomMailer class. Let's create this class, and we need to implement the MailerInterface and MailerInterface provides a single send method, so we need to provide the implementation for that method.
Now, in the constructor, we can accept the DSN and we'll promote this to a property. And within the constructor, we can create the transport object. So we'll paste in the code that we copied before, and we'll simply create the transport using the fromDsn method. And instead of using the environment superglobal, we'll pass the DSN string that we get from the constructor. We can set this to a property as well. And within the send method, we can just call the send method on the transport object and pass the message. So we're pretty much doing the same thing that the other mailer class was doing. If we go back to the mailer class here we see that we're pretty much doing the same thing that this class was doing, except that we're not doing any of the event dispatching and things like that. The point I'm trying to make is that it's that easy to swap out the implementation and roll your own custom mailer class. We can add some other logic here, do some checks, do some logic before the send, do some logic after the send, and so on. Basically there are many ways of implementing and structuring this, and this is not the only way.
Now we also need to edit our config class because if we go back to the app.php here, this will not simply work because CustomMailer expects a DSN string as an argument. So instead of doing this, we can bind it to a closure and instantiate the CustomMailer here and pass in the DSN from the config class. So we'll do $config->mailer->dsn.
We need to add the ability to pull the mailer DSN config from the config class. If we open the Config class, we see that we only have the db option here. We just need to create the entry here for the mailer and we'll set this to an array, and then within that array we'll set it to dsn and we'll get that from the environment variables array that we get in the constructor. So let's close this out and the reason it's underlining it here is because we haven't added the property read PHPDoc here. Let's close that out and let's test it out to make sure that everything still works. We'll open the browser, resubmit the form again with the same values, no errors, so that's a good sign. We refreshed the inbox and we got the email right here.
So this is it for this video. Thank you so much for watching. Smash the like button if you enjoyed the video and subscribe to the channel if you haven't already done so. Stay tuned for the next lesson since we'll be covering cron jobs and how to queue the emails. Thanks again and I'll see you next time.