Methods to Ship E-mail with PHP Utilizing SendGrid and Mezzio


Email is as important a communication tool as ever. To help you better leverage email, I’m going to show you how to send email using PHP’s Mezzio framework and Twilio SendGrid’s API.

Specifically, you’re going to learn how to send emails with both plain text and HTML bodies, and that includes a PDF attachment. You’re also going to learn how to use SendGrid’s Transactional Templates functionality to make creating email bodies simpler both by the development team, as well as any other team within your organization.

Sound good? Let’s begin.

Quick Application Overview

As a way of making this tutorial more meaningful, pretend that the code that we’ll write is part of a fictitious, online ecommerce shop built with Mezzio, named The Little PHP Shop; specifically, the part immediately after a customer makes a purchase. At that point in the user flow, an email is sent to the customer, thanking them for their purchase, and includes a PDF invoice for their records.

We’re going to create a Handler class to send the post-purchase email, which will receive purchase details from an order completed by the customer. With that purchase information, the Handler class will then use several classes in the SendGrid PHP API to build and send the purchase confirmation email. 

Of these, the most important are: `SendGridMailMail` and `SendGrid`. `SendGridMailMail` is the object that stores all of the properties for an email message that we’ll send through SendGrid. The `SendGrid` object forms the transport layer, facilitating the sending of emails through SendGrid.


To complete this tutorial, you will need the following 4 things in your local development environment:

  1. A SendGrid account
  2. PHP 7.4 with the cURL, mbstring, and OpenSSL extensions installed and enabled
  3. Composer globally installed
  4. cURL

Scaffold the Mezzio Application

We first need to create the base application. To do that, we’re going to, as always, use the Mezzio Skeleton, to do it for us. Once the base application’s been scaffolded, we’ll then switch to the newly created project directory. Then run the following commands in your terminal, following the prompts for the first one:

Install the required dependencies

With the project scaffolded, we need to add 3 additional dependencies to complete the project. These are: 

To install them, run the following command in your terminal:

Initialize PHP Dotenv

With the dependencies installed, we load PHP Dotenv, so that it will read the variables set in `.env` and make them available to PHP as environment variables. To do that, insert the following code in `public/index.php`, right after `require ‘vendor/autoload.php’;`.

Add your SendGrid account details

Now you need to supply the application with your SendGrid API key. To do that, after logging into SendGrid, navigate to “Settings -> API Keys”. Once there:

  1. Click “Create API Key” to create an API key
  2. Give the new API key a name, accept the default API Key Permission of “Full Access”, and click “Create and View

After the API key is created, click and copy the key, and click “Done

After that, add two more keys to the file: `SENDGRID_DEFAULT_SENDER_ADDRESS` and `SENDGRID_DEFAULT_SENDER_NAME`. As the names indicate, these are the email address and name that will be used for any emails sent from our application, unless they are overridden.

Note: Whichever email address you use, it needs to be verified. To do this, in Sender Authentication, make sure that it’s listed as “Verified” in the “Single Sender Verification” table.

Set Application Mail Configuration Details

In addition to the SendGrid API key, we’re going to store a few other global mail configuration settings. Specifically, we’re going to set a from and reply to email address and name that we’ll use in every email message. That way, we don’t have to set them each time. 

We’re also going to create two email body templates, one for plain text emails and one for HTML emails. To do that, in `config/autoload` create a new file named ``, and in it add the following code.

Create a class to Instantiate a Mail object

With the key application configuration details set, let’s now create a class that will instantiate a basic `Mail` object with the default properties set. To do that, in a new directory, `src/App/src/Mailer`, create a new file called `SendGridMailMessageFactory.php`, and in it add the code below.

When the class is invoked, it will be provided with access to the application’s DI container, from which it will retrieve the configuration details which we stored in `config/autoload/`. 

After that, it will instantiate a new `SendGridMailMail` object, and set the from and reply to details by passing the respective configuration details to calls to `Mail`’s `setFrom` and `setReplyTo` methods, respectively. After that, the instantiated `Mail` object is returned.

To be able to use it though, it needs to be registered with the DI container. To do that, in `src/App/src/ConfigProvider`, add the following entry to the `factories` element in the array returned from the `getDependencies` method.

Create a Handler to send email

We next need to create a Handler class for composing and sending emails. To do that, we’ll use Mezzio’s CLI tooling, available via Composer, by running the command below.

