在你的 Laravel 应用中是实现 Webhook 功能(通过 Notification 系统)
当我们整合第三方服务时,通常需要实现 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,在这一点上很像 Notifiable
Trait。这不是完全需要的,但它是一种很好的方式——我们可以很容易地为任何想要的类添加其他方法。
现在,我们在app
文件夹下创建一个名为WebhookNotifiable
的新 Trait,它有两个方法: getSigningKey()
和 getWebhookUrl()
。这两个方法是必需的并将作为 Webhook 流程的一部分。
这两个方法通过 Trait 方式访问对象属性,在本例中也就是webhook_url
和api_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 的通知,那么我建议你去了解一下。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
标题多了一个“是”吧。