Laravel Notification をやさしく解説する

これはなに？
========
Laravel 6.x で通知を実装するときのお作法を解説します。
全ては [ドキュメント](https://laravel.com/docs/6.x/notifications) に書いてある通りなんですが、ちょっとわかりにくいと思いました。
僕は一読しただけでは理解できませんでした。

なのでこの記事では、自分なりに噛み砕いた表現で Laravel Notification を説明します。
ドキュメントをぱっと読んでみたけどわからんかった、という人の理解の助けになれば幸いです。

### 解説すること

* 基本的な通知の実装方法
* カスタム通知チャネルの実装方法
    * AWS SDK を利用したサンプル

### 解説しないこと

* 通知方法ごとの通信処理の設定方法
    * 例えば、メール送信のそのものの設定方法など
* 通知イベントの取り扱い
* キューとワーカーを用いた通知の非同期処理

### 対象読者

* Laravel で通知を初めて実装する人
* 通知のドキュメントを読んだけどよく分からなった人

### 読まなくてもいい人

* 通知のドキュメントの内容をおおよそ理解できている人

[Notifications - Laravel](https://laravel.com/docs/6.x/notifications)

通知の三大要素
========

Laravel の通知には大きく 3 つの要素があります。

1. 通知メッセージ
2. 通知方法
3. 通知先

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62103/0e1c35d4-27ec-8cb1-053d-e85d6d408f7b.png)

実装では、これらの要素を Class や Trait ( を use した Class ) として作っていきます。

1. Notification Class 
2. Channel Class
3. Notifiable Class ( used Notifiable Trait )

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62103/6afd4531-231a-8ed8-a86c-d2fd89205a56.png)

以降では、**ユーザー ( 通知先 )** が **予約を申し込んだこと ( 通知メッセージ )** を **メール ( 通知方法 )** で通知する、というシナリオに沿ってサンプルを提示しながら解説していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62103/5eb279f0-ae28-d939-5980-e18cd0c75190.png)

通知メッセージ ( Notification )
========

* `Notification` Class
* `php artisan make:notification` で `app/Notifications` 配下に作成される

通知メッセージは以下の機能を持ちます。

* 通知する内容 ( メッセージ ) を生成
* 通知方法を選択

## 通知する内容 ( メッセージ ) を生成

* 例えば、メール通知ならメール本文
* 例えば、 Slack 通知なら Slack メッセージ本文

通知内容を生成する流れとしては、以下がお決まりのパターンです。

### 1. コンストラクタで通知内容を生成する情報源を受け取る

```php:app/Notifications/Reserved.php
public function __construct(Reservation $reservation)
{
    $this->reservation = $reservation;
}
```

### 2. 通知方法に応じた通知内容を返すメソッドを実装する

* 例えば、メール通知なら `toMail()` に実装
* 例えば、 Slack 通知なら `toSlack()` に実装

