Hype Someone Up This Christmas With Laravel Sail, Twilio Programmable Voice, and SendGrid

December 08, 2022
Written by
Diba Kalantari
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Hype Someone Up This Christmas With Laravel Sail, Twilio Programmable Voice, and SendGrid

In this tutorial, you'll bring a little joy to people you know this Christmas by building an app with Laravel, Laravel Sail, Twilio Programmable Voice and Programmable SMS, and SendGrid.

Specifically, you'll give a gift of good feeling to someone special in your life, by letting them receive inspirational quotes every single day, and in order to avoid boring them, you will do this through three different channels: SMS, phone calls, and email. That’s why I called the project Hype Someone Up.

Prerequisites

To implement this project you will need the following:

Create the application

Initialize the Laravel project

The project will use Laravel Sail as a development environment to save you time setting up the application's dependencies. If you're not familiar with it, Laravel Sail, as the official documentation says, is: 

a light-weight command-line interface for interacting with Laravel's default Docker development environment, and provides a great starting point for building a Laravel application.

To create a new Laravel project using it, run the following command:

curl -s "https://laravel.build/hype-someone-up" | bash

After the project has been created, navigate to the new project's directory and start Laravel Sail by running the commands below

cd hype-someone-up
./vendor/bin/sail up -d 

Now, you will be able to access the project in your browser of choice by opening http://localhost. It should look similar to the screenshot below.

The default Laravel route running in Firefox

Install Twilio's PHP helper library

The next step is to install Twilio's PHP helper library, which simplifies interacting with Twilio's APIs in PHP, by running the command below:

./vendor/bin/sail composer require twilio/sdk 

Set the required credentials

Then, you need to set the credentials that the application will need to interact with both Twilio and SendGrid. Start off by, at the end of .env, adding the following variables.

TWILIO_SID=<<Your account SID>>
TWILIO_AUTH_TOKEN=<<Your auth token>>
TWILIO_SOURCE_NUMBER=<<The number you have purchased>>

Retrieve Account SID and Auth Token from Twilio

Then, you have to retrieve your Twilio Account SIDAuth Token, and phone number from your Twilio account. To do that, login to the Twilio Console, and under "Account Info", copy the Account SID, Auth Token, and phone number and paste them in place of the three respective placeholders that you previously added at the end of .env.

Twilio Console Account Info

Create a SendGrid API key

Next, you need to create and set a SendGrid API key. To do that, log in to your SendGrid account and, in the dashboard under "Settings > API Keys", click "Create API Key".

Create a SendGrid API Key form

Then, add a meaningful name for your API key, click "Create & View", copy your API key and paste it as the value of the MAIL_PASSWORD key in .env.

In addition, you need to update the following MAIL_ settings in .env:

MAIL_MAILER=smtp
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=<<Your SendGrid Username>>
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=<<A SendGrid email address which has a verified identity>>

After setting these values, in the /config directory you need to create a file called twilio.php. Then, paste the following code into it.

<?php

return [
   'sid' => env('TWILIO_SID', ''),
   'auth_token' => env('TWILIO_AUTH_TOKEN', ''),
   'source_number' => env('TWILIO_SOURCE_NUMBER', ''),
];

It’s not a good practice to use .env variables inside code other than in config files – especially if you are running config:cache command during your deployment process; your .env file will not be loaded once configuration has been cached.

In addition to the Twilio configuration, you need to store the receiver's information somewhere. In a real world project you would probably have receiver info stored inside a database, or you would get it via user input.

But as this small project has one specific receiver, it will be stored in .env. So add the following settings to the end of .env.

QUOTE_RECEIVER_NUMBER=<<you arbitrary receiver number>>
QUOTE_RECEIVER_EMAIL=<<you arbitrary receiver email address>>

Next, you have to create another config file in the /config directory called receiver.php, and add the following code to the file.

<?php

return [
    'receiver_number' => env('QUOTE_RECEIVER_NUMBER', ''),
    'receiver_email' => env('QUOTE_RECEIVER_EMAIL', '')
];

Generate the command

Now that the configuration is done, run the following command to generate a Command class, named SendQuotes, to send quotes to the receiver.

./vendor/bin/sail artisan make:command SendQuotes

Before getting into the code of this class, you need to create an interface for all the messengers (SMS, Call, and Email). That way you can randomly choose one of them as a communication channel of the day. This interface will work as a contract between the three implementations and enforce all classes to implement the send() method.

Inside the app directory create a new directory named Messengers. Then, in app/Messengers, create a new file named MessengerInterface.php. In that file, paste the code below:

<?php

namespace App\Messengers;

interface MessengerInterface
{
    public function send(string $text);
}

Next, create three implementations of this interface, in three new files inside the app/Messengers directory, respectively named Call.php, Email.php, and SMS.php.

Paste the code below into app/Messengers/Call.php.

<?php

namespace App\Messengers;

use Twilio\Rest\Client;
use Twilio\TwiML\VoiceResponse;

class Call implements MessengerInterface
{
    public function send(string $text)
    {
        (app('provider'))
            ->calls
            ->create(
                config('receiver.receiver_number'),
                config('twilio.source_number'),
                [
                    'url' => route(
                        'call.text', 
                        [
                            'text' => urlencode($text)
                        ]
                    )
                ]
            );
    }
}

Paste the code below into app/Messengers/Email.php.

 

<?php

namespace App\Messengers;

use Illuminate\Support\Facades\Mail;

class Email implements MessengerInterface
{
   public function send(string $text)
   {
       Mail::raw($text, function ($message) {
           $message
                ->to(config('receiver.receiver_email'))
                ->subject("Get Ready to feel good");
       });
   }
}

And paste the code below into app/Messengers/SMS.php.

 

<?php

