Send Recurring Emails in PHP with SendGrid and Laravel 7 Task Scheduler

March 24, 2020
Written by
Michael Okoko
Contributor
Opinions expressed by Twilio contributors are their own

Send Recurring Emails with SendGrid and Laravel 7 Task Scheduler

Occasionally, your application needs to perform some routine tasks in timed intervals such as sending out weekly reports to an administrator or sending monthly updates to your users.

Traditionally, such routine tasks are carried out via individual cron jobs that trigger a shell script or the part of the code to be executed. As the application grows and the number of tasks we need to run increases, cron jobs quickly become difficult to maintain, especially as they don’t come with error reporting/handling features and they can’t be placed in a Version Control System such as Git.

The Laravel Task Scheduler allows us to define our tasks as code while leveraging all of the logging and error handling features available in the framework. With the scheduler, we only need to define a single cron entry that executes the schedule at an interval.

What are we Building?

In this tutorial, we will be building an application that uses SendGrid and the Laravel Task Scheduler to send daily reports of the number of new signups each day.

Pre-requisites

To complete this tutorial you will need:

Setting up our Sample Application

Create a new Laravel application using Composer and enter into the directory.

$ composer create-project --prefer-dist laravel/laravel daily-metrics
$ cd daily-metrics

Next, open your favorite IDE and update the database-related variables in your .env file with your setup configuration.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=daily_metrics
DB_USERNAME=root
DB_PASSWORD=

This assumes that there is an existing database named daily_metrics. If none exists, you can create it at this point.

By default, Laravel comes with an auto-generated users migration file at database/migrations/2014_10_12_00000_create_users_table.php. This would be the only migration file we need as it contains all that we need for this application. Run php artisan migrate in the project root to apply the schema defined in the migration file to your database. It will generate the users table with the columns defined in the up method of the file.

Add the Laravel Authentication

Now that the users table has been created, we need a way to register users. Laravel 6+ has decoupled authentication by creating a package to scaffold all of the routes and views needed for registration. In the project directory, run the following commands:

$ composer require laravel/ui
$ php artisan ui vue --auth

After successful installation, launch the Laravel application by running php artisan serve and navigate to http://127.0.0.1:8000/ to register a user. We will need to have a user added to the database in order to test the application at the end of the tutorial.

Installing and Setting Up the SendGrid Library

The SendGrid SDK helps us to easily use the SendGrid API in our PHP applications, thus, we would be using it in place of the built-in Laravel Mail. Install the library with the command below:

$ composer require sendgrid/sendgrid

Next, retrieve your SendGrid API key from your SendGrid API Keys Settings and append it to your .env file, alongside your preferred recipient email and name as shown:

SENDGRID_API_KEY="YOUR_SENDGRID_API_KEY"
ADMIN_EMAIL="YOUR_EMAIL"
ADMIN_NAME="Daily Metrics"

Creating our Recurring Task

Tasks for the Scheduler can be defined in various forms including Closures, Queued Jobs, Artisan commands, and PHP invokable objects. In this case, we would be using an invokable object as it provides enough flexibility for us to achieve what we plan to do.

Create a new Tasks directory in your app folder i.e, in the same directory as Console and Http and add the SendDailyMetrics.php file in the new folder by running the commands below in your terminal.

$ mkdir -p app/Tasks
$ touch app/Tasks/SendDailyMetrics.php

In your editor, open the SendDailyMetrics.php file and add the code block below to it. Comments have been added to aid inclarity.

<?php
namespace App\Tasks;

use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use SendGrid\Mail\From;
use SendGrid\Mail\Mail;
use SendGrid\Mail\To;

