1. 程式人生 > >扒一扒 laravel的訊息通知(上)

扒一扒 laravel的訊息通知(上)

  laravel給我們提供了多渠道的訊息通知功能,包括郵件,簡訊,資料庫,slack等通知方式。本文主要分析基於資料庫的訊息通知的底層實現。為了方便,本文將需要接受通知訊息的模型稱為接收者。

  ps:閱讀本文前,請不瞭解Eloquent關聯關係的讀者先點選eloquent relations瞭解相關內容

  通過官方文件可以知道,當我們需要開啟一個model接收訊息通知的功能時,需要在模型中新增Illuminate\Notifications\Notifiable 這個trait。通過程式碼可以發現,這個trait實際上只是使用了HasDatabaseNotifications, RoutesNotifications;

這兩個trait,接下來讓我們一起看看具體是怎樣通過這兩個trait來實現訊息通知的。

  先來看看HasDatabaseNotifications

  public function notifications()
 {
        return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderBy('created_at', 'desc');
 }
  public function unreadNotifications()
 {
  return $this->morphMany(DatabaseNotification::class, 'notifiable'
)
->
whereNull('read_at')->orderBy('created_at', 'desc'); }

  接收者可以接收多個渠道的通知,這是一種一對多的關聯關係,所以這裡通過HasDatabaseNotifications為接受者添加了兩個多型關聯關係到DatabaseNotification::class模型,讓我們可以方便的獲取到接收者對應的通知。

  瞭解了傳送訊息模型和接收訊息模型的關聯關係後,再來看看這兩個模型的具體定義。先分析一下DatabaseNotification::class

class DatabaseNotification extends
Model {
public $incrementing = false; /** * The guarded attributes on the model. * * @var array */ protected $guarded = []; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'data' => 'array', 'read_at' => 'datetime', ]; protected $table = 'notifications'; public function notifiable() { return $this->morphTo(); } //標記已讀,也就是賦值給read_at欄位 public function markAsRead() { if (is_null($this->read_at)){ $this->forceFill(['read_at' => $this->freshTimestamp()])->save(); } } public function newCollection(array $models = []) { return new DatabaseNotificationCollection($models); } }

至此,已經建立好了接收者和通知之間的多型聯絡關係。

很多童鞋到這裡可能就會納悶了,那麼如何將不同型別的通知關聯到不同的模型呢?

還記得我們在建立訊息通知時在app/notifications下建立的通知嗎,現在再來看看laravel為我們生成的這個模型。

 class salePromotion extends Notification implements ShouldQueue
{
    use Queueable;
    public  $data;
    public function __construct($data)
    {
        $this->data=$data;
    }
    //設定通知的渠道是基於資料庫
    public function via($notifiable)
    {
        return ['database'];
    }
    //設定在notifications表中的data欄位對應格式
    public function toDatabase($notifiable)
    {
             return [
                 'data' => $this->data,
             ];
    }
}

可見,這個salePromotion是繼承了我們剛才的DatabaseNotification模型的,也就是說它同時也繼承了DatabaseNotification模型和接受者的關聯關係,所以這多個通知類其實都是對應到了資料庫裡面的notifications表上,在傳送通知時(由於是資料庫通知,實際上是往notification表中寫資料)寫入具體的通知類的資料。

很自然地,這時候我們就會想知道那麼是如何傳送的呢?當接受者只有一個的時候,我們通常只需要呼叫$user->notify(new InvoicePaid($invoice)),而接受者是一個collection時,我們會比較經常用Notification::send($customers, new salePromotion($data));

還記得前面我們說的另外一個trait嗎,RoutesNotifications,開扒~
重點我們來看看notify這個方法:

  public function notify($instance)
    {
        app(Dispatcher::class)->send($this, $instance);
    }

app(Dispatcher::class)返回Illuminate\Notifications\ChannelManager物件,看看它的send方法是怎麼定義的。

public function send($notifiables, $notification)
{
 //將$notifiables轉換成集合或者陣列的形式,返回collection或者陣列
  $notifiables = $this->formatNotifiables($notifiables);
 //檢測是否開啟佇列,佇列我們就不分析了
 if ($notification instanceof ShouldQueue) {
  return $this->queueNotification($notifiables,$notification);
 }
  return $this->sendNow($notifiables, $notification);
}

