iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Modern Web

工作後才知道的後端 30 件小事系列 第 9

Laravel 實作 Webhooks

前言

那時候找不到完全符合需求的可以直接用或改,所以最後自己寫了一個,供大家參考。

根據我爬文,要用 Laravel 實作 Webhook 的方法應該不只一種,也有已經寫好的套件可以使用,可以花點時間看,再根據自己需求選擇用做好的或自開發。

需求

  • 產品、訂單狀態異動時要發 webhooks 通知分銷商
  • 產品、訂單狀態異動為兩個事件,要可以設定各自的 endpoints
  • 可以啟用、關閉通知
  • 有發送紀錄

整理起來的開發項目

  • 發送通知
  • 發送時紀錄內容與回覆
  • 觸發發送通知
  • 把紀錄和控制面板整理到後台介面

本次只會分享「發送通知」和「觸發發送通知」的寫法

Notification & Channel

實作上會用到這兩樣,我的理解如下

  • Notification: 製作訊息、內容 (What to send)
  • Channel: 送訊息的方法 (How to send)

又因為需要控制送到哪、與是否啟用,新增 subscribes table,去記錄。
schema 設計如下,一個分銷商(client)可以訂閱多個事件(event),每個訂閱可以設置一個endpoint(url)、且可以控制是否啟用(active)

Subscribes
id
event_name
client_id
active
url
created_at
updated_at

Publish Subscribe Pattern

Notification

<?php

namespace App\Notifications;

use App\Channels\WebhookChannel;
use App\Models\Subscribe;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;


class WebhookNotification extends Notification implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public $tries = 10;   // 可以設定最多重試幾次
    public $timeout = 45; // 等待分銷商幾秒後沒有回覆算失敗
    public $retryAfter = 100; // 若失敗後幾秒重試
    /**
     * Where the webhook notification will send to
     *
     * @var string
     */
    public $url;

    /**
     * Webhook event
     *
     * @var string
     */
    public $event;

    /**
     * Gds Client instance
     *
     * @var App\Models\Client
     */
    public $client;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct(Subscribe $subscribe)
    {
        $this->url    = $subscribe->url;
        $this->event  = $subscribe->event;
        $this->client = $subscribe->client;
    }

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

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     */
    public function toWebhook($notifiable)
    {
        // 根據不同事件組訊息
        switch ($this->event) {
            case 'product_status_updated':
                return [
                    'event'        => $this->event,
                    'product_id'   => $notifiable->prod_id,
                    'product_name' => $notifiable->prod_name,
                    'new_status'   => $notifiable->prod_status,
                    // 時間資訊會想送「寄送」時間,所以在 Channel 再做
                ];
            case 'order_status_updated':
                return [
                    'event'         => $this->event,
                    'order_id'      => $notifiable->order_id,
                    'order_voucher' => $notifiable->order_voucher,
                    'new_status'    => $notifiable->order_status,
                    // 時間資訊會想送「寄送」時間,所以在 Channel 再做
                ];
            // ...之後有新事件可以加在下面
        }
    }

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

Channel

<?php
namespace App\Channels;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;

class WebhookChannel
{
    public function __construct()
    {
        $this->client = new Client();
    }

    /**
     * @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();
        $body['timestamp'] = $timestamp->format('Y-m-d H:i:s');

        $headers = [
            'Content-Type' => 'application/json'
        ];

        $url = $notification->url;

        $request = new Request('POST', $url, $headers, json_encode($body));

        try {
            $response = $this->client->send(
                $request,
                ['timeout' => 45.0]
            );

            // 這邊看你覺得定義收到什麼回覆算成功,
            // 可以是收到 code 2XX、200、或特定訊息
            // 以下範例是只有收到 200 系統才是為成功
            if ($response->getStatusCode() == 200) {
                // Success
            } else {
                // Get a non 200 respones
                $notification->release(100); // 100秒後重新再跑一次
            }
        } catch (Throwable $th) {
            // handle exception
        }
    }
}

狀態異動時觸發發送

以產品狀態異動為例,可以利用 model 在 saving 的時候觸發發送

class Product extends Model
{
    use Notifiable; // 要加這個

    protected static function boot()
    {
        parent::boot();
        
        // 在儲存時觸發
        static::saving(function ($product) {

            // 檢查狀態是否有異動
            $prev_status = $product->getOriginal('prod_status');
            if ($prev_status != $product->prod_status) {
                // 對有該產品下所有的訂閱發送通知
                $subscribes = $product->getSubscribes("product_status_updated");
                foreach ($subscribes as $subscribe) {
                    $product->notify(new WebhookNotification($subscribe));
                }
            }
        });
    }
    
    public function getSubscribes($event)
    {
        return Subscribe::where('active', 1)
        ->where('event_name', $event)
        ->get();
    }
}


上一篇
一些類似判斷是否為空的方法比較:isset, empty, is_null
下一篇
認識 Laravel Queue Jobs
系列文
工作後才知道的後端 30 件小事20

尚未有邦友留言

立即登入留言