namespace App\Messengers;

class SMS implements MessengerInterface
{
   public function send(string $text)
   {
       app('provider')
            ->messages
            ->create(
                config('receiver.receiver_number'),
                [
                    'from' => config('twilio.source_number'),
                    'body' => $text
                ]
            );
   }
}

Now, you might be wondering what app(‘provider’), referenced in Call and SMS, is. Instead of repeating yourself by repeatedly creating a new Twilio client, this code generates the client inside AppServiceProvider's boot() method, binds the instance into the Laravel service container, allowing it to be used wherever you want. A very handy timesaver!

To do all this, update the boot() method in app/Providers/AppServiceProvider.php to match the code below:

 

public function boot()
{
   $this->app->bind('provider', function () {
       return new Client(
           config('twilio.sid'),
           config('twilio.auth_token'),
       );
   });
}

This approach also has the benefit of allowing you to switch the provider as and when you need to. For example, during testing you will be able to bind a fake provider instead of calling Twilio's APIs.

TwiML URL

As you can see in app/Messengers/Call.php, above, a URL is being passed as the third argument to the create() method, the 'call.text' route. This URL will be called to get the TwiML (Twilio Markup Language) of the quote that needs to be played when the user answers the phone.

To define the route, add the code below to the bottom of /routes/api.php.

 

Route::post('/get-call-text', function (Request $request, VoiceResponse $voiceResponse) {
   $voiceResponse->say($request->text);
   echo $voiceResponse;
})->name('call.text');

Then, include the use statement below, at the top of the file.

use Twilio\TwiML\VoiceResponse;


In this project, routes/api.php will only contain one route. That’s why creating a new controller class is not required. Bear in mind that this is not the best practice for all the projects.

Authentication of the URL

The last thing that you need to take care of is securing the above URL. No one, other than Twilio, should be able to access it, so you need to add some sort of authentication to the route.

How to do so has been thoroughly described in the Twilio webhook security guide, if you'd like to know more. What you need to do is to add a middleware to this route. First, generate the middleware with the following command:

./vendor/bin/sail artisan make:middleware TwilioWebhookMiddleware

It will be created in app/Http/Middleware/ and named TwilioWebhookMiddleware.php.

Now, replace the new middleware's code with the following:

<?php

class TwilioWebhookMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        $requestValidator = new RequestValidator(config('twilio.auth_token'));
        $requestData = $request->post();

        if (array_key_exists('bodySHA256', $requestData)) {
            $requestData = $request->getContent();
        }

        $isValid = $requestValidator->validate(
            $request->header('X-Twilio-Signature'),
            $request->fullUrl(),
            $requestData
        );

        if ($isValid) {
            return $next($request);
        }

        abort(401,'You are not Twilio!');
    }
}

Then, add the following use statement to the top of the file.

use Twilio\Security\RequestValidator;

After creating the middleware, you have to apply it to the route you previously added to routes/api.php. Do that by updating the file to match the following code.


Route::middleware(TwilioWebhookMiddleware::class)
    ->post(
        '/get-call-text', 
        function (Request $request, VoiceResponse $voiceResponse) {
            $voiceResponse->say($request->text);
            echo $voiceResponse;
        }
    )->name('call.text');

Then, add the following use statement to the top of the file.

use App\Http\Middleware\TwilioWebhookMiddleware;

Write the command

In this step you are going to use the classes that you created to implement the functionality of the command class. Update app/Console/Commands/SendQuotes.php to match the following code.


<?php

namespace App\Console\Commands;

use App\Enums\Messenger;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;

class SendQuotes extends Command
{
   /**
    * The name and signature of the console command.
    *
    * @var string
    */
   protected $signature = 'send:quote';

   /**
    * The console command description.
    *
    * @var string
    */
   protected $description = 'This command will get a quote from php artisan inspire command and send it to specified used randomly via sms, call or email using Twilio';

   /**
    * Execute the console command.
    *
    * @return int
    */
   public function handle()
   {
       Artisan::call('inspire');

       $quote = Artisan::output();

       $options = Messenger::cases();
       $todayMessenger = $options[array_rand($options)];

       (new $todayMessenger->value())->send($quote);
   }
}

Then, create a new directory named Enums and in that directory a new PHP file named Messenger.php. In that new file, paste the code below.

<?php

namespace App\Enums;

enum Messenger: string
{
   case CALL = 'App\Messengers\Call';
   case SMS = 'App\Messengers\SMS';
   case EMAIL = 'App\Messengers\Email';
}

The Messenger class is a PHP 8.1 enum (or enumeration) that contains namespaces of all the Messenger implementations. You can get all the cases of this class by using Messenger::cases(), and you can access the value of each case through the value property. As you can see, you have to choose one of these cases randomly with array_rand() and call the send() method of that implementation.

Schedule the command

The only thing missing is the scheduler to run this command daily at 6 O’Clock. In order to do that you have to add the below line inside the schedule() method of app/Console/Kernel.php.

$schedule->command('send:quote')->dailyAt('06:30');

Testing

In order to test the scheduler locally, run the following command:

./vendor/bin/sail artisan schedule:work

 

You don’t need to wait until 6:30 in the morning to check your result. What I recommend you do is to change the dailyAt() method to everyMinute() and let the scheduler run your code, and change it back after your test is finished.

Here is an example of what an SMS will look like.

Example of a quote being sent by SMS

Conclusion

With this small project, you learnt how to implement Twilio Programmable SMS, voice and SendGrid using Laravel. I hope this article helped you to grasp a general idea of how to implement different communication services easily and with flexibility. I also hope that it helps you bring a little joy to someone special in your life.

Diba Kalantari is a backend developer at Smile IT Solutions which is a software development company based in London. You can reach her through Linkedin.