Build a Bulk SMS Sender With Twilio, PHP, and Yii2

August 24, 2021
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Build a Bulk SMS Sending Application With Twilio, PHP, and Yii2

Notifications are an essential feature of an application because they provide an outlet for them to keep the customers updated with activities relating to their accounts. SMS notifications, specifically, provide a large reach due to the sheer number of phones in circulation. They also have the added advantage of accessibility because even customers using feature phones are able to receive notifications.

Consequently, the question then becomes: how do you send SMS notifications to a large number of phones efficiently? This is because sending SMS notifications sequentially will likely create a scalability bottleneck.

So in this article, I will show you how to send SMS notifications in bulk using Twilio, which makes managing/dispatching programmable SMS a breeze, and PHP's Yii2 framework. We will build an application that will send customized SMS notifications in bulk to clients via Twilio's SMS API.

Tutorial Requirements

To follow this tutorial, you need the following components:

  • PHP 7.4. Ideally version 8.
  • Composer installed globally.
  • A free or paid Twilio account. If you are new to Twilio click here to create a free account now and receive a $10 credit when you upgrade to a paid account.
  • A smartphone with an active phone number.

Getting Started

To get started, create a new application named bulk_sms_app, and switch to the project directory using the following commands.

composer create-project --prefer-dist yiisoft/yii2-app-basic bulk_sms_app
cd bulk_sms_app

Install the required dependencies

The application needs one external dependency, the Twilio PHP Helper library, so that it can communicate with Twilio. Install it using the following command.

composer require twilio/sdk

Test that the application works

Then start the application using the following command.

php yii serve

By default, the application will be available at http://localhost:8080/. Open the URL in your preferred web browser where you should see the default page, as shown below.

The default Yii2 home page

Return to the terminal and press Control + C to quit the application.

Update the application's configuration

For the application to work, it needs several configuration options to be set. These are your Twilio Account SID, Auth Token, and phone number, as well as a separate messaging ID.

Before those are retrieved, update config/params.php to match the following, so that the settings are available with placeholder values.

<?php

return [
    'TWILIO_ACCOUNT_SID'   => "your_twilio_account_sid",
    'TWILIO_AUTH_TOKEN'    => "your_twilio_auth_token",
    'TWILIO_MESSAGING_SID' => "your_twilio_messaging_sid",
    'TWILIO_PHONE_NUMBER'  => "your_twilio_phone_number",
];

Next, from the Twilio Dashboard retrieve your Twilio phone number, "ACCOUNT SID", and "AUTH TOKEN". Replace each of the respective placeholder values in config/params.php with these three values.

As a security precaution, your AUTH TOKEN will not be shown on screen. Click on the Copy icon to copy it.

Then, you need to set up a messaging service so that you can retrieve its messaging id. To do this, open the Twilio console and navigate to "All Products and Services > Programmable Messaging > Messaging Services". Once there, click the blue "Create Messaging Service" button.

Twilio&#x27;s Messaging Service Page

Set "Messaging Service friendly name" to "yii_bulk_sms" as our use case is to notify users, and click "Create Messaging Service".

After that, we need to add a sender to our service. Specifically, we need a phone number. Click "Add Senders", select "Phone Number", and then click "Continue".

If you don't have one (or need a new one), you can buy more numbers.

Select Phone Number Section

Then, finish up by clicking "Step 3: Set up integration", clicking "Step 4: Compliance info" on the following page, and finally "Complete Messaging Service Setup" on the page after that.

Now that the service has been created, copy the Messaging Service SID and use it to replace TWILIO_MESSAGING_SID's placeholder value in config/params.php.

Programming Messaging Service Properties

Set up the database

For this application, we will use SQLite for the database. Create a new directory at the root of the application called db. In that directory, create a file named app.db. Then, update config/db.php to match the following code.

<?php

return [
    'class'   => 'yii\db\Connection',
    'dsn'     => 'sqlite:' . dirname(__DIR__) . '/db/app.db',
    'charset' => 'utf8',
];

Make sure that the user you're using to run your code has write permissions on the db directory.

With that done, we now need to create several database migrations to simplify scaffolding the database. The first one will create a user table named "client". This table will have two columns; one for the name and one for the phone number.

Use the yii migrate/create command to create it, providing the name of the migration to be created, as in the example below.

