「send() の真実」─ShouldQueue と sendNow の分岐を深掘り【Laravel Notification】

## 導入
個人開発にハマり中の新卒のエンジニアです。
簡単な自己紹介は以下に
- 私立文系卒
    - 大学2年の冬から
    - 学生時代は1年半ほどプログラミングスクールでインターン
- めちゃめちゃ柏レイソルサポーター
    - 本当に今年は最高

本記事では、NotificationSenderクラスを掘り下げていきます。

僕が以前書いた記事では、通知のリクエストからNotificationSenderの直前までをまとめました。↓

https://qiita.com/JonyTask/items/402c48fb2b9a8ffeefef

このNotificationSenderは、認証に限らず、通知の根幹を成している部分です。
これを丁寧に掘り下げていきます。

- **ソースコードのリーディングで理解を深めたい**
- **通知について知りたい**

こんな方に読んでいただければ光栄です。

## NotificationSenderを読んでいこう
NotificationSenderは以下のリポジトリの、以下のパスにあります。

https://github.com/laravel/framework

framework/src/Illuminate/Notifications/NotificationSender.php
このリポジトリは、Laravelの様々なサービスやコンポーネントを構成している基盤です。

### まずはコンストラクタだけ
まずは以下で名前空間、クラス宣言、プロパティ宣言、コンストラクタのみを載せます。
```
<?php

namespace Illuminate\Notifications;

class NotificationSender
{
    use Localizable;

    protected $manager;
    protected $bus;
    protected $events;
    protected $locale;

    public function __construct($manager, $bus, $events, $locale = null)
    {
        $this->bus = $bus;
        $this->events = $events;
        $this->locale = $locale;
        $this->manager = $manager;
    }
}
```

インスタンスが生成されると、これら4つのプロパティが初期化されます。
これら4つのプロパティの役割について調べることが、NotificationSenderを理解する近道になりそうです。
#### 4つのプロパティの役割
ソースコードを読み進めることでなんとなく掴んだプロパティの役割です。
<dl>
<dt>$this->bus</dt>
<dd>ジョブが追加されるキューへの接続を格納したDispatcherインスタンスが入る。</dd>
<dd>framework/src/Illuminate/Bus/BusServiceProvider.php 23-25行目</dd>
<dt>$this->events</dt>
<dd>新しいイベントを格納したDispatcherインスタンスが入る。イベントリスナーに関するメソッドが多く見られる。</dd>
<dd>framework/src/Illuminate/Events/Dispatcherインスタンス</dd>
<dt>$this->locale</dt>
<dd>nullがデフォルト値</dd>
<dt>$this->manager</dt>
<dd>チャネルを管理しているインスタンスが入る。(データベースチャネルや、メールチャネルなど。送信先によってもチャネルは変化する。)</dd>
<dd>framework/src/Illuminate/Notifications/ChannelManagerインスタンスが入ることが多そう。</dd>
</dl>

これらの情報をもとに、この後送信ロジックを走らせるわけですね。
### send()
```
     public function send($notifiables, $notification)
    {
        $notifiables = $this->formatNotifiables($notifiables);

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

        $this->sendNow($notifiables, $notification);
    }
```
    
こちらが送信ロジックです。

 `$notifiables = $this->formatNotifiables($notifiables);`
 
この`formatNotifiables()`メソッドでは、簡単に言うと、オブジェクトを配列に変換しています。これはループ処理をしやすくするための処理です。

そしてその後、渡されてきた通知オブジェクトが`ShouldQueue`インターフェイスを実装していれば、キューを参照して`queueNotification` による通知を走らせます。

そうでなければ、`sendnow() `による通知を走らせます。

### sendnow()内では
```
    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) {
                    if (! ($notifiable instanceof AnonymousNotifiable && $channel === 'database')) {
                        $this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel);
                    }
                }
            });
        }
    }
```
上のコードを分かりやすく要約するとこんな感じです。

<hr>

```
$notifiables = $this->formatNotifiables($notifiables);
```
こちらのコードでは、送信先について入っているオブジェクトをコレクションまたは配列に揃えています。
<hr>

```
$original = clone $notification;
```
こちらのコードでは、渡されてきた$notificationオブジェクトを複製しています。
clone で複製しているのは、cloneしないとオブジェクトを共有するためです。
破壊的変更は避けたいです。
<hr>

```
foreach ($notifiables as $notifiable) {
```
まずは外側の繰り返し処理です。
それぞれの通知エンティティごとに、どのチャネルで送るかを判断します。
その後、内側の繰り返し処理に入ります。
<hr>

```
if (empty($viaChannels = $channels ?: $notification->via($notifiable))) {
    continue;
}
```
こちらが、どのチャネルで送るかを判断している様です。
チャネルが設定されていなければ、通知エンティティの`$via()`メソッドでチャネルを取得します。

もしそれでもチャネルが設定されていなければ、次のループにスキップします。
その通知エンティティは実行されないということですね。
<hr>

```
$this->withLocale(
    $this->preferredLocale($notifiable,$notification),
    function () use ($viaChannels, $notifiable, $original) {
```
この部分が分かりづらいですね。
ここでは、通知エンティティのロケールを決定しています。
ロケールとは、言語と地域の組み合わせの識別子です。
決定されたロケールごとに、通知内容を翻訳したりもできるみたいです。
<hr>

```
foreach ((array) $viaChannels as $channel) {
    if (!($notifiable instanceof AnonymousNotifiable && $channel === 'database')) {
        $this->sendToNotifiable(
            $notifiable,
            $notificationId,
            clone $original,
            $channel
        );
    }
}
```
ここでは、決定されたチャネルごとに通知を渡しています。
チャネルがデータベースでない場合に通知を渡します。

この理由として、
1. 保存コスト／ゴミデータ を避ける
1. 不要な個人情報の蓄積リスク を減らす
1. 「匿名宛先＝一過性、database＝永続」の責務を明確に分離

が挙げられるみたいです。
これはChat GPT o3に頼りました。
<hr>

## 終わり
コードリーディングを進めていくと、クラス間の構造について、理解度が上がっている感覚があります。
ものすごく良いものですね。

最近は新卒研修中にQiita記事を書いています。

環境に文句は言わず、その時できる努力をすることが、どの時代も大事なことです。

「それでも！」と言い続けます。
（パチンカスには分かる）