Build a Spoiler App in PHP with Twilio SMS and Laravel

June 27, 2019
Written by
Dotun Jolaoso
Contributor
Opinions expressed by Twilio contributors are their own

TV Show Spoiler App with Twilio SMS & Laravel.png

In this tutorial, we’ll be building an SMS application that sends out spoiler alerts to our friends every week after the show has been aired. We’ll be making use of Twilio’s Copilot feature to learn how we can send outbound SMS to a large number of people at once.

Technical Requirements

To follow along, you’ll need the following:

Create New Laravel Project

Let’s install a new Laravel project via the Composer create-project command. From your terminal, run the following command:

$ composer create-project --prefer-dist laravel/laravel twilio-got-spoiler

This will install a new Laravel project for us in the twilio-got-spoiler directory.

Creating Models and Migrations

We’ll be needing two migrations. One for storing the spoilers to be sent out and the other for storing the phone numbers that should receive those spoilers.

From your terminal, run the following command:

$ php artisan make:model Spoiler -m 
$ php artisan make:model PhoneNumber -m 

Tip: The -m flag will create a migration file associated with the model.

Next, let’s edit the migration files to reflect the structure each of those tables will need. Head over to the database/migrations directory and edit the spoiler migration with the following code:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateSpoilersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('spoilers', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('message');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('spoilers');
    }
}

Also, edit the phone_number migration with the following code:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePhoneNumbersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('phone_numbers', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('phone_number');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('phone_numbers');
    }
}

Next, edit the .env file with your database details and execute the following command to run all of our migrations.

$ php artisan migrate

Also, it’s important that we identify the attributes on our models that can be mass assignable. Add the following code to the Spoiler and PhoneNumber models respectively. They are located in the app folder.

 protected $fillable = ['message'];
protected $fillable = ['phone_number'];

Saving Spoilers and Phone Numbers

Next, we’ll add the routes for our application that allow us to save spoiler messages, as well as register phone numbers. Edit the routes/web.php file with the following code:

Route::get('/', 'HomeController@index');
Route::post('/spoiler/create', 'HomeController@storeSpoiler')->name('create.spoiler');
Route::post('/phone-number/create', 'HomeController@storePhoneNumber')->name('create.phone.number');

To add the corresponding controller methods, from the terminal, run the following command to create a new controller:

$ php artisan make:controller HomeController 

Next, head over to the app/Http/Controllers directory and edit the HomeController file we just created with the following code:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Spoiler;
use App\PhoneNumber;

class HomeController extends Controller
{
    public function index()
    {
        $spoilers = Spoiler::latest()->get();

        $phoneNumbers = PhoneNumber::all();

        return view('home', compact('spoilers', 'phoneNumbers'));
    }

    public function storeSpoiler(Request $request, Spoiler $spoiler)
    {
        $this->validate($request, [
            'message' => 'required'
        ]);

        $spoiler->create($request->only(['message']));

        return back()->with('success', 'Spoiler has been added successfully');
    }

    public function storePhoneNumber(Request $request, PhoneNumber $number)
    {
        $this->validate($request, [
            'phone_number' => 'required' 
        ]);

        $number->create($request->only(['phone_number']));

        return back()->with('success', 'Phone Number has been added successfully');
    }
}

In the index method, we query the database to fetch all the spoilers and phone numbers we’ve created - which is none so far - and then pass them down to a view file we’ll be creating shortly.

The storeSpoiler method validates the incoming request to ensure a message field is present and not empty, and then stores the spoiler messages to be sent out.

The storePhoneNumbers method also performs a validation check to ensure the phone_number field is required and then stores the phone number.

Building the Frontend

Let’s create the simple view our application will have. We’ll be needing two forms. One for creating the spoiler messages and the other for registering the phone numbers. In the resources/views directory, create a home.blade.php file and add the following code to the file:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>GOT Spoiler With Twilio</title>
    <!-- Bootstrap styles CDN -->
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet"     integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
</head>

