扒一扒 laravel的訊息通知(上)
laravel給我們提供了多渠道的訊息通知功能,包括郵件,簡訊,資料庫,slack等通知方式。本文主要分析基於資料庫的訊息通知的底層實現。為了方便,本文將需要接受通知訊息的模型稱為接收者。
ps:閱讀本文前,請不瞭解Eloquent關聯關係的讀者先點選eloquent relations瞭解相關內容
通過官方文件可以知道,當我們需要開啟一個model接收訊息通知的功能時,需要在模型中新增Illuminate\Notifications\Notifiable
這個trait。通過程式碼可以發現,這個trait實際上只是使用了HasDatabaseNotifications, RoutesNotifications;
先來看看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是接收者的類名):
插入資料後:
總結
可見,laravel的訊息通知的主要流程是:
1.建立一個繼承自DatabaseNotification的通知類
2.在接收者模型中新增Notifiable trait,其中通過HasDatabaseNotifications新增模型間的關聯關係,通過RoutesNotifications添加發送通知的方法
3.傳送通知:chanelManager只是提供了呼叫的介面,具體的傳送是通過在chanelManager中的sendNow方法中利用獲取到對應的驅動物件來完成傳送,在本文中,可以看到資料庫裡面的send方法就是對notifications表執行寫入操作。
筆者覺得最關鍵是要學到這種思想,把邏輯和功能分離,把邏輯放在上層,預留功能介面,通過驅動層來完成具體的動作和功能,這樣驅動層就只需要專心於實現功能,邏輯層專注於實現業務邏輯。