如何在 Laravel 中 “规范” 的开发验证码发送功能

很久没有在 Laravel China 发技术贴了,今天要分享的就是「如何在 Laravel 中“规范”的开发验证码发送功能」之所以我把「规范」二字扩起来,是因为没有绝对规范,可能只是我个人使用 Laravel 理解的在 Laravel 中的「规范」。

需求场景

发送「验证码」或者「消息通知」,可发送到手机或者发送到邮箱中。

完成

首先,我觉得的在 Laravel 中的规范就是使用 Laravel 的「消息通知」,这里基于场景为「验证码」。这个需求几乎所有系统都有使用到。

创建通知场景

首先,使用 php artisan make:notification 创建一个通知类,创建成功后默认已经存在了三个方法 viatoMailtoArray ,因为是发送验证码,我将这个控制类叫做 VerificationCode

然后我们创建一个验证码数据模型和数据表迁移,可以使用 php artisan make:model "VerificationCode" -m 直接快速创建数据模型和迁移。

我的迁移如下:

<?php

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

class CreateVerificationCodesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('verification_codes', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->nullable()->default(null)->comment('关联用户');
            $table->string('channel', 50)->comment('发送频道,例如 mail, sms');
            $table->string('account', 100)->comment('发送账户');
            $table->string('code', 20)->comment('发送验证码');
            $table->tinyInteger('state')->nullable()->default(0)->comment('状态');
            $table->timestamps();
            $table->softDeletes();

            $table->index('account');
            $table->index('user_id');
        });
    }

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

然后我们打开数据模型类,在里面添加 Illuminate\Notifications\Notifiable 性状:

<?php

namespace Zhiyi\Plus\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;

class VerificationCode extends Model
{
    use Notifiable, SoftDeletes;

    /**
     * Get the notification routing information for the given driver.
     *
     * @return mixed
     */
    public function routeNotificationFor()
    {
        return $this->account;
    }

    /**
     * Has User.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     */
    public function user()
    {
        return $this->hasOne(User::class, 'id', 'user_id');
    }
}

可以看到我还添加了「软删除」,因为是基于手机号或者邮箱的验证码发送,所以我们不需要其他的内置花花肠子,也不需要记录到 「消息通知数据表」中,所以 routeNotificationFor 方法我选择直接返回需要发送的账号(手机号或者邮箱)

加入工厂模式,快捷发送

打开 database/factories/ModelFactory.php 在里面添加一个 关于通知数据模型的工厂定义:

$factory->define(Zhiyi\Plus\Models\VerificationCode::class, function (Faker\Generator $faker) {
    return [
        'user_id' => null,
        'channel' => 'mail',
        'account' => $faker->safeEmail,
        'code' => $faker->numberBetween(1000, 999999),
        'state' => 0,
    ];
});

这样,我们就可以通过 factory(\Zhiyi\Plus\Models\VerificationCode::class) 工厂函数快捷的创建验证码并发送通知了。

为什么在验证码数据模型增加通知性状?

首先 Illuminate\Notifications\Notifiable 这份性状,Laravel 默认添加到 User 模型中的,所以通过 $user->notify() 可以快速的给用户发送一个通知,但是在 规范文档中有这么一句话:

Remember, you may use the Illuminate\Notifications\Notifiable trait on any of your models. You are not limited to only including it on your User model.

这是 Laravel 官方文档原话,意思就是 Illuminate\Notifications\Notifiable 不仅仅是用在 User 模型上的。

所以我们在验证码模型中添加 Illuminate\Notifications\Notifiable 是完全符合 laravel 通知的正确使用的。

开发通知类

首先,我在 数据表迁移中存在一个字段 channel 也就是通知频道标识,我们可根据这个值来决定用什么方式发送验证码,而这个操作在 通知类 的 via 中实现的:

public function via(VerificationCodeModel $notifiable)
{
    return [$notifiable->channel];
}

