在你的 Laravel 应用中是实现 Webhook 功能(通过 Notification 系统)
16

file

当我们整合第三方服务时,通常需要实现 Webhooks 。Webhooks 是一个通过 HTTP 请求向其它应用程序发送数据或者事件的 Web 应用程序。例如,大多数邮件服务像 Mailgun 和 Spark Post ,会给你的应用邮箱发送一些邮件被拒收或者垃圾邮件通知相关的消息 。

接收方,一般我们简单地创建一个控制器就可以很容易的接收 Webhook 消息 。发送方这边,在 Laravel 消息通知框架 的帮助下,发送 Webhook 消息也是同样简单,我们只需要创建一个 自定义通知通道 即可。

安装我们依赖项

首先我们需要安装一个用于发生 HTTP 请求的库。我们在这里要用到的是 Guzzle,这在大多数 PHP Web 应用程序中都很常见。

我们可以通过使用 Composer 来安装它。

composer require guzzlehttp/guzzle:~6.0

这样做以后,我们可以创建新的 Webhook 通知通道。

通知通道

Laravel 中的一个通知通道为用户、团队提供了一个发送结构化消息的机制,在任何一个“通道”中都可以找到适合的内容,这可以是电子邮件、 Slack 通道或 SMS 消息。这些通道自身通常会发送 HTTP 请求,所以这不是我们在这里做的新东西。

首先,我们创建一个  app/Channels 目录并添加一个 WebhookChannel 类。这个类将实现一个发送方法,该方法需要一个 Notifiable 对象和一个 Notification 对象。

鉴于框架中通道的使用方式,我们还需要在该类的构造函数中引入一个 Guzzle 客户端和一个日志实例,将我们的新类中的某些依赖关系置于其中。日志实例的引入主要是为了更容易的确认消息的送达情况。

所以现在我们的 WebhookChannel 应该是这样子的: 

<?php

namespace App\Channels;

use GuzzleHttp\Client;
use Illuminate\Log\Logger;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;

class WebhookChannel
{
    /**
     * @var Client
     */
    private $client;
    /**
     * @var Logger
     */
    private $logger;

    public function __construct(Client $client, Logger $logger)
    {
        $this->client = $client;
        $this->logger = $logger;
    }

    /**
     * @param Notifiable $notifiable
     * @param Notification $notification
     * @throws WebHookFailedException
     */
    public function send($notifiable, Notification $notification)
    {

    }
}

我们稍后将实现具体的方法,但是现在我们应该使我们自己的可通知特性标准化以及如何通过 Webhook 来接收消息。

一个 Webhook 类型的 trait 通知

我们将用 Webhook 为我们想要通知的对象创建一个 Trait,在这一点上很像 NotifiableTrait。这不是完全需要的,但它是一种很好的方式——我们可以很容易地为任何想要的类添加其他方法。

现在,我们在app文件夹下创建一个名为WebhookNotifiable的新 Trait,它有两个方法: getSigningKey()和 getWebhookUrl()。这两个方法是必需的并将作为 Webhook 流程的一部分。

这两个方法通过 Trait 方式访问对象属性,在本例中也就是webhook_urlapi_key属性。前者是通知的目标 URL,而后者api_key用来授予终端用户一种验证方式,验证我们发送给他们的 HTTP 请求。

<?php

namespace App;

trait WebhookNotifiable
{
    /**
     * @return string
     */
    public function getSigningKey()
    {
        return $this->api_key;
    }

    /**
     * @return string
     */
    public function getWebhookUrl()
    {
        return $this->webhook_url;
    }
}

创建可以接收 Webhook的用户