Running the command above does four things for us:

  1. Creates a new handler class, `src/App/Handler/EmailSenderHandler.php`
  2. Creates a factory class to instantiate the handler class, `src/App/Handler/EmailSenderHandlerFactory.php`
  3. Creates a template file (`src/App/templates/app/email-sender.html.`), which we won’t need. In the file name `` is determined by the template engine that you chose during the `create-project` stage.
  4. Registers the new handler class as a service in the DI (Dependency Injection) container, by adding an entry to `config/autoload/`.

Refactor the Handler to send emails

With `EmailSenderHandler.php` created, we now need to refactor it so that it can send emails. To do that, we’ll first refactor `EmailSenderHandler`’s, constructor, replacing the `TemplateRendererInterface` parameter with three new parameters. These will initialize three new class member variables:

  • A `SendGridMailMail` object
  • A `SendGrid` object
  • An array containing the required configuration details

You can see the revised constructor in the example below, along with the related class member variables. Replace the existing class member variable and the constructor with this code.

Note: make sure you remove the now unused use statement for `TemplateRendererInterface`.

Next, we need to refactor the `handle` method. Replace the existing contents of `EmailSenderHandler`’s `handle` method, with the code below, then let’s step through what it’s doing.

It starts by using `$request->getParsedBody()` to retrieve any parameters provided in the request body, which will return an associative array, initializing `$details`. With the parameters available, it calls the `SendGridMailMail` object’s `addTo` method to set the email’s recipient, passing in the recipient’s email address and name in the first two arguments, and an array of substitutions in the third argument. 

Substitutions allow email messages to be customized for each recipient. In plain text emails, any string immediately surrounded by hyphens, e.g., `-first_name-` is a substitution. In HTML emails, it uses the Handlebars syntax, which surrounds strings with double parenthesis, e.g.,  `{{ first_name }}`.

Next, the message’s subject is set and a plain text and HTML message body are added. With that our email is ready to be sent, so we use the `SendGrid` object, `$this->mailer`, to send it, initializing a new variable, `$response`, with the response from attempting to send the message. Finally, we return a JsonResponse object, containing the status code and body from the response. 

Note: The status code can be one of 202, 400, 401, 403, and 413. The message will only contain a value, which says what went wrong if the status code is not 200.

Refactor EmailSenderHandlerFactory’s __invoke method

Now that we’ve completed refactoring `EmailSenderHandler`, we need to refactor `EmailSenderHandlerFactory` so that it will instantiate `EmailSenderHandler` correctly. To do that, replace the existing definition of its `__invoke` method with the following code.

This retrieves a `SendGridMailMail` object from the DI container, pre-initialized with the sender and reply to email details, and initializes a new object named `$message`. It then instantiates a new `SendGrid` object, named `$mailer` for sending the mail message. Finally, it retrieves the mail configuration from the application’s configuration. It then uses these to initialize and return the `EmailSenderHandler` object.

Update the routing table

With all of those changes, there’s one last change to make before we can test the code and send an email. We have to update the routing table, so that the default route uses our new Handler class as the route’s handler, instead of `HomePageHandler`. To do that, replace the default route’s definition in `config/routes.php` with the following example.

Send the first email

Now it’s time to send the first email. To do that, first, start the application by running the command below in the terminal.

Then, using cURL, make a GET request to `http://localhost:8080`, as in the example below, replacing the values in angle brackets with relevant details for your email.

You should see `{“status”:202,”message”:””}` output to the terminal, and you should receive an email that looks like the image below.


Note: Feel free to use your browser, or another testing tool, such as Postman, instead.

Use Transactional Email Templates instead of template strings

While we’ve been able to send an email with both a plain text and HTML body, how we’ve done it, however, is less than ideal. For each email that we send — and our application may end up sending quite a few — we’ll need to add a plain text and HTML body for them. 

But storing the email body definitions in code places the majority of the effort on the development team. However, at least in my experience, it’s often the case that other teams, often marketing, who create and maintain email templates. 

So for that reason, and because it would speed up development and share the load across multiple teams, we’re going to refactor the application to use Transactional Email Templates. These can be created and maintained through the SendGrid UI, by team members who may have little or no technical experience, instead of in code.

If you’ve not heard of them, the SendGrid glossary defines them as follows:

> Transactional email templates are pre-coded email layouts that marketers, designers, and developers can use to quickly and easily create transactional email campaigns. SendGrid’s transactional email templates allow non-technical and technical people alike to make real-time changes to the email their recipients receive.

For this article, we only need a fairly basic one. To do that, follow the details in the SendGrid documentation and create one that has as its content only a single text module. Then, for the body text, use the text below.

Note that there are 6 substitutions in the email:

  • `first_name`: The customer’s first name
  • `last_name`: The customer’s last name
  • `sender_name`: The e-commerce shop’s name (The Little PHP Shop)
  • `sender_state`: The e-commerce shop’s state
  • `sender_country`: The e-commerce shop’s country
  • `support_email`: The support email that customers can use to get after-sales support.

Given that, we need to make that information available to our application. The customer’s first and last names we already have. The remaining 4 substitutions will be global, so, as we did earlier, we’ll add the configuration below to `config/autoload/`, after the `templates` element.

With the new configuration file created, in `EmailSenderHandler`’s `handle` method, replace the two calls to `$this->mail->addContent()`, with the following code.

The two method calls add the sender details and support email address as global substitutions. These substitutions are applied to the email body before any other substitutions but are overridden if there are substitutions with the same key for an email recipient.

Next, you need to retrieve the transactional email template’s id. You can find it by clicking the template’s name in the templates list, as you can see in the screenshot below.


Copy it and store it in `config/autoload/` in a new element with the key `template_id` and then remove `plain` and `html`. When finished, the `mail/templates`  element of the returned array will look like the code below, where `` will be replaced with your template’s id.

With the configuration updated, we now need to add a call to `Mail`’s `setTemplateId` method in `EmailSenderHandler`’s `handle` method, passing in the template id that we just added to `config/autoload/`. You can see an example in the code below.

Let’s test the changes

As before, using cURL, make a GET request to `http://localhost:8080` to test the changes work as expected. You should see `{“status”:202,”message”:””}` output to the terminal, as in the previous example. In your email inbox, you should see a lovely email, with the substitutions replaced, as in the screenshot below.

PHP library

Attach a PDF invoice

Now that we’re using dynamic templates, let’s attach the PDF invoice which we talked about at the top of the article. To save you the time and effort of looking for or creating one yourself, I’ve created a sample PDF invoice that you can use for this article. Save it in the application’s `data` directory.

Note: All of the data in it, except for the line items, is completely fictitious.

To attach an invoice we need to make use of `Mail`’s `addAttachment` method, which you can see in the next code example. The method takes five arguments, but we’re only supplying the first four. These are:

  1. An `Attachment` object or Base64 encoded string. If the value of this parameter is not base64 encoded, the method will do that for us. 
  2. The attachment’s mime type (now known as media type).
  3. The attachment’s file name. This is the name that the file will be saved as, by default, unless the user changes it.
  4. The attachment’s content disposition. If you’re not familiar with content-disposition, inline content-disposition means that the attachment should be automatically displayed when the message is displayed, and attachment content-disposition means that the attachment is not displayed automatically and requires some form of action from the user to open it

In the code example below, PHP’s file_get_contents method reads the contents of the file into a new variable named `$invoice`. Then, the PDF is attached to the message by calling the `addAttachment` method. 

Here, we:

  • Pass in the contents of the invoice, which will be Base64 encoded
  • Set the mimetype to `application/pdf` as we’re attaching a PDF file
  • Set the invoice’s file name to a fictitious name which a customer might reasonably expect; and 
  • Set the content disposition to `attachment`

Now that we’ve finished those changes, let’s test that they work. Run the same cURL request that we ran the previous two times. Then, in your email inbox, you should see a lovely email with the example PDF invoice visible when viewing the email.


That’s how to send email with PHP using SendGrid and Mezzio

While we only scratched the surface of what’s possible when sending emails with SendGrid and Mezzio, you can now send an email with a plain text and HTML body, as well as with an attachment. You’ve also learned how to use Transactional Templates, and substitutions that can be set globally and on a per-recipient basis.

I strongly encourage you to have a look at the PHP library’s documentation to see what else is available, such as scheduling email sends, attaching a file from Amazon S3, adding headers, and adding sections and categories.

Matthew Setter is a PHP Editor in the Twilio Voices team and — naturally — a PHP developer. He’s also the author of Mezzio Essentials. When he’s not writing PHP code, he’s editing great PHP articles here at Twilio. He can be reached via:

Leave A Reply

Your email address will not be published.