我选择方式就是直接返回 channel 值,这个只可以是任何值,只要我们实现了这个通知频道,都可以发送,而 Laravel 已经内置和一些发送频道 databasemailnexmo

完成邮件验证码发送

其实,这个步骤我们要做的事情已经很少了,生产 通知类 的时候,已经完成了 toMail 方法,所以,我们直接修改其消息内容即可。

完成短信发送

短信发送我是采用 overtrue/easy-sms 这个包,这个是 安正超 开发的一个 短信发送客户端,已经内置了很多短信平台,实现也很优秀。(吐槽:虽然有些细节有问题,例如不按照契约调用方法传递网关)

首先依赖 短信发送客户端 包 composer require overtrue/easy-sms 然后新建配置 /config/sms.php ,内容嘛,就按照 easy-sms 首页的说明增加即可,我先贴出我的配置内容(为了减少文章字数,我只保留阿里大于配置):

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | HTTP 请求的超时时间
    |--------------------------------------------------------------------------
    |
    | 设置 HTTP 请求超时时间,单位为「秒」。可以为 int 或者 float。
    |
    */

    'timeout' => 5.0,

    /*
    |--------------------------------------------------------------------------
    | 默认发送配置
    |--------------------------------------------------------------------------
    |
    | strategy 为策略器,默认使用「顺序策略器」,可选值有:
    |       - \Overtrue\EasySms\Strategies\OrderStrategy::class  顺序策略器
    |       - \Overtrue\EasySms\Strategies\RandomStrategy::class 随机策略器
    |
    | gateways 设置可用的发送网关,可用网关:
    |       - alidayu 阿里云信
    |       - alidayu 阿里大于
    |       - yunpian 云片
    |       - submail Submail
    |       - luosimao 螺丝帽
    |       - yuntongxun 容联云通讯
    |       - huyi 互亿无线
    |       - juhe 聚合数据
    |       - sendcloud SendCloud
    |       - baidu 百度云
    |
    */

    'default' => [
        'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,
        'gateways' => ['alidayu'],
    ],

    /*
    |--------------------------------------------------------------------------
    | 发送网关配置
    |--------------------------------------------------------------------------
    |
    | 可用的发送网关,基于网关列表,这里配置可用的发送网关必要的数据信息。
    |
    */

    'gateways' => [
        'alidayu' => [
            'app_key' => null,
            'app_secret' => null,
            'sign_name' => null,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | 消息支持频道
    |--------------------------------------------------------------------------
    |
    | 发送消息可用根据不同频道配置注入不同频道配置数据。
    |
    */

    'channels' => [

        // 验证码频道
        'code' => [
            'alidayu' => [
                'template' => null,
            ],
        ],
    ],
];

我增加了一个 channel 配置,用于走不同场景,例如验证码场景 code 以方便消息器读取配置。

然后打开 AppServiceProvider.php 在 register 中增加如下:

public function register()
{
       $this->app->singleton(\Overtrue\EasySms\EasySms::class, function ($app) {
            return new \Overtrue\EasySms\EasySms(
                $app->config['sms']
            );
        });
}

至此 EasySms 在 Laravel 中的集成已经完成,但是还没有开发实际功能,我们接着往下看。

开发 sms 发送频道

为什么要开发?首先,easy-sms 支持的很多,可以考虑单独为每个发送平台开发一个通知发送频道类,也可以采用只开发一个 sms 发送频道类,我先泽开发一个 sms 通知发送类,通过 easy-sms 的策略机制去多平台发送验证码。

首先,新建一个 app/Notifications/Channels/SmsChannel.php 文件,因为 Laravel 没有提供生成函数,这个需要自己创建哟,只要实现 send 方法即可。我的 SmsChannel 内容如下:

<?php

namespace Zhiyi\Plus\Notifications\Channels;

use Overtrue\EasySms\EasySms;
use Illuminate\Notifications\Notification;

class SmsChannel
{
    /**
     * The SMS notification driver.
     *
     * @var \Overtrue\EasySms\EasySms
     */
    protected $sms;

    /**
     * Create the SMS notification channel instance.
     *
     * @param \Overtrue\EasySms\EasySms $sms
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function __construct(EasySms $sms)
    {
        $this->sms = $sms;
    }

    /**
     * Send the given notification.
     *
     * @param  mixed  $notifiable
     * @param  \Illuminate\Notifications\Notification  $notification
     * @return \Nexmo\Message\Message
     */
    public function send($notifiable, Notification $notification)
    {
        if (! $to = $notifiable->routeNotificationFor('sms')) {
            return;
        }

        $message = $notification->toSms($notifiable, $this->sms->getConfig());

        return $this->sms->send($to, $message);
    }
}

这样基于 easy-sms 的 短信通知发送频道已经完成。

开发场景发送消息

这部分完全属于 easy-sms 使用开发了,我们新建一个 VerificationCodeMessage.php 我的内容如下:

<?php

namespace Zhiyi\Plus\Notifications\Messages;

use Overtrue\EasySms\Message;
use Overtrue\EasySms\Contracts\GatewayInterface;
use Illuminate\Config\Repository as ConfigRepository;

class VerificationCodeMessage extends Message
{
    protected $config;
    protected $code;
    protected $gateways = ['alidayu'];

    /**
     * Create the message instance.
     *
     * @param \Illuminate\Config\Repository $config
     * @param int $code
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function __construct(ConfigRepository $config, int $code)
    {
        $this->config = $config;
        $this->code = $code;
    }

    /**
     * Get the message content.
     *
     * @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway
     * @return string
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function getContent(GatewayInterface $gateway = null)
    {
        return sprintf('验证码%s,如非本人操作,请忽略本条信息。', $this->code);
    }

    /**
     * Get the message template.
     *
     * @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway
     * @return string
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function getTemplate(GatewayInterface $gateway = null)
    {
        return $this->config->get('alidayu.template');
    }

    /**
     * Get the message data.
     *
     * @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway
     * @return array
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function getData(GatewayInterface $gateway = null)
    {
        return [
            'code' => strval($this->code),
        ];
    }
}

然后我们回到 VerificationCode 验证码通知类中,增加 toSms 方法,我的代码如下:

public function toSms(VerificationCodeModel $notifiable, Config $config)
{
    return new Messages\VerificationCodeMessage(
        new ConfigRepository($config->get('channels.code')),
        $notifiable->code
    );
}

可以看到,我在实例化 验证码消息 的时候传递了一个 config 进去,做什么用呢?最上面我也提到了,我在 配置文件中增加长场景配置,例如验证码不同频道的 template 等。这样消息器就可以更具发送网关来判断使用场景的配置是什么。

再次吐槽,easy-sms 的契约设计也应该是这个思想,但是 getContent/getTemplate/getData 在实际网关调用的时候根本没有传递网关过来。。。

好了我们开发完成了

发送验证码

在创建验证码数据模型的时候就已经添加到「工厂」中,所以我们可以直接使用 factory 函数了,发送演示:

// sms
$model = factory(\Zhiyi\Plus\Models\VerificationCode::class)->create([
    'account' => '1878xxxx50x',
    'channel' => 'sms',
]);
$model->notify(
    new \Zhiyi\Plus\Notifications\VerificationCode($model)
);

// mail
$model = factory(\Zhiyi\Plus\Models\VerificationCode::class)->create([
    'account' => 'example@example.com',
    'channel' => 'mail',
]);
$model->notify(
    new \Zhiyi\Plus\Notifications\VerificationCode($model)
);

大功告成,easy-sms 是一个很不错的包哟。

上面代码都是来自于 ThinkSNS Plus ,看完整的开发代码可以看仓库哟:

GitHub: https://github.com/slimkit/thinksns-plus

开源不易,求 Star 哟。

Seven的代码太渣,欢迎支持我们的开源项目 ThinkSNS Plus 点个 star。