用Twilio、PHP和Yii2建立一个群发的短信发送器(详细指南)

695 阅读9分钟

通知是一个应用程序的基本功能,因为它们提供了一个渠道,让客户了解与他们账户有关的最新活动。短信通知,特别是,由于流通中的手机数量庞大,提供了一个很大的覆盖面。它们还有一个额外的优势,那就是可及性,因为即使是使用功能手机的客户也能够收到通知。

因此,问题就变成了:你如何向大量的手机有效地发送短信通知?这是因为按顺序发送短信通知很可能会产生一个可扩展性瓶颈。

所以在这篇文章中,我将向你展示如何使用Twilio和PHP的Yii2框架批量发送短信通知,Twilio使得管理/调度可编程的短信变得轻而易举。我们将建立一个应用程序,通过Twilio的短信API向客户批量发送定制的短信通知

教程要求

要学习本教程,你需要以下组件:

  • PHP 7.4。最好是8版本。
  • 全局安装的Composer
  • 一个免费或付费的Twilio账户。如果你是Twilio的新手,请 点击这里,现在就可以 创建一个免费账户,当你升级到付费账户时,会收到10美元的积分。
  • 一个拥有有效电话号码的智能手机。

开始使用

要开始使用,请创建一个名为bulk_sms_app 的新应用程序,并使用以下命令切换到项目目录:

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

安装所需的依赖性

该应用程序需要一个外部依赖,即Twilio PHP Helper库,以便它能与Twilio通信。使用下面的命令安装它:

composer require twilio/sdk

测试应用程序是否工作

然后用下面的命令启动该应用程序:

php yii serve

默认情况下,该应用程序将在http://localhost:8080/。在你喜欢的网络浏览器中打开该网址,你应该看到默认页面,如下图所示。

The default Yii2 home page

返回到终端,按Control + C ,退出应用程序。

更新应用程序的配置

为了让应用程序工作,它需要设置几个配置选项。这些是你的Twilio账户SID、 Auth Token和电话号码,以及一个单独的消息传递ID。

在这些被检索之前,更新config/params.php以匹配以下内容,这样设置就可以用占位符的值:

<?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",
];

接下来,从Twilio仪表板检索你的Twilio电话号码,"ACCOUNT SID",和 "AUTH TOKEN"。用这三个值替换config/params.php中各自的占位符值。

作为一项安全预防措施,你的AUTH TOKEN不会显示在屏幕上。点击 "复制 "图标来复制它。

然后,你需要设置一个消息服务,以便你可以检索其消息ID。要做到这一点,打开Twilio控制台,导航到*"所有产品和服务 > 可编程消息服务 >* 消息服务"。一旦到达那里,点击蓝色的*"创建消息服务*"按钮。

Twilio's Messaging Service Page

设置 "Messaging Service friendly name "为 "yii_bulk_sms",因为我们的用例是通知用户,然后点击 "Create Messaging Service"。

之后,我们需要为我们的服务添加一个发件人。具体来说,我们需要一个电话号码。点击 "添加发件人",选择 "电话号码",然后点击 "继续"。

如果你没有(或需要一个新的),你可以购买更多的号码

Select Phone Number Section

然后,通过点击 "第3步:设置集成 "完成,在接下来的页面点击 "第4步:合规信息",最后在之后的页面点击 "完成消息服务设置"。

现在服务已经创建,复制消息服务的SID,用它来替换config/params.phpTWILIO_MESSAGING_SID'的占位符数值。

Programming Messaging Service Properties

设置数据库

对于这个应用程序,我们将使用SQLite作为数据库。在应用程序的根目录下创建一个名为db的新目录。在该目录中,创建一个名为app.db的文件*。然后,更新config/db.php*以匹配以下代码:

<?php

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

确保你用来运行代码的用户在db目录上有写权限。

