How to Send Email With PHP Using Twilio SendGrid and Mezzio

How to Send Email With PHP Using Twilio 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 plaintext and HTML bodies, and that includes a PDF attachment. You’re also going to learn how to use Twilio 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.

Sounds 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, the customer receives an email, 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 “SendGrid\Mail\Mail” and “\SendGrid.” “SendGrid\Mail\Mail” is the object that stores all of the properties for an email message that we’ll send through Twilio SendGrid. The “SendGrid” object forms the transport layer, facilitating the sending of emails through Twilio SendGrid.


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

  1. Twilio 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 we scaffold the base application, we’ll 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’ll 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 Twilio SendGrid account details

Now you need to supply the application with your SendGrid API key. To do that, after logging into Twilio 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
With the API key created, click and copy the key, then click “Done.

After that, add 2 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 we’ll use for any emails sent from our application unless overridden.

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

Set application mail configuration details

In addition to the Twilio 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 2 email body templates: one for plaintext 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 we invoke the class, it has access to the application’s dependency injection (DI) container, from which it’ll retrieve the configuration details that we stored in “config/autoload/”

After that, it’ll instantiate a new “SendGrid\Mail\Mail” object and set the from and reply to details by passing the respective configuration details to calls to “Mail,” “setFrom,” and “setReplyTo” methods, respectively. After that, it’ll return the instantiated “Mail” object.

To use it, though, you have to register it 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.<ext>”), which we won’t need. The file name “<ext>” 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 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 3 new parameters. These will initialize 3 new class member variables:

  • A “\SendGrid\Mail\Mail” 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 the “EmailSenderHandler” “handle” method with the code below, then let’s step through what it does.

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 2 arguments and an array of substitutions in the third argument. 

Substitutions allow you to customize email messages for each recipient. In plaintext 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, we set the message’s subject and add a plaintext and HTML message body. With that, our email is ready to send. 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.” This will instantiate “EmailSenderHandler” correctly. To do that, replace the existing definition of its “__invoke`” method with the following code.

This retrieves a “\SendGrid\Mail\Mail” object from the DI container, preinitialized 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. Then, it 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 plaintext 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 plaintext 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, 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. You can create and maintain these through the Twilio 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 Twilio SendGrid glossary defines them as follows:

Transactional email templates are precoded email layouts that marketers, designers, and developers can use to quickly and easily create transactional email campaigns. Twilio SendGrid’s transactional email templates allow nontechnical 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 Twilio SendGrid documentation and create one that has as its content only in 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 the “EmailSenderHandler” “handle” method, replace the 2 calls to “$this->mail->addContent(),” with the following code.

The 2 method calls add the sender details and support email address as global substitutions. These substitutions apply 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 “<the template’s id>” replaces your template’s ID.

With the configuration updated, we now need to add a call to the “Mail” “setTemplateId” method in the “EmailSenderHandler” “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 if 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.

Attach a PDF invoice

Now that we’re using dynamic templates, let’s attach the PDF invoice that 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 the “Mail” “addAttachment” method, which you can see in the next code example. The method takes 5 arguments, but we’re only supplying the first 4. 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 save 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 automatically display when the message displays, and attachment content-disposition means that the attachment does not display 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 attaches to the message by calling the “addAttachment” method.

Here, we:
  • Pass in the contents of the invoice, which will be Base64 encoded
  • Set the MIME type to “application/pdf” as we’re attaching a PDF file
  • Set the invoice’s file name to a fictitious name that a customer might reasonably expect  
  • 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 2 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 Twilio SendGrid and Mezzio

While we only scratched the surface of what’s possible when sending emails with Twilio SendGrid and Mezzio, you can now send an email with a plaintext and HTML body, as well as with an attachment. You’ve also learned how to use transactional templates and substitutions that you can 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:

Recommended For You

Most Popular

Send With Confidence

Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.