class SendDailyMetrics
{
        public function __invoke()
        {
            // retrieve user count as at previous day
            $yesterday = Carbon::yesterday()->format('Y-m-d');
            $usersFromYesterday = User::whereDate('created_at', $yesterday)->count();

            // retrieve user count as at today
            $today = Carbon::today()->format('Y-m-d');
            $usersToday = User::whereDate('created_at', $today)->count();

            // calculate the percentage difference
            if ($usersFromYesterday > 0) {
                $percentageDiff = ((abs($usersFromYesterday - $usersToday)) / $usersFromYesterday) * 100;
                if ($usersFromYesterday > $usersToday) {
                    // convert to it's negative equivalent if the number of users declined
                    $percentageDiff = -1 * $percentageDiff;
                }
                $this->sendEmail($usersToday, $percentageDiff);
            }
            $this->sendEmail($usersToday);
        }

        private function sendEmail(int $userCount, $percentageDiff = 0) {
            // only append the '%' sign if an actual percentage was provided.
            $percentageDiff = ($percentageDiff == 0) ? $userCount : $percentageDiff."%";

            $name = getenv('ADMIN_NAME');
            $htmlContent = "<p>Hi $name, We got <b>$userCount</b> new users today,
                a <b>$percentageDiff</b> difference from yesterday</p>";
            $textContent = "Hi $name, We got $userCount new users today,
                a $percentageDiff difference from yesterday</p>";
            $from = new From(getenv('ADMIN_EMAIL'), getenv('ADMIN_NAME'));
            $subject = "Here's how our app performed today.";
            $recipient = new To(getenv('ADMIN_EMAIL'), getenv('ADMIN_NAME'));

            $email = new Mail();
            $email->setFrom($from);
            $email->setSubject($subject);
            $email->addTo($recipient);
            $email->addContent("text/plain", $textContent);
            $email->addContent("text/html", $htmlContent);

            $sendgrid = new \SendGrid(getenv('SENDGRID_API_KEY'));
            try {
                $response = $sendgrid->send($email);
                $context = json_decode($response->body());
                if ($response->statusCode() == 202) {
                    Log::info("Metric email has been sent", ["context" => $context]);
                }else {
                    Log::error("Failed to send metric email", ["context" => $context]);
                }
            } catch (\Exception $e) {
                Log::error($e);
            }
        }
}

The __invoke magic method above is what makes instances of the SendDailyMetrics class invocable objects. They are executed when such instances are called as functions (which is how the Laravel Task Scheduler invokes them).

When the __invoke method is called, we first retrieve the number of users that registered the previous day and the number that registered on the current day. Also, we calculate the percentage based on these two numbers which are then sent to the admin email as the day’s metric.

Next, we will update the schedule method of our Console’s Kernel class to execute the task everyday at 11:59 PM, and send whatever output it has to a log file we have explicitly specified. That way, Laravel is also aware of our new Task. Open the Kernel.php file in the app/Console/ directory and replace it’s schedule method with the code below:

protected function schedule(Schedule $schedule)
        {
            $tasksLog = storage_path('/logs/tasks-output.log');
            $schedule->call(new SendDailyMetrics)
                ->daily()
                ->at("23:59")
                ->appendOutputTo($tasksLog);
        }

NOTE: Remember to import the SendDailyMetrics class.

Testing our Application

Our application requires only the cron entry below to execute any number of tasks that are available. It will execute the scheduler every minute, which will in turn, go through the scheduled tasks and execute the ones that are due

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

NOTE: If you are unfamiliar with adding cron entries, the cron man page could give you a headstart or you can consider using tools like Laravel Forge.

While developing locally, you can lower the schedule frequency to a minute and directly execute php artisan schedule:run to get the email immediately.

Conclusion

Recurring tasks are a frequently needed feature of modern applications and this tutorial provides a guide on implementing such features with Laravel, while also going through how to send emails with the Twilio SendGrid SDK.

Michael Okoko is a student currently pursuing a degree in Computer Science and Mathematics at Obafemi Awolowo University, Nigeria. He loves open source and is mostly interested in Linux, Golang, PHP, and fantasy novels! You can reach him via: