如何在 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 哟。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Seven 的代码太渣,欢迎关注我的新拓展包 medz/cors 解决 PHP 项目程序设置跨域需求。
本帖由 Summer 于 6年前 加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 14

感谢,正好用得上!

6年前 评论

为什么按照你的步骤,提示Driver [sms] not supported.

6年前 评论

实在不知道问题在哪儿,我最后没用这种方式,而且我还是倾向缓存存放验证码,不过我很想知道为什么按照你的方法,以及参照文档都会报错

6年前 评论
medz

@山海王子 很抱歉现在才回复你,有点忙,才看到邮件提示,https://github.com/slimkit/thinksns-plus/b... 你可以看一下我们在产品中的实现代码。和帖子一致的。

6年前 评论
medz

@山海王子 其次,你提示 Driver [sms] not supported. 是因为你没有对 \Illuminate\Notifications\ChannelManager 进行驱动拓展,在服务提供者中拓展代码:

    $this->app->extend(\Illuminate\Notifications\ChannelManager::class, function ($channel) {
            $channel->extend('sms', function ($app) {
                return $app->make(\Zhiyi\Plus\Notifications\Channels\SmsChannel::class);
            });

            return $channel;
        });
6年前 评论
medz

@山海王子 另外,我是用数据库的原因是产品需求导致,要求后台可以看到发送记录。

6年前 评论

@medz 嗯嗯,感谢回复,昨天我在测试的时候也扩展了这个驱动的,我刚才试着直接在EasySmsServiceProvider中扩展这个驱动就成了,但是我模仿你的方式注册一个NotificationServiceProvider却不行,我再找找原因

6年前 评论

@medz 而且对于注册请求验证码,我直接给注册的Request增加Notifiable性状,这种做法是不是不太合理呢?

6年前 评论
medz

@山海王子 NotificationServiceProvider 你需要加入 /config/app.phpproviders 才会被加载。
第二个是否合理这个问题,算仁者见仁智者见智的问题了,如果你把每一个手当初一个目标,那就是合理,如果把注册时候传递过来的手机号码当作临时无效数据,则不合理。我是把传递过来的手机号码当作目标数据并记录在数据库,并展示给后台管理员,而使用 Notifiable 则是合理,因为这个手机号码是有意义的数据了,我向这个数据发送一个通知的行为则可以被理解为正常行为。

6年前 评论

@medz 多谢了,我把所有缓存都清掉就可以了 :sweat_smile: :sweat_smile:

6年前 评论

@medz 你提供了一个很好的思路

6年前 评论

easy-sms不支持华为云?

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
创始人 @ Odore Inc.
文章
33
粉丝
202
喜欢
532
收藏
198
排名:23
访问:24.7 万
私信
所有博文
社区赞助商