<body>
    <div class="container">
        <div class="jumbotron">
            @include('alert')
            <div class="row">
                <div class="col">
                    <div class="card">
                        <div class="card-header">
                            Add Spoiler
                        </div>
                        <div class="card-body">
                            <form action="{{route('create.spoiler')}}" method="post">
                                @csrf
                                <div class="form-group">
                                    <label>Enter Spoiler</label>
                                    <input type="text" name="message" class="form-control">
                                </div>
                                <button type="submit" class="btn btn-primary">Add Spoiler</button>
                            </form>
                        </div>
                    </div>
                </div>
                <div class="col">
                    <div class="card">
                        <div class="card-header">
                            Spoiler Messages
                        </div>
                        <div class="card-body">
                            <form>
                                <div class="form-group">
                                    <label>All Spoilers</label>
                                    <select multiple class="form-control">
                                        @foreach($spoilers as $spoiler)
                                        <option>{{$spoiler->message}}</option>
                                        @endforeach
                                    </select>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>

            <br>

            <div class="row">
                <div class="col">
                    <div class="card">
                        <div class="card-header">
                            Add Phone Numbers
                        </div>
                        <div class="card-body">
                            <form action="{{route('create.phone.number')}}" method="post">
                                @csrf
                                <div class="form-group">
                                    <label>Register Phone Number</label>
                                    <input type="tel" name="phone_number" class="form-control">
                                </div>
                                <button type="submit" class="btn btn-primary">Add Number</button>
                            </form>
                        </div>
                    </div>
                </div>

                <div class="col">
                    <div class="card">
                        <div class="card-header">
                            Phone Numbers
                        </div>
                        <div class="card-body">
                            <form>
                                <div class="form-group">
                                    <label>All Phone Numbers</label>
                                    <select multiple class="form-control">
                                        @foreach($phoneNumbers as $phoneNumber)
                                        <option> {{$phoneNumber->phone_number}}</option>
                                        @endforeach
                                    </select>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>

If you take a closer look at the code above, you’ll notice we included an alert partial with the following line @include('alert'). Let’s create the partial in the resources/views directory by  creating a alert.blade.php file and add the following code to the file:

@if (session('success'))
<div class="alert alert-success">
    {{ session('success') }}
</div>
@endif
@if ($errors->any())
<div class="alert alert-danger">
    <ul>
        @foreach ($errors->all() as $error)
        <li>{{ $error }}</li>
        @endforeach
    </ul>
</div>
@endif
@if (session('success'))
<div class="alert alert-success">
    {{ session('success') }}
</div>
@endif
@if ($errors->any())
<div class="alert alert-danger">
    <ul>
        @foreach ($errors->all() as $error)
        <li>{{ $error }}</li>
        @endforeach
    </ul>
</div>
@endif

This partial is used to display any message that has been flashed into the session.

Next, navigate to the app’s homepage and you should see the following page:

Setting up Twilio

We will be using the Twilio SDK for PHP library to interact with Twilio’s API. Run the following command to install the library:

$ composer require twilio/sdk

Handling Bulk SMS using Copilot

We can safely assume that we’ll be sending out spoiler outbound SMS messages to hundreds of users at once. To make this process easy and seamless, Twilio provides a feature called  Copilot which is perfect for bulk messaging.

Before we can make use of Twilio’s Copilot, we first need to create a new Messaging Service. Head over to your Twilio dashboard to create one. Next, you’ll be prompted with a modal similar to the one below.

After you’ve created the messaging service, take note of the Service SID as we’ll be needing it shortly.

Twilio dashboard with message service

Also, it’s important to note that we’ll need to add phone numbers, short codes or an Alpha Sender ID to the Messaging Service we just created. This helps Copilot to select an identity to use when sending our messages.

Environment and Config Variables

Edit the .env file with your Twilio credentials:

TWILIO_ACCOUNT_SID=xxxxx
TWILIO_AUTH_TOKEN=xxxxx
TWILIO_MESSAGING_SERVICE_SID=xxxxx

Next, we’ll add a new Twilio array in the config/services.php file so that we can easily reference the environment variables we just defined.

  'twilio' => [
        'account_sid' => env('TWILIO_ACCOUNT_SID'),
        'auth_token' => env('TWILIO_AUTH_TOKEN'),
        'messaging_service_sid' => env('TWILIO_MESSAGING_SERVICE_SID')
    ]

Creating a Twilio Wrapper

Since our Messaging Service has been configured, we are ready to start sending spoilers. We’ll be using a Twilio wrapper that we’ll create shortly to interact with the Twilio SDK library we installed earlier. Create a new Twilio.php file under the app\Services directory and then add the following code:

<?php

namespace App\Services;

use Twilio\Rest\Client;

class Twilio
{
    protected $account_sid;

    protected $auth_token;

    protected $messagingServiceSid;

    protected $client;

    /**
     * Create a new instance
     *
     * @return void
     */

    public function __construct()
    {
        $this->account_sid = config('services.twilio.account_sid');

        $this->auth_token = config('services.twilio.auth_token');

        $this->messagingServiceSid = config('services.twilio.messaging_service_sid');

        $this->client = $this->setUp();
    }