完成这些后,我们现在需要创建几个数据库迁移,以简化数据库的支架。第一个将创建一个名为 "client "的用户表。这个表将有两列;一列是姓名,一列是电话号码。

使用yii migrate/create 命令来创建它,提供要创建的迁移的名称,如下图所示:

php yii migrate/create create_client_table

当系统提示确认时,输入 "是 "并按回车键

默认情况下,迁移文件位于项目根目录下的 migrations 目录中。迁移文件名的前缀是字母m和创建时的UTC日期时间。例如m210809_113722_create_client_table.php。

打开刚刚创建的migrations/m<YYMMDD_HHMMSS>_create_user_table.php,修改safeUpsafeDown 函数,使其与下面的代码一致:

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

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

第二个迁移将为客户表注入一组假数据,这样我们的应用程序就有一些数据可以使用了。使用下面的命令来创建它:

php yii migrate/create seed_client_table

和以前一样,当被要求确认时,输入 "yes "并按回车键。然后,打开migrations/m<YYMMDD_HHMMSS>_seed_client_table.php,修改safeUp 函数,使之与下面的代码一致:

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()
            ]
        );
    }
}

这将向客户表插入1000条记录。

创建好这两个迁移程序后,使用下面的命令运行它们:

php yii migrate

当要求确认时,输入 "yes "并按回车键。运行完迁移后,你可以使用sqlite3命令行工具来验证数据库是否已经创建并适当地播种,就像下面的命令示例那样:

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

数据库中的前100个客户将被打印到命令行中,与下面的截图类似:

Listing the seeded user records in the terminal

创建客户模型

我们将创建一个ActiveRecord模型,它将为我们管理查询的创建和执行,而不是编写原始的SQL查询来与数据库互动。更重要的是,通过这样做,我们将有一个面向对象的方法来访问和存储数据库中的数据。使用下面的命令为Client 实体创建一个模型。当出现提示时,按回车键。

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

这个新的类将被创建在一个名为models的新目录下,位于项目的根目录下。

创建客户端控制器

有了数据库和模型,我们现在可以创建控制器来处理客户的显示和短信通知的发送。使用下面的命令创建一个控制器来处理与客户有关的请求;在提示时输入 "yes "并按回车:

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

完成后,将创建两个新的文件:controllers/ClientController.php(它扩展了yii/web/Controller),以及views/client/index.php

为了让API能够处理POST请求,CSRF验证将在ClientController.php中被禁用*。*要做到这一点,请在该类的开头添加以下内容:

public $enableCsrfValidation = false;

在没有额外检查的情况下,禁用CSRF可能会使你的应用程序暴露在安全威胁之下

在我们创建处理请求的动作之前,我们需要为新的API端点添加路由规则。要做到这一点,在config/web.php中取消对urlManager 组件的注释,并在其rules元素中添加以下代码:

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

使用Yii提供的速记符号,我们可以为一个特定的URL指定路由。URL是作为一个键提供的,而路由是作为一个值提供给相应的行动。通过在URL前加上一个HTTP动词,应用程序能够处理具有相同模式或不同动作的URL。你可以在这里阅读更多关于这方面的内容。

该应用程序将有两个路由:/clients/clients/notify 。第一个路由(*/clients*)将被用来显示保存在数据库中的客户列表。这将在ClientController.php中由一个名为actionIndex的函数处理。

第二条路线(*/clients/notify*)将被用来向所提供的客户批量发送短信通知。它也将在ClientController.php中由一个名为actionNotify的函数来处理。

为了让我们的控制器能够解析JSON请求,我们需要给我们的应用组件添加一个JSON解析器。config/web.php中的components 数组包含了一个请求数组,它保存了请求应用组件的配置。

为了添加JSON解析器,在request 数组中,在cookieValidationKey 元素之后添加以下内容:

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

然后,更新controllers/ClientController.php以符合下面的例子:

<?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'
            ]
        );
    }
}

