【Laravel】 通知に関する補足と拡張の手引き

# はじめに

まずはマニュアルの一読を。

- [通知 5.7 Laravel](https://readouble.com/laravel/5.7/ja/notifications.html)

基本的に Laravel のコンポーネントは完成度が高く，あまり自分で拡張する必要は無いが，「通知」に限っては機能拡張しないと実用性に乏しい面が多い。そのため，拡張すべきところとその実装例も一部示していくことにする。

なお通知機能は Laravel の他の基礎機能の上に構築されているため，それらに対する理解も必要だ。以下に関連する記事を示しておく。

- [【Laravel】サービスコンテナ・サービスプロバイダ・ファサード・契約に関する補足資料 - Qiita](https://qiita.com/mpyw/items/8b6f4666bfc98ff544dd)
- [【Laravel】 キュー・イベント・ブロードキャストに関する補足とフロントエンドへの導入 - Qiita](https://qiita.com/mpyw/items/90bc5a7f30a2812a7bcd)

# 通知のアーキテクチャ

全体の概要図を示す。通知の送信手段としては

- メール送信
- ブロードキャスト送信
- データベースレコード作成

などがあるが，ここでは例としてデータベースレコード作成を行う [`DatabaseChannel`] を Channel の1つとして使用している。

![notification](https://qiita-image-store.s3.amazonaws.com/0/25060/fba6b3f7-3d17-6c23-381e-dfc39ecfc9f6.png)

| 概念 | 説明 |
|:---|:---|
| **Notifiable** | 通知の送信先ユーザ。<br>基本的には [`App\User`] を表す。|
| **Channel** | 通知の送信経路。 <br>Notification 定義をもとに Message を生成し，<br>Notifiable に対して具体的な送信処理を行う。 |
| **Notification** | 通知の種類ごとの定義。<br>**基本的に [`ShouldQueue` 契約] を実装する。**<ul><li>**`via($notifiable)`** で<br> `$notifiable` への送信に用いる<br>  Channel のクラス名の配列を返す。</li><li><strong>`to{ドライバ名}($notifiable)`</strong> で<br>Channel および `$notifiable` に<br>対応する Message を生成して返す。</li></ul> |
| **Message** | 生成された通知の内容。 |

| クラス名 | 説明 |
|:---|:---|
| [`AnonymousNotifiable`] | Slack 通知など，送信先がユーザではない特殊な場合に使う。 |
| **[`ChannelManager`]** | 通知サービスのルートオブジェクト。<br>`Notification` ファサードに対応する。 |
| [`NotificationSender`] | [`ChannelManager`] から処理の委譲を受けるクラス。 |
| **[`SendQueuedNotifications`]** | キュー経由で送信を行うためのジョブ。<br>[`ShouldQueue` 契約] を実装した Notification に対して使用される。 | 
| [`DatabaseChannel`] | データベース通知作成のために使用される Channel。 |
| [`DatabaseMessage`] | Notification の `toDatabase()` メソッドが返す Message。 |
| **[`DatabaseNotification`]** | 通知の内容を格納する Eloquent Model。<br>デフォルトでは以下のカラムを持つ。<ul> <li>`id`<li>`type`<li>`notifiable_type`<li>`notifiable_id`<li>`data`<li>`read_at`<li>`created_at`<li>`updated_at`</ul> |

#### **<font color="blue"><ins>Channel に投入されるまでの流れ （バックグラウンド）</ins></font>**

通知は，キューに入る段階で1単位ごとにバラけられる。

1. Notifiable・Notification を引数にして **[`ChannelManager::send()`]** を実行する。
2. Notification の **`via()`** メソッドで使用する経路を特定し，<br>**Notifiable 1人・Channel 1経路ごとに**<br> [`SendQueuedNotifications`] ジョブがディスパッチされる。
3. [`SendQueuedNotifications::handle()`] メソッドの中で，<br>Notifiable・Notification・**Channel** を引数として [`ChannelManager::sendNow()`] が実行される。
4. Notifiable・Notification を引数として Channel の `send()` メソッドが実行される。
<br>

#### **<font color="red"><ins>Channel に投入されるまでの流れ （フォアグラウンド）</ins></font>**

通知は，送信直前に1単位ごとにバラけられる。

1. Notifiable・Notification を引数にして **[`ChannelManager::sendNow()`]** を実行する。
2. Notification の **`via()`** メソッドで使用する経路を特定し，<br>**Notifiable 1人・Channel 1経路ごとに**<br>Notifiable・Notification を引数として Channel の `send()` メソッドが実行される。
<br>

#### **<ins>Channel に投入された後の流れ</ins>** （ここでは [`DatabaseChannel`] について説明）

1. Notification の **`toDatabase()`** メソッドで [`DatabaseMessage`] が取得される。
2. [`DatabaseMessage`] を使って `data` フィールドを埋め， [`DatabaseNotification`] が作成される。

# 動作の流れ

ここから，ステップ別にソースコードリーディングを行っていく。

## Notifiable の準備

実は，**`Notifiable` 契約というものは存在せず**， [`Notifiable` トレイト]が存在するだけになっている。PHPDocの型には `mixed` が濫用されている形だ。

（型安全性が…）

```php
<?php

namespace Illuminate\Notifications;

trait Notifiable
{
    use HasDatabaseNotifications, RoutesNotifications;
}
```

ロジックは [`HasDatabaseNotifications`](https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/HasDatabaseNotifications.php) と [`RoutesNotifications`](https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/RoutesNotifications.php) に委譲されている。冗長なコメントを省いてこれらを合成したものを以下に示す。

```php
trait *
{
    public function notifications()
    {
        return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderBy('created_at', 'desc');
    }

    public function readNotifications()
    {
        return $this->notifications()->whereNotNull('read_at');
    }

    public function unreadNotifications()
    {
        return $this->notifications()->whereNull('read_at');
    }

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

    public function notifyNow($instance, array $channels = null)
    {
        app(Dispatcher::class)->sendNow($this, $instance, $channels);
    }

    public function routeNotificationFor($driver, $notification = null)
    {
        if (method_exists($this, $method = 'routeNotificationFor'.Str::studly($driver))) {
            return $this->{$method}($notification);
        }

        switch ($driver) {
            case 'database':
                return $this->notifications();
            case 'mail':
                return $this->email;
            case 'nexmo':
                return $this->phone_number;
        }
    }
}
```

最初の `notifications()` `readNotifications()` `unreadNotifications()` は **[`DatabaseNotification`] に関する**リレーションの定義である。

（…おい待て，ここで [`DatabaseNotification`] 決め打ちの実装書いちゃうってどうなんだ…まだ誰もこの機能を使うとは言ってないぞ）

`notify()` `notifyNow()` は自分自身を第1引数として [`ChannelManager::send()`] [`ChannelManager::sendNow()`] に処理を委譲している。これがあるため，例えば以下の2つの処理は等価になる。どちらを使ってもよい。

```php
$user->notify(new PostCreated($post));
```

```php
Notification::send($user, new PostCreated($post));
```

その下の `routeNotificationFor()` は **Channel が Notifiable から必要な情報をもらう**ために存在する。が，見ての通り返されるものが文字列だったりリレーションだったりぐちゃぐちゃ…

（型安全性…）

## Notification の準備

Notification はゼロから自分で作るわけではなく，[基底 `Notification` クラス]を継承して使用する形となっている。

```php
<?php

namespace Illuminate\Notifications;

use Illuminate\Queue\SerializesModels;

class Notification
{
    use SerializesModels;

    /**
     * The unique identifier for the notification.
     *
     * @var string
     */
    public $id;

    /**
     * The locale to be used when sending the notification.
     *
     * @var string|null
     */
    public $locale;

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return [];
    }

    /**
     * Set the locale to send this notification in.
     *
     * @param  string  $locale
     * @return $this
     */
    public function locale($locale)
    {
        $this->locale = $locale;

        return $this;
    }
}
```

**`$id`** と `$locale` は既定で存在する public フィールドだ。これらは後で登場するので存在だけ覚えておこう。そのほか， [`BroadcastChannel`] を使うためにブロードキャスト用のメソッドも存在している。

（…基底クラスで全部入りにしちゃうのやりすぎでは？）

さて，下に継承した定義例を示す。

- `via()` で通知する Channel クラス名の配列を返す。
- ここでは [`DatabaseChannel`] を利用することから, [`DatabaseMessage`] を返す `toDatabase()` メソッドを実装している。

いずれも Notifiable を引数で受け取るため， Notifiable の内容によって配信する Channel，生成される Message を切り替えることができる。**逆に言えば，Notifiable 自身を Notification インスタンスで保持するのはアーキテクチャ的にあまり正しくないようだ。送信相手を特定しない状態で Notification インスタンスを生成する必要がある。**

```php
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Channels\DatabaseChannel;
use Illuminate\Notifications\Messages\DatabaseMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;

class PostCreated extends Notification implements ShouldQueue
{
    use Queueable;

    /**
     * 通知チャンネルの取得
     *
     * @param  mixed  $notifiable
     * @return array|string
     */
    public function via($notifiable)
    {
        return [DatabaseChannel::class];
    }

    /**
     * 通知のデータベースプレゼンテーションを取得
     *
     * @param  mixed  $notifiable
     * @return DatabaseMessage
     */
    public function toDatabase($notifiable)
    {
        // ...
    }
}
```

## [`ChannelManager::send()`] の実行

```php
Notification::send($user, new PostCreated($post));
```

を実行したときの流れを追ってみよう。まず， [`ChannelManager`] の中身を見る。

```php
<?php

namespace Illuminate\Notifications;

/* ... */
use Illuminate\Support\Manager;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Bus\Dispatcher as Bus;
use Illuminate\Contracts\Notifications\Factory as FactoryContract;
use Illuminate\Contracts\Notifications\Dispatcher as DispatcherContract;

class ChannelManager extends Manager implements DispatcherContract, FactoryContract
{
    /* ... */
    protected $locale;

    /**
     * Send the given notification to the given notifiable entities.
     *
     * @param  \Illuminate\Support\Collection|array|mixed  $notifiables
     * @param  mixed  $notification
     * @return void
     */
    public function send($notifiables, $notification)
    {
        return (new NotificationSender(
            $this, $this->app->make(Bus::class), $this->app->make(Dispatcher::class), $this->locale)
        )->send($notifiables, $notification);
    }

    /* ... */
}
```

このクラスは処理のほとんどを [`NotificationSender`] に委譲しているようだ。

- [`ChannelManager`]
- Bus Dispatcher
- Event Dispatcher
- ロケール（既定値は `null`）

これらの値をコンテナも使って揃えた上で，丸投げしている。続いて [`NotificationSender`] を見ると

```php
<?php

namespace Illuminate\Notifications;

use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Traits\Localizable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Translation\HasLocalePreference;
use Illuminate\Database\Eloquent\Collection as ModelCollection;

class NotificationSender
{
    /* ... */

    /**
     * The notification manager instance.
     *
     * @var \Illuminate\Notifications\ChannelManager
     */
    protected $manager;

    /**
     * The Bus dispatcher instance.
     *
     * @var \Illuminate\Contracts\Bus\Dispatcher
     */
    protected $bus;

    /**
     * The event dispatcher.
     *
     * @var \Illuminate\Contracts\Events\Dispatcher
     */
    protected $events;

    /**
     * The locale to be used when sending notifications.
     *
     * @var string|null
     */
    protected $locale;

    /**
     * Create a new notification sender instance.
     *
     * @param  \Illuminate\Notifications\ChannelManager  $manager
     * @param  \Illuminate\Contracts\Bus\Dispatcher  $bus
     * @param  \Illuminate\Contracts\Events\Dispatcher  $events
     * @param  string|null  $locale
     * @return void
     */
    public function __construct($manager, $bus, $events, $locale = null)
    {
        $this->bus = $bus;
        $this->events = $events;
        $this->manager = $manager;
        $this->locale = $locale;
    }

    /**
     * Send the given notification to the given notifiable entities.
     *
     * @param  \Illuminate\Support\Collection|array|mixed  $notifiables
     * @param  mixed  $notification
     * @return void
     */
    public function send($notifiables, $notification)
    {
        $notifiables = $this->formatNotifiables($notifiables);

        if ($notification instanceof ShouldQueue) {
            return $this->queueNotification($notifiables, $notification);
        }

        return $this->sendNow($notifiables, $notification);
    }

    /**
     * Send the given notification immediately.
     *
     * @param  \Illuminate\Support\Collection|array|mixed  $notifiables
     * @param  mixed  $notification
     * @param  array  $channels
     * @return void
     */
    public function sendNow($notifiables, $notification, array $channels = null)
    {
        /* ... */
    }

    /* ... */

    /**
     * Queue the given notification instances.
     *
     * @param  mixed  $notifiables
     * @param  array[\Illuminate\Notifications\Channels\Notification]  $notification
     * @return void
     */
    protected function queueNotification($notifiables, $notification)
    {
        /* ... */
    }

    /**
     * Format the notifiables into a Collection / array if necessary.
     *
     * @param  mixed  $notifiables
     * @return \Illuminate\Database\Eloquent\Collection|array
     */
    protected function formatNotifiables($notifiables)
    {
        if (! $notifiables instanceof Collection && ! is_array($notifiables)) {
            return $notifiables instanceof Model
                            ? new ModelCollection([$notifiables]) : [$notifiables];
        }

        return $notifiables;
    }
}
```

`formatNotifiables()` で単一の通知対象をコレクションでラップした上で，処理を `queueNotification()` または `sendNow()` で続けている。**[`ShouldQueue` 契約]を実装している場合，キューに投入される。**

## キューへの投入

ここではまず，キューに投入される前者を見てみよう。

```php
    /**
     * Queue the given notification instances.
     *
     * @param  mixed  $notifiables
     * @param  array[\Illuminate\Notifications\Channels\Notification]  $notification
     * @return void
     */
    protected function queueNotification($notifiables, $notification)
    {
        $notifiables = $this->formatNotifiables($notifiables);

        $original = clone $notification;

        foreach ($notifiables as $notifiable) {
            $notificationId = Str::uuid()->toString();

            foreach ($original->via($notifiable) as $channel) {
                $notification = clone $original;

                $notification->id = $notificationId;

                if (! is_null($this->locale)) {
                    $notification->locale = $this->locale;
                }

                $this->bus->dispatch(
                    (new SendQueuedNotifications($notifiable, $notification, [$channel]))
                            ->onConnection($notification->connection)
                            ->onQueue($notification->queue)
                            ->delay($notification->delay)
                );
            }
        }
    }
```

ここで大雑把に流れを示すと，以下のような2重ループになっている。

1. **Notifiable ごとに UUID を作成して，それぞれ別の Notification オブジェクトとして扱う**
2. さらに， **Channel ごとに通知送信のためのジョブを発行している**

![download.png](https://qiita-image-store.s3.amazonaws.com/0/25060/44290471-16e7-55c4-4eee-8ffad9aab558.png)

注意すべきは，主キーとして **UUID** が使われていることだ。ここでもし[パフォーマンス上の理由](https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/)やソートにおける利便性での理由で **Ordered UUID** を代わりに使う場合，このクラスの拡張が必須となる。

## キューからの取り出し

次に，キューから取り出されたときに実行される [`SendQueuedNotifications::handle()`] を見てみよう。

```php
<?php

namespace Illuminate\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendQueuedNotifications implements ShouldQueue
{
    use Queueable, SerializesModels;

    /* ... */

    /**
     * Create a new job instance.
     *
     * @param  \Illuminate\Support\Collection  $notifiables
     * @param  \Illuminate\Notifications\Notification  $notification
     * @param  array  $channels
     * @return void
     */
    public function __construct($notifiables, $notification, array $channels = null)
    {
        $this->channels = $channels;
        $this->notifiables = $notifiables;
        $this->notification = $notification;
        $this->tries = property_exists($notification, 'tries') ? $notification->tries : null;
        $this->timeout = property_exists($notification, 'timeout') ? $notification->timeout : null;
    }

    /**
     * Send the notifications.
     *
     * @param  \Illuminate\Notifications\ChannelManager  $manager
     * @return void
     */
    public function handle(ChannelManager $manager)
    {
        $manager->sendNow($this->notifiables, $this->notification, $this->channels);
    }

    /* ... */

    /**
     * Call the failed method on the notification instance.
     *
     * @param  \Exception  $e
     * @return void
     */
    public function failed($e)
    {
        if (method_exists($this->notification, 'failed')) {
            $this->notification->failed($e);
        }
    }

    /* ... */
}
```

もう一度 [`ChannelManager::sendNow()`] を実行し直しているに過ぎないが，この時点ですでに **Notifiable 1人・Channel 1経路ごとに** ジョブ単位で分割されていることに注意されたい。

## [`ChannelManager::sendNow()`] の実行

更に [`ChannelManager::sendNow()`] を実行したときの流れを追ってみよう。もし `SendQueuedNotifications` から自動的に投げられる場合は

```php
Notification::sendNow([$user], new PostCreated($post), [$channel]);
```

のような呼び出しとなる。 [`ChannelManager`] の中身を見ると，

```php
<?php

namespace Illuminate\Notifications;

/* ... */
use Illuminate\Support\Manager;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Bus\Dispatcher as Bus;
use Illuminate\Contracts\Notifications\Factory as FactoryContract;
use Illuminate\Contracts\Notifications\Dispatcher as DispatcherContract;

class ChannelManager extends Manager implements DispatcherContract, FactoryContract
{
    /* ... */
    protected $locale;

    /* ... */

    /**
     * Send the given notification immediately.
     *
     * @param  \Illuminate\Support\Collection|array|mixed  $notifiables
     * @param  mixed  $notification
     * @param  array|null  $channels
     * @return void
     */
    public function sendNow($notifiables, $notification, array $channels = null)
    {
        return (new NotificationSender(
            $this, $this->app->make(Bus::class), $this->app->make(Dispatcher::class), $this->locale)
        )->sendNow($notifiables, $notification, $channels);
    }
}
```

同じように処理のほとんどが [`NotificationSender`] に委譲され，

```php
/**
 * Send the given notification immediately.
 *
 * @param  \Illuminate\Support\Collection|array|mixed  $notifiables
 * @param  mixed  $notification
 * @param  array  $channels
 * @return void
 */
public function sendNow($notifiables, $notification, array $channels = null)
{
    $notifiables = $this->formatNotifiables($notifiables);

    $original = clone $notification;

    foreach ($notifiables as $notifiable) {
        if (empty($viaChannels = $channels ?: $notification->via($notifiable))) {
            continue;
        }

        $this->withLocale($this->preferredLocale($notifiable, $notification), function () use ($viaChannels, $notifiable, $original) {
            $notificationId = Str::uuid()->toString();

            foreach ((array) $viaChannels as $channel) {
                $this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel);
            }
        });
    }
}
```

先ほどとほぼ同じような2重ループ処理が行われる。

```php
/**
 * Send the given notification to the given notifiable via a channel.
 *
 * @param  mixed  $notifiable
 * @param  string  $id
 * @param  mixed  $notification
 * @param  string  $channel
 * @return void
 */
protected function sendToNotifiable($notifiable, $id, $notification, $channel)
{
    if (! $notification->id) {
        $notification->id = $id;
    }

    if (! $this->shouldSendNotification($notifiable, $notification, $channel)) {
        return;
    }

    $response = $this->manager->driver($channel)->send($notifiable, $notification);

    $this->events->dispatch(
        new Events\NotificationSent($notifiable, $notification, $channel, $response)
    );
}

/**
 * Determines if the notification can be sent.
 *
 * @param  mixed  $notifiable
 * @param  mixed  $notification
 * @param  string  $channel
 * @return bool
 */
protected function shouldSendNotification($notifiable, $notification, $channel)
{
    return $this->events->until(
        new Events\NotificationSending($notifiable, $notification, $channel)
    ) !== false;
}
```

これはイベントハンドラでキャンセルされなければ，

```php
$this->manager->driver($channel)->send($notifiable, $notification);
```

が実行されることになる。[`ChannelManager`] の処理を追うと，Laravel おなじみのマジックメソッドを使ったドライバ実装パターンが見える。ここでいうドライバとは Channel のことだ。

```php
<?php

namespace Illuminate\Notifications;

use InvalidArgumentException;
use Illuminate\Support\Manager;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Bus\Dispatcher as Bus;
use Illuminate\Contracts\Notifications\Factory as FactoryContract;
use Illuminate\Contracts\Notifications\Dispatcher as DispatcherContract;

class ChannelManager extends Manager implements DispatcherContract, FactoryContract
{
    /* ... */

    /**
     * Create an instance of the database driver.
     *
     * @return \Illuminate\Notifications\Channels\DatabaseChannel
     */
    protected function createDatabaseDriver()
    {
        return $this->app->make(Channels\DatabaseChannel::class);
    }

    /**
     * Create an instance of the broadcast driver.
     *
     * @return \Illuminate\Notifications\Channels\BroadcastChannel
     */
    protected function createBroadcastDriver()
    {
        return $this->app->make(Channels\BroadcastChannel::class);
    }

    /**
     * Create an instance of the mail driver.
     *
     * @return \Illuminate\Notifications\Channels\MailChannel
     */
    protected function createMailDriver()
    {
        return $this->app->make(Channels\MailChannel::class);
    }

    /**
     * Create a new driver instance.
     *
     * @param  string  $driver
     * @return mixed
     *
     * @throws \InvalidArgumentException
     */
    protected function createDriver($driver)
    {
        try {
            return parent::createDriver($driver);
        } catch (InvalidArgumentException $e) {
            if (class_exists($driver)) {
                return $this->app->make($driver);
            }

            throw $e;
        }
    }
}
```

`createDriver()` の実装を見ると，以下の2つのパターンに対応していることが分かる。

```php:メソッド名の一部を指定
$this->manager->driver('database');
```

```php:Channel&#32;クラス名を完全修飾で指定
$this->manager->driver(DatabaseChannel::class);
```

これによって生成された Channel の `send()` メソッドが呼ばれる。

## `Channel::send()` の実行

ここでは一例として， [`DatabaseChannel`] の実装を見てみよう。

![DatabaseChannel](https://qiita-image-store.s3.amazonaws.com/0/25060/baf3c7cf-6de0-3d34-254f-78ae23ec810f.png)

```php
<?php

namespace Illuminate\Notifications\Channels;

use RuntimeException;
use Illuminate\Notifications\Notification;

class DatabaseChannel
{
    /**
     * Send the given notification.
     *
     * @param  mixed  $notifiable
     * @param  \Illuminate\Notifications\Notification  $notification
     * @return \Illuminate\Database\Eloquent\Model
     */
    public function send($notifiable, Notification $notification)
    {
        return $notifiable->routeNotificationFor('database', $notification)->create(
            $this->buildPayload($notifiable, $notification)
        );
    }

    /**
     * Get the data for the notification.
     *
     * @param  mixed  $notifiable
     * @param  \Illuminate\Notifications\Notification  $notification
     * @return array
     *
     * @throws \RuntimeException
     */
    protected function getData($notifiable, Notification $notification)
    {
        if (method_exists($notification, 'toDatabase')) {
            return is_array($data = $notification->toDatabase($notifiable))
                                ? $data : $data->data;
        }

        if (method_exists($notification, 'toArray')) {
            return $notification->toArray($notifiable);
        }

        throw new RuntimeException('Notification is missing toDatabase / toArray method.');
    }

    /**
     * Build an array payload for the DatabaseNotification Model.
     *
     * @param  mixed  $notifiable
     * @param  \Illuminate\Notifications\Notification  $notification
     * @return array
     */
    protected function buildPayload($notifiable, Notification $notification)
    {
        return [
            'id' => $notification->id,
            'type' => get_class($notification),
            'data' => $this->getData($notifiable, $notification),
            'read_at' => null,
        ];
    }
}
```

`routeNotificationFor()` は， ここでは `notifiable_type` `notifiable_id` が埋まったリレーションを返す。これを踏まえた上で，メソッド分割を外して最小限の処理を書き下すと以下のようになる。

```php
<?php

namespace Illuminate\Notifications\Channels;

use RuntimeException;
use Illuminate\Notifications\Notification;

class DatabaseChannel
{
    /**
     * Send the given notification.
     *
     * @param  mixed  $notifiable
     * @param  \Illuminate\Notifications\Notification  $notification
     * @return \Illuminate\Database\Eloquent\Model
     */
    public function send($notifiable, Notification $notification)
    {
        return $notifiable->morphMany(DatabaseNotification::class, 'notifiable')->create([
            'id' => $notification->id,
            'type' => get_class($notification),
            'data' => $notification->toDatabase($notifiable)->data,
            'read_at' => null,
        ]);
    }
}
```

これで，データベース通知が生成されて処理が終了する。が， `data` カラムの型は　[`notification.stub`](https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/Console/stubs/notifications.stub) を見ると

```php
/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('notifications', function (Blueprint $table) {
        $table->uuid('id')->primary();
        $table->string('type');
        $table->morphs('notifiable');
        $table->text('data');
        $table->timestamp('read_at')->nullable();
        $table->timestamps();
    });
}
```

となっている通り，ただの `TEXT` 型となっている。

（…検索性悪すぎない？）

さて，数々の本家による「微妙な実装」が見えたところで，問題点と改善策についての提案をしていこう。

# 拡張すべきポイントとその手引き

問題点を箇条書きで挙げてみる。

- `Notifiable` 契約が存在しない
- [`Notifiable` トレイト]が [`DatabaseNotification`] の存在を前提としている
- Notifiable の `routeNotificationFor()` メソッドが型安全を破壊する行き過ぎた抽象化をしている<br>⇔ Channel が具象性を放棄しすぎている
- [基底 `Notification` クラス]が [`BroadcastChannel`] の存在を前提としている
- **`Notification::$id` に Ordered UUID ではなく UUID が使われている**
- **[`DatabaseNotification`] に使われる `notifications` テーブルの検索性が悪い**
- [`DatabaseNotification`] を作成し，その結果レコードを別 Channel から送信するという手段が選択できない
- [`DatabaseChannel`] 経由で通知を更新する手段がない

挙げだすとキリが無いが，このうちの一部を改善できる実装例を以下に示す。

## 「リソース」通知という概念の導入

ある通知に対し，何かしらリソースを1つ関連付けたいケースは多い。例えば

- 新規投稿があります
- 動画のエンコードが終わりました

こういう通知であれば，

- `resource_type=post, resource_id=xxx`
- `resource_type=video, resource_id=yyy`

のような情報を持たせたくなるだろう。これを [`DatabaseNotification`] と合わせて採用する場合は， `notifications` テーブルの定義自体を拡張しておこう。こうすることで， `data` に雑に突っ込むよりは検索性が大幅に向上する。

```php
$table->string('resource_type')->nullable();
$table->unsignedBigInteger('resource_id')->nullable();
```

その上で，例えば以下のように実装するとよい。クラス名のインポートなどは省略する。

```php
class PostCreated extends Notification
{
    use Queueable, InteractsWithSockets;

    public $resource;

    public function __construct(Post $post)
    {
        $this->resource = $post;
    }

    public function via($notifiable): array
    {
        return [DatabaseChannel::class];
    }

    public function toDatabase(): DatabaseMessage
    {
        return new DatabaseMessage('...');
    }
}
```

```php
use Illuminate\Notifications\Channels\DatabaseChannel as BaseDatabaseChannel;

class DatabaseChannel extends BaseDatabaseChannel
{
    protected function buildPayload($notifiable, Notification $notification)
    {
        return [
            'id' => $notification->id,
            'type' => get_class($notification),
            'data' => $this->getData($notifiable, $notification),
            'resource_type' => $notification->resource->getMorphClass() ?? null,
            'resource_id' => $notification->resource->getKey() ?? null,
            'read_at' => null,
        ];
    }
}
```

[`DatabaseNotification`] の `resource` に関する Eager Loading 対応は割愛する。
（それなりに大変）

## UUID を Ordered UUID に変更する

これを実施するには，[`ChannelManager`] および [`NotificationSender`] の置き換えが必要だ。

```php:app/Providers/NotificationServiceProvider.php
<?php

declare(strict_types=1);

namespace App\Providers;

use App\Notifications\ChannelManager;
use Illuminate\Notifications\ChannelManager as BaseChannelManager;
use Illuminate\Notifications\NotificationServiceProvider as BaseNotificationServiceProvider;

class NotificationServiceProvider extends BaseNotificationServiceProvider
{
    public function register(): void
    {
        parent::register();

        $this->app->singleton(BaseChannelManager::class, function ($app) {
            return new ChannelManager($app);
        });
        $this->app->alias(BaseChannelManager::class, ChannelManager::class);
    }
}
```

```php:app/Notifications/NotificationSender.php
<?php

declare(strict_types=1);

namespace App\Notifications;

use Illuminate\Notifications\NotificationSender as BaseNotificationSender;
use Illuminate\Notifications\SendQueuedNotifications;
use Illuminate\Support\Str;

class NotificationSender extends BaseNotificationSender
{
    public function sendNow($notifiables, $notification, array $channels = null): void
    {
        $notifiables = $this->formatNotifiables($notifiables);
        $original = clone $notification;

        foreach ($notifiables as $notifiable) {
            if (empty($viaChannels = $channels ?: $notification->via($notifiable))) {
                continue;
            }

            $notificationId = (string)Str::orderedUuid(); // ←ここを変更
            foreach ((array)$viaChannels as $channel) {
                $this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel);
            }
        }
    }

    protected function queueNotification($users, $notification): void
    {
        $notifiables = $this->formatNotifiables($notifiables);
        $original = clone $notification;

        foreach ($notifiables as $notifiable) {
            $notificationId = (string)Str::orderedUuid(); // ←ここを変更

            foreach ($original->via($user) as $channel) {
                $notification = clone $original;
                $notification->id = $notificationId;

                $this->bus->dispatch(
                    (new SendQueuedNotifications($notifiable, $notification, [$channel]))
                        ->onConnection($notification->connection)
                        ->onQueue($notification->queue)
                        ->delay($notification->delay)
                );
            }
        }
    }
}
```

但し，**バージョン 5.7 時点では Laravel 本家のサービスプロバイダの実装にバグがある**ため， composer.json の書き換えが必須になる。詳しくは以下の Issue を参照。（5.7 にもマージしてってお願いしたけど完全にスルーされた）

- [Fix Notification service resolution in provider by mpyw · Pull Request #10 · laravel/slack-notification-channel](https://github.com/laravel/slack-notification-channel/pull/10)
- [Fix Notification service resolution in provider by mpyw · Pull Request #5 · laravel/nexmo-notification-channel](https://github.com/laravel/nexmo-notification-channel/pull/5)

# あとがき

本音を言うと，Laravel の Notification サービスは，**全サービス中最も実装が汚い**と言ってもいいだろう。尤も，非常に抽象化が難しい内容でもあるのだが…
（今回の記事も「どこまで改善例を出せばいいだろう…」というので非常に悩みが大きかった）

Laravel 6 以降のリファクタリングに期待しよう。

[`ChannelManager::send()`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/ChannelManager.php#L28-L40
[`ChannelManager::sendNow()`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/ChannelManager.php#L42-L55
[`Notifiable` トレイト]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/Notifiable.php
[基底 `Notification` クラス]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/Notification.php
[`Notification::send()`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/ChannelManager.php#L28-L40
[`NotificationSender`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/NotificationSender.php
[`ShouldQueue` 契約]: https://github.com/illuminate/contracts/blob/b1d565887f3b2223157a9efd15b9e542c0b0e01d/Queue/ShouldQueue.php
[`SendQueuedNotifications`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/SendQueuedNotifications.php
[`SendQueuedNotifications::handle()`]: https://github.com/laravel/framework/blob/c0b09518d4b525046b5902beb1d02df6fefb2e12/src/Illuminate/Notifications/SendQueuedNotifications.php#L65-L74
[`DatabaseNotification`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/DatabaseNotification.php
[`App\User`]: https://github.com/laravel/laravel/blob/1f0640f99d045ca2eeec7c87a5c6644c197f466b/app/User.php
[`AnonymousNotifiable`]: https://github.com/illuminate/notifications/blob/master/AnonymousNotifiable.php
[`ChannelManager`]: https://github.com/illuminate/notifications/blob/master/ChannelManager.php
[`DatabaseChannel`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/Channels/DatabaseChannel.php
[`DatabaseChannel::send()`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/Channels/DatabaseChannel.php#L10-L22
[`DatabaseMessage`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/Channels/DatabaseChannel.php
[`BroadcastChannel`]: https://github.com/illuminate/notifications/blob/163f91ba6bf424759cd8eb1d9a21df1f12375fa4/Channels/BroadcastChannel.php