[参考: Formatting Mail Messages](https://laravel.com/docs/6.x/notifications#formatting-mail-messages)

```php:app/Notifications/Reserved.php
public function toSlack($notifiable)
{
    return (new MailMessage)
        ->greeting('予約を受け付けました')
        ->line('予約番号:' . $this->reservation->id)
        ->line('予約日:' . $this->reservation->date)
        ->line('氏名:' . $notifiable->name)
        ->line('当日のご来店をお待ちしております。');
}
```

`$notifiable` は、後ほど解説する通知先のインスタンスです。
上記のサンプルコードは予約をした文脈なので、予約を申し込んだユーザーが `$notifiable` に入ります。

## 通知方法を選択

[参考: Specifying Delivery Channels](https://laravel.com/docs/6.x/notifications#specifying-delivery-channels)

`via()` メソッドで通知方法を選択します。
一つの通知方法しかないなら決め打ちで良いです。

```php:app/Notifications/Reserved.php
public function via($notifiable)
{
    return ['mail'];
}
```

環境によって切り替えたいというのは、よくあるニーズかもしれません。
例えば、ローカル環境ではテストのため Slack に通知を飛ばすなど。

```php:app/Notifications/Reserved.php
public function via($notifiable)
{
    return \App::environment('local') ? ['slack'] : ['mail'];
}
```

ユーザーの属性によって通知先を変えることもあるでしょう。
例えば、とあるユーザーはメールではなく SMS で通知を受けたいなど。

```php:app/Notifications/Reserved.php
public function via($notifiable)
{
    return $notifiable->prefers_sms ? ['nexmo'] : ['mail'];
}
```

## Notification class の使い方

実際に通知を送る処理を書くときには、実装した Notification class をインスタンス化して利用します。

```php
use App\Notifications\Reserved;

$notification = new Reserved($reservation);
```

通知先 ( Notifiable )
========

* 通知先となるインスタンス
* 例えば、今回の解説では予約を申し込んだ **ユーザー**

通知先は以下の機能を持ちます。

* 通知送信メソッド
* 通知に伴う通信先情報の提供


## 通知送信メソッド

実装はとても簡単で、通知先にしたいクラスで `Notifiable` trait を use するだけです。

```php:app/User.php
<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}
```

[参考: Using The Notifiable Trait](https://laravel.com/docs/6.x/notifications#using-the-notifiable-trait)

これだけで、ユーザーに通知を送れるようになります。

```php
use App\Notifications\Reserved;

$user->notify(new Reserved($reservation));
```

複数のユーザーに同一の通知を送りたい場合は、 `Notification` ファサードの `send()` メソッドが便利です。

```php
Notification::send($users, new Reserved($reservation));
```

`$users` は `Notifiable` インスタンスの配列もしくはコレクションです。


## 通知方法に応じた通信先情報の提供

通知方法としてメールを送信する場合、送信先のメールアドレスが必要です。
デフォルトでは `$notifiable->email` を参照しますが、これを変更したい場合は以下のメソッドを実装します。

```php:app/User.php
public function routeNotificationForMail($notification)
{
    return $this->email_address;
}
```

この `routeNotificationForMail` というメソッド名は、通知方法 ( Channel ) クラスによる規約です。
他の通知方法の場合は、それぞれの規約に応じたメソッドを実装する必要があります。

* Slack: Webhook URL を返す
* SMS: 電話番号を返す

など。詳しくはドキュメントを参照ください。

通知方法 ( Channel )
========

* 通知のプロトコル
* メールや Slack, SMS などは Laravel に組み込みで実装されている
* デフォルトの通知方法を利用する場合は、ここはノータッチで OK
* カスタムチャネルを作ろうと思っている人は、一度フレームワークの実装を見てみてもいいかも

[laravel/framework/src/Illuminate/Notifications/Channels/MailChannel.php ](https://github.com/laravel/framework/blob/6.x/src/Illuminate/Notifications/Channels/MailChannel.php)

通知方法は以下の機能を持ちます。

## プロトコルに応じた通知先との通信処理 [^channel]

`$notifiable` と Notification class を受け取る `send()` メソッドを実装します。

[参考: Custom Channels](https://laravel.com/docs/6.x/notifications#custom-channels)

今回は AWS SDK を利用して AWS SNS 経由で SMS を送るカスタムチャネルを作ってみます。
Channel class のファイルの場所の規約はないので、好きなディレクトリ配下に置いて構いません。
サンプルでは `app/Notifications/Channels` というディレクトリ配下に作成しました。

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

namespace App\Notifications\Channels;

use Illuminate\Support\Facades\App;
use App\Notifications\Reserved;

class AwsSnsPublishSms
{
    /**
     * 電話番号への SMS 通知の送信
     *
     * @param  $notifiable
     * @param  \App\Notifications\Reserved $notification
     * @return void
     */
    public function send($notifiable, Reserved $notification)
    {
        // SMS 用の通知メッセージを返すメソッド toPbulishSms() の実装を Notification class の規約とした
        $message = $notification->toPublishSms($notifiable);

        $snsClient = App::make('aws')->createClient('Sns');
        $snsClient->publish([
            'Message'     => $message,
            'PhoneNumber' => $this->toE164nizeInJapan($notifiable->phone_number),
        ]);
    }

    /**
     * AWS SNS で SMS を送信するためには E.164 形式の電話番号が必要
     * 
     * @return string E.164nize phone number in JP
     */
    private function toE164nizeInJapan($phone_numeber) : string
    {
        return '+81' . substr($phone_numeber, 1);
    }
}
```

```php:app/Notifications/Reserved.php
public function toPbulishSms($notifiable)
{
    // メッセージの整形は blade テンプレートを使うとスッキリ書けます
    return view('otifications.reserved', [
            'id'   => $this->reservation->id,
            'date' => $this->reservation->date,
            'name' => $notifiable->name,
        ])->render();
}
```

```php:resources/views/notifications/reserved.blade.php
予約を受け付けました
予約番号: {{ $id }}
予約日: {{ $date }}
氏名: {{ $notifiable }}
当日のご来店をお待ちしております。
```

あとは、 Notification class の `via()` メソッドでカスタムチャネルを指定すれば完了です。

```php:app/Notifications/Reserved.php
use App\Notifications\Channels\AwsSnsPublishSms;

public function via($notifiable)
{
    return [AwsSnsPublishSms::class];
}
```

サードパーティのカスタムチャネル
========

Laravel Notification Channels というサイトにサードパーティのカスタムチャネルがのっています。

https://laravel-notification-channels.com

* APN などプラットフォームの規格
* Twitter, Facebook 等の SNS / メッセンジャーサービス
* Twillo などの SMS や VoIP サービス

などが対応しています。
利用したい通知先のサービスやプロトコルがあるなら、まずはここから探してみてもいいかもしれません。
僕が欲しかった AWS SNS は残念ながらなかったので、泣く泣くカスタムチャネルを実装しました。 [^aws-sns-channel]

まとめ
========

* Laravel の通知の実装には大きく 3 つの要素がある
* 通知メッセージ ( Notification class ) でやること
    * 通知内容
    * 通知方法の選択
* 通知方法 ( Channel class ) でやること
    * プロトコルに応じた通知の通信処理
* 通知先 ( Notifiable ) でやること
    * 通知送信メソッドの実装
        * Notifiable trait を use するだけで良い
    * 通知方法に応じた通信先情報の提供

Laravel での通知の実装方法がよくわかってなくて手が出せてなかった方も、ぜひ一度、通知処理を実装してみてください。
今回取り上げませんでしたが、 Slack 通知が incoming-webhook でとってもお手軽に試せるので、オススメです。

参考
========
* [Notifications - Laravel](https://laravel.com/docs/6.x/notifications)
* [Laravel Notification Channels](https://laravel-notification-channels.com)

[^channel]: DB Notification など、通信っぽくないものありますが、アプリケーションの外とやり取りするという意味で通信という言葉で表現しています
[^aws-sns-channel]: 昔あったみたいだけど、メンテナンスされてなくて消えたっぽい