LINE notifications channel for Laravel を作ってみた

# 経緯

[LINEのプッシュ通知](https://developers.line.biz/ja/reference/messaging-api/#send-push-message)をLaravelで実装しようと思って調べてみたら、[コミュニティが提供している通知チャンネル](https://laravel-notification-channels.com/about/#suggesting-a-new-channel)の中にLINE通知がなかったため、自分で作ってみました。
（LINEの国外ユーザ数を考えれば、提供されていないのは当然かもしれませんが、、）

# 環境

* PHP 8.0.9
* Laravel Framework 8.51.0

# 前提

* [LINE Messaging API](https://developers.line.biz/ja/docs/messaging-api/getting-started/)でチャネルを作成していて、`Channel access token`と`Channel secret`を持っている
* LINEの`userId`（友達検索などで使うLINE IDではなく、LINEが管理しているユーザ固有のID）がDBに登録されている

# Laravel で LINE Messaging API を簡単に使う

`curl`でそれぞれのエンドポイントを叩くコードを自分でイチから実装しても良かったのですが、めんどくさかったので[LINE Messaging API SDK for PHP](https://github.com/line/line-bot-sdk-php)を使うことにしました。

下記コマンドでSDKをインストールします。

```bash
$ composer require linecorp/line-bot-sdk
```

`.env`ファイルに以下の環境変数を追加します。

```bash
LINE_BOT_CHANNEL_ACCESS_TOKEN=<前提のChannel Access Token>
LINE_BOT_CHANNEL_SECRET=<前提のChannel Secret>
```

この環境変数は、`vendor\linecorp\line-bot-sdk\src\Laravel\config\line-bot.php`で読み込まれて、`LINEBotServiceProvider`によって`LINEBot`ファサードが登録されます。

これで、Laravel App のどこからでも`LINEBot`ファサードを呼べるようになり、LINE Messaging APIを利用することができます。

例：[LINEのプロフィール情報を取得する](https://developers.line.biz/ja/reference/messaging-api/#get-profile)

```php
$profile = \LINEBot::getProfile($user_id);
```

# LINE通知チャンネルの作成

下記を参考に実装しました。

* [Custom Channels](https://laravel.com/docs/8.x/notifications#custom-channels)
* [Amazon Simple Notification Service (AWS SNS) notification channel for Laravel](https://github.com/laravel-notification-channels/aws-sns)

```bash:ディレクトリ構成
app/
├── Channels/
│   └── LINEBot/
│       ├── LineChannel.php
│       └── Exceptions/
│           └── CouldNotSendNotification.php
```

```php:app\Channels\LINEBot\LineChannel.php
<?php

namespace App\Channels\LINEBot;

use App\Channels\LINEBot\Exceptions\CouldNotSendNotification;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Notifications\Events\NotificationFailed;
use Illuminate\Notifications\Notification;
use LINE\LINEBot\MessageBuilder\TextMessageBuilder;

class LineChannel
{
    /**
     * @var Dispatcher
     */
    protected $events;

    public function __construct(Dispatcher $events)
    {
        $this->events = $events;
    }

    /**
     * Send the given notification.
     *
     * @return \LINE\LINEBot\Response
     */
    public function send($notifiable, Notification $notification)
    {
        try {
            $destination = $this->getDestination($notifiable, $notification);
            $message = $this->getMessage($notifiable, $notification);

            $response = \LINEBot::pushMessage($destination, $message);
            if (!$response->isSucceeded()) {
                throw CouldNotSendNotification::failedToPushMessage($response);
            }
            return $response;
        } catch (\Exception $e) {
            \Log::error($e);

            $event = new NotificationFailed(
                $notifiable,
                $notification,
                'line',
                ['message' => $e->getMessage(), 'exception' => $e]
            );
            $this->events->dispatch($event);
        }
    }

    /**
     * Get the LINE userId to send a notification to.
     *
     * @throws CouldNotSendNotification
     */
    protected function getDestination($notifiable, Notification $notification)
    {
        if ($to = $notifiable->routeNotificationFor('line', $notification)) {
            return $to;
        }

        return $this->guessDestination($notifiable);
    }

    /**
     * Try to get the LINE userId from some commonly used attributes for that.
     *
     * @throws CouldNotSendNotification
     */
    protected function guessDestination($notifiable)
    {
        $commonAttributes = ['user_id', 'line_id', 'line_user_id'];
        foreach ($commonAttributes as $attribute) {
            if (isset($notifiable->{$attribute})) {
                return $notifiable->{$attribute};
            }
        }

        throw CouldNotSendNotification::invalidReceiver();
    }

    /**
     * Get the LINEBot TextMessageBuilder object.
     *
     * @throws CouldNotSendNotification
     */
    protected function getMessage($notifiable, Notification $notification): TextMessageBuilder
    {
        $message = $notification->toLine($notifiable);
        if (is_string($message)) {
            return new TextMessageBuilder($message);
        }

        if ($message instanceof TextMessageBuilder) {
            return $message;
        }

        throw CouldNotSendNotification::invalidMessageObject($message);
    }
}
```

```php:app\Channels\LINEBot\Exceptions\CouldNotSendNotification.php
<?php

namespace App\Channels\LINEBot\Exceptions;

use LINE\LINEBot\MessageBuilder\TextMessageBuilder;
use LINE\LINEBot\Response;

class CouldNotSendNotification extends \Exception
{
    public static function invalidReceiver()
    {
        return new static(
            'The notifiable did not have a receiving LINE userId. Add a routeNotificationForLine
            method or one of the conventional attributes to your notifiable.'
        );
    }

    public static function invalidMessageObject($message)
    {
        $type = is_object($message) ? get_class($message) : gettype($message);

        return new static(
            'Notification was not sent. The message should be a instance of `'.TextMessageBuilder::class."` and a `{$type}` was given."
        );
    }

    public static function failedToPushMessage(Response $response)
    {
        return new static(
            "Failed to send LINE messages\n".
            "httpStatus: {$response->getHTTPStatus()}\n".
            "message: {$response->getJSONDecodedBody()['message']}"
        );
    }
}
```

# LINE通知チャンネルの使い方

他の通知チャンネルと同じように使うことができます。
通知クラスの`via()`メソッドで`LineChannel::class`を指定して、`toLine()`メソッドを実装します。

```php
<?php

use App\Channels\LINEBot\LineChannel;
use Illuminate\Notifications\Notification;

class SomeNotification extends Notification
{
    public function via($notifiable)
    {
        return [LineChannel::class];
    }

    public function toLine($notifiable)
    {
        // 通知内容が書かれたViewファイルの文字列を取得（もちろん、文字列を直接記述してもよい）
        $message = view('notifications.some_notification')->render();

        // 文字列をそのまま返す方法:
        return $message;

        // LINEBotのTextMessageBuilderを返す方法:
        return new \LINE\LINEBot\MessageBuilder\TextMessageBuilder($message);
    }
}
```

次に、LINE通知先のuserIdが格納されたテーブルのモデルに、`Notifiable`トレイトを追加します。
デフォルトで、テーブルカラム名が`user_id`, `line_id`, `line_user_id`のいずれかであるデータを通知先として採用します。
通知先をオーバライドするためには、`routeNotificationForLine()`メソッドを実装します。

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;

class SomeModel extends Model
{
    use Notifiable;

    public function routeNotificationForLine($notification)
    {
        return 'xxxxxxxxxxxx';
    }
}
```

これで、モデルの`notify()`メソッドを呼ぶと、実際にLINE通知を送信することができるようになります。

```php
SomeModel::find($input_id)->notify(new SomeNotification());
```