php yii migrate/create create_client_table

When prompted for confirmation, enter "yes" and press Enter.

By default, migration files are located in the migrations directory in the project's root directory. Migration file names are prefixed with the letter m and the UTC datetime of its creation. For example m210809_113722_create_client_table.php.

Open migrations/m<YYMMDD_HHMMSS>_create_user_table.php which was just created and modify the safeUp and safeDown functions to match the following code.

public function safeUp() 
{
    $this->createTable('client', [
        'id'          => $this->primaryKey(),
        'name'        => $this->string()->notNull(),
        'phoneNumber' => $this->string(12)->notNull(),
    ]);
}

public function safeDown() 
{
    $this->dropTable('client');
}

The second migration will seed the client table with a set of fake data so that our application has some data to work with. Create it using the following command.

php yii migrate/create seed_client_table

As before, when asked for confirmation, enter "yes" and press Enter. Then, open migrations/m<YYMMDD_HHMMSS>_seed_client_table.php and modify the safeUp function to match the following code.

public function safeUp() 
{
    $this->insertFakeMembers();
}

private function insertFakeMembers() 
{
    $faker = Faker\Factory::create();

    for ($i = 0; $i < 1000; $i++) {
        $this->insert(
            'client',
            [
                'name'        => $faker->name(),
                'phoneNumber' => $faker->e164PhoneNumber()
            ]
        );
    }
}

This will insert 1,000 records into the client table.

With the two migrations created, run them using the following command:

php yii migrate

When asked for confirmation, type “yes” and press Enter. After the migrations have been run, you can verify that the database has been created and seeded appropriately using the sqlite3 command-line tool, as in the example command below.

sqlite3 db/app.db "select * from client limit 100;"

The first 100 clients in the database will be printed to the command line, similar to the screenshot below.

Listing the seeded user records in the terminal

Creating Client Models

Instead of writing raw SQL queries to interact with the database, we will create an ActiveRecord model which will manage the query creation and execution for us. What's more, by doing this we will have an object-oriented means of accessing and storing data in the database. Create a model for the Client entity using the command below. When prompted, press Enter.

php yii gii/model --tableName=client --modelClass=Client

The new class will be created in a new directory named models, located in the project's root directory.

Create the client Controller

With the database and models in place, we can now create Controllers to handle the display of clients and the dispatch of SMS notifications. Create a Controller to handle client-related requests using the following command; Type “yes” and press Enter when prompted.

php yii gii/controller --controllerClass="app\controllers\ClientController"

When finished, two new files will have been created: controllers/ClientController.php (which extends yii/web/Controller), and views/client/index.php.

To allow the API to handle POST requests, CSRF validation will be disabled in ClientController.php. To do that, add the following to the start of the class.

public $enableCsrfValidation = false;

Disabling CSRF without additional checks in place may expose your application to security threats.

Before we create actions to handle requests, we need to add routing rules for the new API  endpoints. To do that, in config/web.php uncomment the urlManager component and add the code below to its ruleselement.

'GET clients' => 'client/index',
'POST clients/notify' => 'client/notify',

Using the shorthand notation provided by Yii, we can specify the route for a given URL. The URL is provided as a key with the route to the appropriate action as a value. By prefixing the URL with an HTTP verb, the application is able to handle URLs with the same pattern or different actions accordingly. You can read more about this here.

The application will have two routes: /clients and /clients/notify. The first route (/clients) will be used to display the list of clients saved in the database. This will be handled in ClientController.php by a function named actionIndex.

The second route (/clients/notify) will be used to send SMS notifications in bulk to the provided clients. It will also be handled in ClientController.php by a function named actionNotify.

To allow our controllers to parse JSON requests, we need to add a JSON parser to our application component. The components array within config/web.php contains a request array that holds the configuration for the request Application Component.

To add the JSON parser, add the following to the request array, after the cookieValidationKey element.

'parsers' => [
    'application/json' => 'yii\\web\\JsonParser',
]

Then, update controllers/ClientController.php to match the following example.

<?php

namespace app\controllers;

use app\models\Client;
use Yii;
use yii\data\Pagination;
use yii\web\Controller;

class ClientController extends Controller 
{
    public $enableCsrfValidation = false;

