📱

Twilioを使ってLaravelのSMS通知をする

2021/01/12に公開

こんにちはみなさん

うん、まあ、ちょっとしたアクシデントがありまして、Laravelが公式提供しているVonage Communication Apiを使ったSMS通知が使えなくなってしまったのですよね。
というわけで、別のSMSサービスとしてTwilioを使うようにします。

ではやっていきましょう。

問題設定

今回はユーザへのSMS通知を、Twilioを使って実現してみましょう。
方針は以下の通りです。

  • SMS通知チャンネルを作る
  • 通知チャンネルで使用するクライアントをTwilioにする。
  • 実際に送信する

結局どうしたかというと、パッケージを作成して、簡単にtwilioを入れられるようにしたって感じです。

Quick Start

どうやっているのかを知りたい人はほとんどおらんと思いますので、どうやれば動かせるかを先に解説しちゃいます。

パッケージインストール

まず、パッケージとsdkを導入します。

composer require niisan/laravel-sms-notification twilio/sdk

個人的な趣向により、sdkを直接はパッケージに依存させていないのです。

ついで、設定ファイルを作ります。

php artisan vendor:publish --provider "Niisan\Notification\SmsNotificationServiceProvider"

設定ファイルの中身は、基本的には環境変数で外から入れることができます。

SMS_DRIVER=twilio
SMS_DEFAULT_FROM='+1111222233'
TWILIO_SID=***********************
TWILIO_TOKEN=***********************

SID と TOKEN はTwilioに登録したときにもらえます。
また、FROMはTwilioにログインしたときのダッシュボードから、無料の電話番号を発行して、それを使います。

通知の方法

SMS通知をたくさんうつ場合は通知対象のモデルに以下のメソッドをはやしておきます。

<?php
// 省略
class User extends Authenticatable
// 省略
    public function routeNotificationForSms()
    {
        return $this->phone_number;
    }

細かいことですが、Twilioでは国際電話番号の規格を使用していて、+から必要になるようです。(ex. +1122223333 )

次にNotificationを作ります。

<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use Niisan\Notification\SmsMessage;

class SmsNotification extends Notification
{
    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['sms'];
    }

    public function toSms($notifiable): SmsMessage
    {
        return (new SmsMessage)
                ->unicode()
                ->body('hello world! こんにちは!' . $notifiable->name);
    }
}

viaメソッドでsmsを追加し、toSmsメソッドで、メッセージクラスを返します。
SmsMessageオブジェクトで、メッセージに日本語を含める場合はunicodeメソッドで、マルチバイトをオンにしておきます。

あとは、普通にnotifyするだけです。

$user->notify(new SmsNotification);

おまけ

送信先や送信元をNotificationで変更することもできます。

        return (new SmsMessage)
                ->unicode()
                ->body('hello world! こんにちは!' . $notifiable->name)
		->to('+1122223333')
		->from('+2233334444')

こうすると、notifiableやdefault_fromに設定してある電話番号を無視して、ここで設定した電話番号を使うようにします。

SMS通知チャンネルを作る

ここからはパッケージの実装を説明します。多くの人にはいらんですが、興味のある方だけ見て行ってくださいませ。

公式のパッケージではないので、Laravelの通知チャンネルを拡張する必要があるわけですが、公式のマニュアルがちょっと不親切というか、interface用意しておいてほしいのよね。
https://laravel.com/docs/8.x/notifications#custom-channels

チャンネルの拡張

SMSの通知チャンネルをこんな風に作っておきます。

<?php
namespace Niisan\Notification;

use Illuminate\Notifications\Notification;
use Niisan\Notification\Contracts\SmsClient;
use RuntimeException;

class SmsChannel
{

    /** @var SmsClient $client */
    private $client;

    public function __construct(SmsClient $client)
    {
        $this->client = $client;
    }

    public function send($notifiable, Notification $notification)
    {
        $message = $notification->toSms($notifiable);
        if (!$message->to) {
            if (!method_exists($notifiable, 'routeNotificationForSms')) {
                throw new RuntimeException('A notifiable object must have "routeNotificationForSms" method.');
            }
            $message->to($notifiable->routeNotificationForSms());
        }

        $message->from(config('sms-notification.default_from'));
        
        $this->client->send($message);
    }
}

NotificationオブジェとのtoSmsメソッドからメッセージオブジェクトを取得し、必要であればtoやfromを入れて、クライアントに投げる形式になっています。
基本、クライアントはTwilio以外のも考えられると思ったので、他にも入れられるようにしてあります。

SmsClientはinterfaceになっており、public function send(SmsMessage $message): void;の未定義されていますので、これをクライアントごとに実装すればオッケーとなっています。

チャンネルの拡張を適用する

サービスプロバイダーを使って、チャンネルの拡張を適用します。

<?php
namespace Niisan\Notification;

use Illuminate\Notifications\ChannelManager;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ServiceProvider;
use Niisan\Notification\Clients\Twilio;
use Niisan\Notification\Contracts\SmsClient;

class SmsNotificationServiceProvider extends ServiceProvider
{

    public function boot()
    {
        $this->publishes([
            __DIR__.'/config/sms-notification.php' => config_path('sms-notification.php'),
        ]);
    }

    public function register()
    {
        $this->app->bind(SmsClient::class, function ($app) {
            if (config('sms-notification.driver') === 'twilio') {
                $client = new \Twilio\Rest\Client(
                    config('sms-notification.twilio.sid'),
                    config('sms-notification.twilio.token')
                );
                return new Twilio($client);
            }
        });

        Notification::resolved(function (ChannelManager $service) {
            $service->extend('sms', function ($app) {
                return $app[SmsChannel::class];
            });
        });
    }
}

まあ、Twilioしかないんですがね。
一応、SmsClientについては入れ替えできるようになっているので、別のSMSサービス使いたい場合は新しいクライアントクラスを実装して、サービスプロバイダで入れ替えちゃえばオッケーです。
多分。

クライアントクラスの実装

クライアントクラスはそのまま、Twilioのクライアントをラップする感じです。

<?php
namespace Niisan\Notification\Clients;

use Niisan\Notification\Contracts\SmsClient;
use Niisan\Notification\SmsMessage;
use Twilio\Rest\Client;

class Twilio implements SmsClient
{

    private $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }
    
    /**
     * Send message through Twilio.
     *
     * @param SmsMessage $message
     *
     * @return void
     */
    public function send(SmsMessage $message): void
    {
        $this->client->messages->create(
            $message->to,
            [
                'from' => $message->from,
                'body' => $message->body,
                'smartEncoded' => $message->unicode
            ]
        );
    }
}

これはもう、単純にTwilio SDKのクライアントをラップしているだけです。
別のサービスのクライアントを実装する場合は、Niisan\Notification\Contracts\SmsClientを実装しておいて、サービスプロバイダで入れ替えればオッケーです。

SmsMessageクラスは対して言及するところもないので、割愛します。

まとめ

ということで、Laravel公式のSMS通知が使えなくなってしまったので、自作したという話でした。
いや、Laravelはパッケージ自作して突っ込むの簡単ななんですが、微妙にinterfaceがないとか、足りんところもあるなぁといった感想。
まあ、パッケージづくりは楽しいというか、プロダクト開発のようにカネに直結しない分、気楽にできますね。
いや、カネ儲けも好きなんですがね。

今回はこんなところです。

Discussion