actionIndex 函数中,我们使用分页法从数据库中分批检索客户。这些客户,连同分页对象,被传递给位于views/layouts/main.php的视图*。*

actionNotify函数做了三件事它:

  1. 接受请求并检索客户端和smsContent键。
  2. 返回一个带有成功信息的JSON响应。向选定的客户发送消息。

创建客户视图

客户列表将显示在一个表格中。在列表中的每个客户旁边,将有一个复选框来表示哪个客户被选中,在表格的底部将有一个按钮来发送短信给所有选中的客户。你可以在下面的图片中看到一个模拟图。

Mockup of the clients' view

点击这个按钮将触发一个弹出窗口,在里面输入短信内容。提交该表格将产生一个POST请求,其中包含短信的内容和要通知的客户。一旦请求成功完成,将显示一个警报。

在编辑视图之前,让我们编辑主布局,删除默认的Yii2头和脚,并通过CDN导入SweetAlert。SweetAlert将被用于显示弹出式表单和通知。

打开views/layouts/main.php并更新它,使之与以下内容相匹配:

<?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() ?>

接下来,更新views/client/index.php以匹配以下内容:

<?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>

除了渲染一个包含内容的表格,我们还添加了一个脚本,包含两个函数:sendSMSgetSelectedClients

sendSMS 是在点击发送短信按钮时被调用。这个函数触发了弹出的表单,在那里要输入短信内容。一旦提交表格,就会使用 ,检索所选择的客户。getSelectedClients

与短信内容一起,通过fetch APIclients/notify 端点发出一个POST请求。响应信息会显示在一个警报中,用户可以关闭。

创建一个帮助类来批量发送短信

在项目的根部,创建一个名为helpers的新文件夹。然后,在该目录下,使用你喜欢的IDE或文本编辑器,创建一个名为TwilioSMSHelper.php的新文件,在其中加入以下代码:

<?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
                ]
            );
        }
    }
}

由于我们使用FakerPHP来生成电话号码,你可以在create 函数中硬编码一个有效的电话号码用于测试。

在构造函数中,我们使用TwilioClient 对象的 twilio_account_sidtwilio_auth_token我们在config/params.php中设置了一个Twilio对象。我们还提取了电话号码和消息服务ID,作为消息配置的一部分。这些都被保存为类中的私有字段。在sendBulkNotifications 函数中,我们循环浏览客户,为每个客户创建一个新的消息。

没有必要在你的逻辑中添加延迟。你可以随心所欲地发送消息,因为Twilio会按照你规定的速率限制排队发送。

添加批量通知功能

controllers/ClientController.php中*,*更新actionNotify 函数,使其与以下内容相匹配:

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'
            ]
        );
    }

不要忘记下面的TwilioSMSHelper类的导入语句:

use app\helpers\TwilioSMSHelper;

有了这些,我们就可以向客户发送自定义的短信通知了。

还记得我们用FakerPHP为数据库中的每个客户生成随机和无效的电话号码吗?如果被选中,Twilio的API将无法连接这些号码,并在此过程中抛出一个错误。

为了测试,你可以通过使用下面的脚本改变一个或两个数字,并改成有效数字:

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

这将把数据库中的第二个电话号码的值改为上面指定的号码。

现在,再次启动应用程序,在项目根目录的终端运行php yii serve 。然后,在你选择的浏览器中打开http:://localhost:8080/client,选择一些客户端。

Selected / All Client Lists

之后,点击 "发送短信 "按钮,弹出表单,输入信息,并点击发送。你将会得到成功通知,以及发送到所提供电话号码的短信。

SMS content box

SMS notification example

总结

在这篇文章中,我们研究了如何使用Twilio发送批量短信通知。集成Twilio SDK简化了创建和发送通知的过程。

此外,你不必担心排队和发送过程,因为Twilio基础设施能够为你的用户实时处理通知。

本教程的整个代码库可在GitHub上找到。请自由地进一步探索。编码愉快!