    public function actionIndex() 
    {

        $query = Client::find();
        $count = $query->count();
        $pagination = new Pagination(['totalCount' => $count]);

        $clients = $query->offset($pagination->offset)
                         ->limit($pagination->limit)
                         ->all();

        $this->view->title = 'Clients';

        return $this->render(
            'index',
            [
                'clients'    => $clients,
                'pagination' => $pagination
            ]
        );
    }

    public function actionNotify()
    {
        $request = Yii::$app->request;
        $clients = $request->post('clients');
        $smsContent = $request->post('smsContent');

        return $this->asJson(
            [
                'message' => 'Notifications sent successfully'
            ]
        );
    }
}

In the actionIndex function, we use pagination to retrieve the clients from the database in batches. These clients, along with the pagination object, are passed to the view located in views/layouts/main.php.

The actionNotify function does three things; it:

  1. Takes the request and retrieves the clients and smsContent keys.
  2. Returns a JSON response with a success message. Dispatches the messages to the selected clients.

Create the clients' view

The clients' list will be displayed in a table. Beside each client in the list, there will be a checkbox to indicate which of the clients has been selected, and at the bottom of the table will be a button to send an SMS to all the selected clients. You can see a mockup in the image below.

Mockup of the clients&#x27; view

Clicking on this button will trigger a pop-up into which the SMS content is typed. Submitting that form will make a POST request containing the content of the SMS and the clients to be notified. Once the request has been completed successfully, an alert will be displayed.

Before we edit the view, let's edit the main layout to remove the default Yii2 header and footer, and import SweetAlert via CDN. SweetAlert will be used to display the popup form and notifications.

Open views/layouts/main.php and update it to match the following.

<?php

/* @var $this \yii\web\View */

/* @var $content string */

use app\assets\AppAsset;
use yii\helpers\Html;

AppAsset::register($this);
?>
<?php
$this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
    <meta charset="<?= Yii::$app->charset ?>">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <?php $this->registerCsrfMetaTags() ?>
    <title><?= Html::encode($this->title) ?></title>
    <?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>

<div class="container">
    <?= $content ?>
</div>
<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

Next, update views/client/index.php to match the following.

<?php
/* @var $this yii\web\View */
use yii\widgets\LinkPager;
?>
<h1>Clients</h1>
<table class="table" id="clientsTable">
    <thead>
    <tr>
        <th>&nbsp;</th>
        <th>#</th>
        <th>Name</th>
        <th>Phone number</th>
    </tr>
    </thead>
    <?php
    foreach ($clients as $i => $client): ?>
        <tr>
            <td><input type="checkbox"/></td>
            <td style="display: none"><?= $client->id ?></td>
            <td><?= $i + 1 ?></td>
            <td><?= $client->name ?></td>
            <td><?= $client->phoneNumber ?></td>
        </tr>
    <?php
    endforeach; ?>
</table>

<button
        class='btn btn-primary'
        style="display: block; margin: auto"
        onclick="sendSMS()"
>Send SMS</button>
<div style="width: 50%; margin: auto">
    <?php
    echo LinkPager::widget(
        [
            'pagination' => $pagination,
        ]
    ); ?>
</div>

<script>
    const getSelectedClients = () => {
        const table = document.getElementById('clientsTable');
        const checkboxes = Array.from(table.getElementsByTagName('input'));
        const selectedClients = [];
        checkboxes
            .filter(checkbox => checkbox.checked)
            .forEach(checkbox => {
                    let row = checkbox.parentNode.parentNode;
                    selectedClients.push(row.cells[1].innerHTML)
                }
            )

        return selectedClients;
    }

    const sendSMS = () => {
        Swal.fire({
            title: 'Enter the content of the SMS',
            input: 'textarea',
            inputAttributes: {
                autocapitalize: 'off'
            },
            showCancelButton: true,
            confirmButtonText: 'Send',
            showLoaderOnConfirm: true,
            preConfirm: (smsContent) => {
                const data = {
                    clients: getSelectedClients(),
                    smsContent
                };
                return fetch('clients/notify', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify(data)
                }).then(response => response.json()).then(data => {
                    Swal.fire({
                        title: data.message,
                    })
                }).catch(error => {
                        Swal.showValidationMessage(
                            `Request failed: ${error}`
                        )
                    })
            },
            backdrop: true,
            allowOutsideClick: () => !Swal.isLoading()
        })
    }