看來真正在幹活的是sendNow這個方法呀。

public function sendNow($notifiables, $notification, array $channels = null)
{
      $notifiables = $this->formatNotifiables($notifiables);
      //為了防止傳送期間內通知類資料被改動,這裡通過克隆來避免這個問題
      $original = clone $notification;
    foreach ($notifiables as $notifiable) {
    //為該條通知生產一個uuid
     $notificationId = Uuid::uuid4()->toString();
     //獲取傳送要採用的通道,可以採取多通道,此時取到的是陣列
     $channels = $channels ?: $notification->via($notifiable);
     if (empty($channels)) {
           continue;
     }
     foreach ($channels as $channel) {
     //恢復上面克隆的通知物件
       $notification = clone $original;
      /**
      因為傳入的通知物件可以在外部修改,所以這裡才要加上檢測,
      當用戶沒修改的時候才將id賦值為系統自動生成的,那麼為什麼
      不把前面生成uuid的語句放if裡面??
      **/
       if (!$notification->id) {
            $notification->id = $notificationId;
       }
       //是否成功觸發通知事件
       if (! $this->shouldSendNotification($notifiable, $notification, $channel)) {
                continue;
       }
        //終於要發了。。
         $response = $this->driver($channel)
         ->send($notifiable, $notification);
         //觸發訊息傳送事件
         $this->app->make('events')
         ->fire(new Events\NotificationSent($notifiable, $notification, $channel, $response)
                );
            }
        }
    }

簡單看看shouldSendNotification()

protected function shouldSendNotification($notifiable, $notification, $channel)
{
  /**如果已經返回一個非空的響應,則觸發通知傳送事件
   $this->app->make('events')返Illuminate\Events\Dispatcher物件**/
    return $this->app->make('events')->until(
     new Events\NotificationSending($notifiable,$notification, $channel)) !== false;
}
/**由於我們設定資料庫通知的方式,$this->driver($channel)返回Illuminate\Notifications\Channels\DatabaseChannel**/
$this->driver($channel)->send($notifiable, $notification);

逐漸逼近boss!DatabaseChannel的send方法~

public function send($notifiable,Notification $notification)
    {
        return $notifiable->routeNotificationFor('database')->create([
        'id' => $notification->id,
        'type' => get_class($notification),
        'data' => $this->getData($notifiable, $notification),
        'read_at' => null,
        ]);
    }

來看看$notifiable->routeNotificationFor(‘database’)返回了什麼

public function routeNotificationFor($driver)
    {
        if (method_exists($this, $method = 'routeNotificationFor'.Str::studly($driver))) {
            return $this->{$method}();
        }
        switch ($driver) {
            case 'database':
                return $this->notifications();
            case 'mail':
                return $this->email;
            case 'nexmo':
                return $this->phone_number;
        }
    }

可見,routeNotificationFor方法返回的是通知的傳送地址,由於本文分析的是資料庫通知,所以該方法返回的是該model的關聯notifications物件。
所以,沒錯,當我們呼叫接收者的notify方法時,最終是在關聯的notifications表中將具體通知類物件的資料插入(驅動是database情況下)。
下面是notification表的結構(type欄位是通知類的類名,notifiable_id是接收者的id,notifiable_type是接收者的類名):
notifications資料表結構

插入資料後:
這裡寫圖片描述

總結
可見,laravel的訊息通知的主要流程是:
1.建立一個繼承自DatabaseNotification的通知類

2.在接收者模型中新增Notifiable trait,其中通過HasDatabaseNotifications新增模型間的關聯關係,通過RoutesNotifications添加發送通知的方法

3.傳送通知:chanelManager只是提供了呼叫的介面,具體的傳送是通過在chanelManager中的sendNow方法中利用獲取到對應的驅動物件來完成傳送,在本文中,可以看到資料庫裡面的send方法就是對notifications表執行寫入操作。

筆者覺得最關鍵是要學到這種思想,把邏輯和功能分離,把邏輯放在上層,預留功能介面,通過驅動層來完成具體的動作和功能,這樣驅動層就只需要專心於實現功能,邏輯層專注於實現業務邏輯。