下面我们在用户模块使用这个 Trait 。只需要花几秒钟修改用户的迁移表,并创建 api_key 和 webhook_url 两个字段进行存储。同时我们将 Trait 添加到 app/User.php 的 User 类中。这里已经又一个 Notifiable Trait ,我们只需要在额外添加刚才我们创建的这个即可。

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable, WebhookNotifiable;

    /**
     * 可填充的属性
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'api_key', 'webhook_url',
    ];

    /**
     * 隐藏不展示的属性
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token', 'api_key',
    ];
}

完成我们的 Webhook 通道

现在我们有一个带通知特性的 Webhook ,我们可以编写我们的 Send 方法通过我们的 WebhookChannel 来发送通知。我们将编辑 Send 方法,首先检查通知中是否有  toWebHook() 方法,如果没有它将会调用  toArray() 方法来取代它。Laravel 默认使用  toArray()  方法来进行通知,但是很高兴的是我们有可选择的多个通道来调用这个方法。她允许我们可以从被通知的 User 用户的通知中获取内容。这将成为我们 Webhook 请求的 HTTP 主体。
然后我们将为我们的 Webhook 请求生成一些头部信息。我们这样做的目的是为了确保接受通知方能够确保通知来自于我们而不是其他人。我们通过哈希随机字串来生成用户的签名键来实现这一点。提供时间戳是用来确保来自 Hooks 的请求不会过期的好方法。

然后,我们用 Guzzle 客户端来发送消息。如果没有返回一个 HTTP 200 的状态码,我们将抛出异常。在这种情况下,我创建来一个简单的 WebhookFailedException 类来标准化处理异常。 同时,我们确保如果捕获 Guzzle 抛出的异常并将其重新抛出为 WebhookFailedException 。我们使用异常,是因为如果一个通知在已在排队且在后台进程中处理,异常将导致通知重新排队并等待进一步尝试。需要说明的是,我们也使用日志实例把请求写入日志是为了清楚的记录请求的成功或者失败。

<?php

namespace App\Channels;

use App\Exceptions\WebHookFailedException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Illuminate\Log\Logger;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;

class WebhookChannel
{
    /**
     * @var Client
     */
    private $client;
    /**
     * @var Logger
     */
    private $logger;

    public function __construct(Client $client, Logger $logger)
    {
        $this->client = $client;
        $this->logger = $logger;
    }

    /**
     * @param Notifiable $notifiable
     * @param Notification $notification
     * @throws WebHookFailedException
     */
    public function send($notifiable, Notification $notification)
    {
        if (method_exists($notification, 'toWebhook')) {
            $body = (array) $notification->toWebhook($notifiable);
        } else {
            $body = $notification->toArray($notifiable);
        }
        $timestamp = now()->timestamp;
        $token = str_random(16);

        $headers = [
            'timestamp' => $timestamp,
            'token' => $token,
            'signature' => hash_hmac(
                'sha256',
                $token . $timestamp,
                $notifiable->getSigningKey()
            ),
        ];

        $request = new Request('POST', $notifiable->getWebhookUrl(), $headers, json_encode($body));

        try {
            $response = $this->client->send($request);

            if ($response->getStatusCode() !== 200) {
                throw new WebHookFailedException('Webhook received a non 200 response');
            }

            $this->logger->debug('Webhook successfully posted to '. $notifiable->getWebhookUrl());

            return;

        } catch (ClientException $exception) {
            if ($exception->getResponse()->getStatusCode() !== 410) {
                throw new WebHookFailedException($exception->getMessage(), $exception->getCode(), $exception);
            }
        } catch (GuzzleException $exception) {
            throw new WebHookFailedException($exception->getMessage(), $exception->getCode(), $exception);
        }

        $this->logger->error('Webhook failed in posting to '. $notifiable->getWebhookUrl());
    }
}

通知示例

现在已经完成了建立通过 Webhook 发送数据的机制的过程,我们来实现一个通知的简单示例吧。

我们可以通过 Artisan 命令 php artisan make:notification SomethingHappenedNotification 生成 app/Notifications/SomethingHappenedNotification.php 类文件。

给该通知类添加 $message 参数,这是为了展示可以传递给通知类的数据类型。

接下里移除我们不会使用的 toMail() 方法,添加一个返回数组类型的方法 toWebhook($notification) 。此时,我们只是为了测试它而放置一些值。

为了实现上面的事情需要修改 via() 方法,返回一个包含 WebhookChannel 类的数组。这将告诉 Laravel 该通知需要通过 WebhookChannel 来传递。 我们还需要添加 ShouldQueue 接口来实现队列。这非常有用,意味着可以使用 队列 进行处理,因此,如果你实现了队列, Laravel 将把它作为一个后台进程来处理。这对于处理 HTTP 请求超时或阻断给用户发送的响应时非常有用。

<?php

namespace App\Notifications;

use App\Channels\WebhookChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;

class SomethingHappenedNotification extends Notification implements ShouldQueue
{
    use Queueable;
    /**
     * @var string
     */
    private $message;

    /**
     * Create a new notification instance.
     *
     * @param string $message
     */
    public function __construct($message)
    {
        //
        $this->message = $message;
    }

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

    public function toWebhook($notifiable)
    {
        return [
            'message' => $this->message,
        ];
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [];
    }
}

集中测试

显然,从实现到构建一个工作示例有点困难。所以我已经在 GitHub 上创建了一个小项目,你可以尽情的使用它。自述中提供了一些基本的使用说明供你参照。

总结

好吧,没什么好多说的了。Laravel 的通知自 5.3 版本开始就已经存在了,它是真正帮助框架脱颖而出的独特特性之一。它的伟大之处在于,它是可扩展的,可以用来简化那些需要发生消息给每个收件人的流程。
更棒的是,它还支持消息队列机制,这在你项目刚刚启动的时候也许不重要,但是随着用户基数的增长,它能够帮助你加快应用程序的请求速度。总之,如果你到目前为止还没有了解过 Laravel 的通知,那么我建议你去了解一下。

Living on the bleeding edge

原文地址:https://medium.com/@SlyFireFox/laravel-i...

译文地址:https://laravel-china.org/topics/13195/i...

本帖已被设为精华帖!
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!