</script>

In addition to rendering a table containing the contents, we added a script that contains two functions: sendSMS and getSelectedClients.

sendSMS is called when the Send SMS button is clicked. This function triggers the pop-up form where the SMS content is to be typed in. Once the form is submitted, the selected clients are retrieved using getSelectedClients.

Along with the content of the SMS, a POST request is made via the fetch API to the clients/notify endpoint. The response message is then shown in an alert which the user can close.

Create a helper class to send SMS in bulk

At the root of the project, create a new folder called helpers. Then, in that directory, using your favorite IDE or text editor, create a new file named TwilioSMSHelper.php, adding the following code to it.

<?php

namespace app\helpers;

use Twilio\Rest\Client;
use Yii;

class TwilioSMSHelper 
{
    private string $phoneNumber;
    private string $messagingSID;
    private Client $twilio;

    public function __construct() 
    {
        $params = Yii::$app->params;
        $accountSID = $params['TWILIO_ACCOUNT_SID'];
        $authToken = $params['TWILIO_AUTH_TOKEN'];
        $this->phoneNumber = $params['TWILIO_PHONE_NUMBER'];
        $this->messagingSID = $params['TWILIO_MESSAGING_SID'];
        $this->twilio = new Client($accountSID, $authToken);
    }

    public function sendBulkNotifications(array $clients, string $message) 
    {
        foreach ($clients as $client) {
            $this->twilio->messages->create(
                $client->phoneNumber,
                [
                    'body'                => "Dear {$client->name} \n\n$message",
                    'from'                => $this->phoneNumber,
                    'messagingServiceSid' => $this->messagingSID
                ]
            );
        }
    }
}

Since we used FakerPHP to generate the phone numbers, you can hardcode a valid phone number in the create function for testing purposes.

In the constructor, we initialize a Twilio Client object using the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN which we set in config/params.php. We also pull the phone number and Messaging Service ID which are passed as part of the message configuration. These are saved as private fields in the class. In the sendBulkNotifications function, we loop through the clients and create a new message for each one.

There is no need to add a delay in your logic. You can send as many messages as you’d like because Twilio will queue them up for delivery at your prescribed rate limit.

Add the bulk notification feature

In controllers/ClientController.php, update the actionNotify function to match the following.

public function actionNotify() 
{
        $request = Yii::$app->request;
        $clientIds = $request->post('clients');
        $smsContent = $request->post('smsContent');
        $clients = [];
        foreach ($clientIds as $clientId) {
            $clients[] = Client::findOne($clientId);
        }
        $smsHelper = new TwilioSMSHelper();
        $smsHelper->sendBulkNotifications($clients, $smsContent);

        return $this->asJson(
            [
                'message' => 'Notifications sent successfully'
            ]
        );
    }

Don't forget the import statement for the TwilioSMSHelper class, below.

use app\helpers\TwilioSMSHelper;

With this in place, we can send custom SMS notifications to our clients.

Remember that we used FakerPHP to generate the random and invalid phone numbers for each client in the database? Twilio's API will not be able to connect those numbers if selected and throw an error in the process.

For testing purposes, you can alter one or two numbers and change to valid ones by using the following script:

sqlite3 db/app.db "UPDATE client SET phoneNumber = '+2349057042039' WHERE id = 2;"

This will change the value of the second phone number in the database to the one specified above.

Now, start the application again, by running php yii serve in the terminal in the project's root directory. Then, open http:://localhost:8080/client in your browser of choice and select some clients.

Selected / All Client Lists

After that, click the "Send SMS" button to bring up the form, type in a message, and click Send. You will get the success notification along with the text messages sent to the provided phone number.

SMS content box

SMS notification example

Conclusion

In this article, we looked at how to send bulk SMS notifications using Twilio. Integration of Twilio SDK simplifies the process of creating and dispatching notifications.

In addition, you don't have to worry about queuing and the delivery process as Twilio infrastructure is capable of handling notifications for your users in real-time.

The entire codebase for this tutorial is available on GitHub. Feel free to explore further. Happy coding!

Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest to solve day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile.

A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and content on several blogs on the internet. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.