    public function setUp()
    {
        $client = new Client($this->account_sid, $this->auth_token);

        return $client;
    }

    public function notify($number, $spoiler)
    {
        $message = $this->client->messages->create($number, [
            'body' => $spoiler,
            'messagingServiceSid' => $this->messagingServiceSid
        ]);

        return $message;
    }
}
    

In the constructor, we initialized the Twilio credentials we added to our config\services.php file earlier. We also created a new Twilio Rest Client and assigned it to the client property of our class.

The notify method handles sending outbound SMS using the Twilio API. It accepts two arguments,  the phone number that we’ll be sending the spoiler too as well as the spoiler message itself.

Creating a Job

We’ll make use of a Job to handle dispatching the spoiler alerts to all the phone numbers registered in our application. Run the following command:

$ php artisan make:job SendGotSpoiler

This will create a new class for us in the app/Jobs directory. Edit the file with the following code:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Spoiler;
use App\PhoneNumber;
use App\Services\Twilio;

class SendGotSpoiler implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $spoiler;

    protected $twilio;

    protected $phoneNumbers;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(Twilio $twilio)
    {
        $this->spoiler = Spoiler::latest()->first();

        $this->phoneNumbers = PhoneNumber::all();

        $this->twilio = $twilio;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $twilio = $this->twilio;

        $this->phoneNumbers->map(function ($phoneNumber) use ($twilio) {
             $twilio->notify($phoneNumber->phone_number, $this->spoiler->message);
        });
    }
}

Let’s break this class into parts so that we can understand what’s happening here.

public function __construct(Twilio $twilio)
    {
        $this->spoiler = Spoiler::latest()->first();

        $this->phoneNumbers = PhoneNumber::all();

        $this->twilio = $twilio;
    }

Here, the constructor accepts the Twilio class we created in the previous section as a dependency. We fetch the most recent spoiler that has been created and assign it to the spoiler property of the class. Similarly, we fetch all the registered phone numbers as well and then assign them to the phone numbers property of the class.

  public function handle()
    {
        $twilio = $this->twilio;

        $this->phoneNumbers->map(function ($phoneNumber) use ($twilio) {
             $twilio->notify($phoneNumber->phone_number, $this->spoiler->message);
        });
    }

In the handle method, we map all the registered phone numbers and then send the spoiler message to each of them.

Creating a Custom Command

Let’s create a custom command that we can use via the terminal to trigger sending the spoiler messages. Run the following command:

$ php artisan make:command SendGotSpoilerCommand

This will create a SendGotSpoilerCommand.php file for us in the app/Console/Commands directory. Edit the file with the following code:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Jobs\SendGotSpoiler;

class SendGotSpoilerCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'send-got-spoiler';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Send Game of Thrones Spoiler';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle(SendGotSpoiler $sendGotSpoiler)
    {
        $sendGotSpoiler->handle();

        $this->comment('Hehe, Spoilers have been sent');
    }
}

In this class we import the SendGotSpoiler job, and then pass it as a dependency to the handle method of the class.  We then call the handle method on the SendGotSpoiler job and then display a comment to let us know when the command has finished executing.

Testing

It’s time to finally test what we’ve been building so far. Head over to the homepage and create a spoiler. Then register the phone number that should receive the spoiler.

Frontend view of the spoiler app

Next, run the following command to send the spoiler:

$ php artisan send-got-spoiler

You should see the following

kqjD5AnaeFTqKRfjAGxPOOre-9Nv8QqlrLgmIenFOAcl__jl6n6738KJ9YgnC6W7t-oe4e8YjuMtSaDy0euqVnKdddWgPLmmyht8NEPu3wciZ-NHL5-HLTphf75Adc1p5MbLTI9j

You should receive the following SMS

Z-Qdgw36v37qulHSf2-PZtqegh5kih4pISHQRQ-WkYGpBNuQtD8Fj38_8sWJPuMrlouXrNmfL6zvqRR5y4MT_Xl1IgT1f0EyOX5WiUpPOWXo0eweVA6ejlfLw0BybT8uaQDDIHaY

Taking it Further

My spoiler of choice is for Game of Thrones. Since it aired every Sunday, you can go ahead and schedule a cron job that automatically calls the send-got-spoiler command every Sunday. Note that you’ll always have to create the latest spoiler yourself after the episode has been aired.

Conclusion

In this tutorial, we’ve built a simple spoiler application that sends out spoiler alerts to our friends. We’ve also learned how to send bulk SMS to a group of people at once using Twilio’s Copilot. You can find the repo on Github.

 

Dotun Jolaoso

Twitter: @dotunj_

Github: @dotunj