【Laravel】イベントとリスナー　~イベントを使う場合/使わない場合を比べてみる~

#はじめに

###イベント・リスナーとは
Webアプリケーションにおけるイベントとは、ユーザー登録やログイン、ログアウトなど、様々なアクションを指します。
そして、これら何らかのアクションの発生を感知し、そのタイミングに付随してあらかじめ決められている処理を行うことができるのがリスナーです。


###この記事でやりたいこと
この記事では`ユーザー登録時に登録完了メールを送信する機能`を実装します。

この処理は、イベントを使用しなくても実装することはできますが、イベントを利用することで、

* コントローラーをスリムにできる
* 処理を分けることでメンテナンスが楽になる
* 新たに処理を追加する際も、コントローラーや他のファルへの影響を最小限に抑えられる。

といったメリットがあります。

イベントとリスナーの仕組みやメリットを理解するため、実際に`イベントを使わない場合/イベントを使う場合それぞれの処理を実装`して比べてみます。


#イベントを使う場合/使わない場合の共通手順
まずはイベントの使用有無に関わらず必要となる、環境や設定を整えていきます。

##1. 環境構築（Laravelのプロジェクト作成、ログイン機能を実装）
まずはcomposerコマンドで、Laravelのプロジェクトを作成します。

```
$ composer create-project --prefer-dist laravel/laravel event_listener
```

データベースを作成します。

```
$ mysql -u root -p      //パスワードを求められるので、入力する
$ create database event_listener;
```

データベースへの接続を設定します。

```env:.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=event_listener    //上記で作成したデータベース名
DB_USERNAME=root
DB_PASSWORD=******
```

今回はlaravel/uiを使ってログイン機能を実装します。

```
//laravel/uiパッケージをインストール
$ composer require laravel/ui

//認証機能に必要なファイルを生成
$ php artisan ui vue --auth

//migrate実行
$ php artisan migrate
```

テーブルが作成されました。
![スクリーンショット 2021-09-22 21.19.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208065/86fca3f2-f13d-14a0-dc8d-31703ac4deab.png)

フロント関連のパッケージをインストールし、CSSやJavascriptの装飾を付けます。

```
$ npm install
$ npm run dev
```

以上でログイン機能が実装できました。

ローカルサーバーを立ち上げ、`localhost:8000/register`にアクセスしてみると、以下の画面が表示できています。
![スクリーンショット 2021-09-22 21.57.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208065/e1e288db-79fd-29a5-3746-8589ce310f62.png)


##2. Mailableクラスの作成
ユーザー登録が行われた際に、登録完了メールを送信する機能を実装します。
今回はイベントとリスナーに焦点を当てているので、Mailableについての説明は割愛します。（詳細は下記の記事にて。）

https://qiita.com/hinako_n/items/ff451ec558abefc41247


まずはMailableクラスを作成します。

```
$ php artisan make:mail RegisterMail
```
これで`app/Mailディレクトリ`と`RegisterMail.php`ファイルが作成されました。

続いて下記のように編集します。

```RegisterMail.php
class RegisterMail extends Mailable
{
    use Queueable, SerializesModels;
    private $user;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($user)
    {
        $this->user = $user;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('emails.register')
            ->subject('登録完了しました')
            ->from('admin@hoge.co.jp', 'event事務局')
            ->with('user', $this->user);
    }
}

```
RegisterMail.phpにて、上記の通り`return $this->view('emails.register')`と記述したので、`resources/views/emails/register.blade.php`にメール文面のテンプレートを作成します。


```php:register.blade.php
<p>〇〇サービスをご利用いただきありがとうございます。</p>
<p>下記内容にてユーザー登録が完了しました。</p>

<p>Name：{{ $user['name'] }}様</p>
<p>E-Mail Address：{{ $user['email'] }}</p>
```

今回はMailtrapを使用します。Mailtrapで取得したユーザーネームとパスワードを.envに記述します。

https://mailtrap.io/


```env:.env
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=********    //Mailtrapより
MAIL_PASSWORD=********    //Mailtrapより
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
```


次はユーザー登録が行われた際に、メールを送信する処理を記述します。
まずはイベントを使わない場合をやってみます。

#イベントを使わない場合
今回やりたいことは、ユーザー登録された時に登録完了メールを送信する機能の実装です。
よってシンプルに、このRegisterボタンが押されたときに呼び出されるコントローラーのメソッドに、メール送信の処理を記述したいと思います。
![スクリーンショット 2021-09-26 10.24.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208065/86b10988-83a2-a8db-ac15-901def70cced.png)

詳細は割愛しますが、laravel/uiのルートを読み解くと、ユーザー登録処理が行われるまでの流れはざっくり以下のようになります。

* Registerボタンを押すとlocalhost:8000/registerにPOSTメソッドでアクセスされる。
* RegisterControllerのregisterメソッド（実際はトレイトのRegistersUsers.phpの中のregisterメソッド）が実行され、その中のRegister
Controller.phpのcreateメソッドでユーザー登録の処理が実行される。


ですので今回は、この`App/Http/Controllers/Auth/RegisterController.phpのcreateメソッド`内に、メール送信処理を追記します。

```RegisterController.php
//略
use Mail;
use App\Mail\RegisterMail;


//元のコード
protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }


//変更後のコード
protected function create(array $data)
    {
        $user = User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);

        Mail::to($user->email)->send(new WelcomeMail($user));

        return $user;
    }
```

###動作確認

これで`localhost:8000/register`にアクセスし、Registerボタンを押して実際にユーザー登録を行ってみます。![スクリーンショット 2021-09-23 5.26.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208065/ae188de9-6a46-c5fb-2ace-faf7f7d833fa.png)

すると、Mailtrapにもきちんとメールが届きました。
![スクリーンショット 2021-09-23 5.27.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208065/a6a01d34-08a7-86fe-c4f7-dc58b8f8110b.png)

これでイベントを使わない場合のメール送信機能を実装することができました。
次はイベントとリスナーを使って、同じ処理を書いてみます。
編集したコントローラーは、一旦デフォルトの状態に戻しておきます。

#イベントを使う場合
##1. イベントとリスナーの作成
###イベントとリスナーの仕組み
今回のメール送信処理を書く前に、イベントとリスナーの概要をまとめます。

イベントを利用するには、

**① イベントクラスととリスナークラスの作成
② EventServiceProvider（イベントを管理する専用のサービスプロバイダ）への登録**

の2つが必要になります。（EventServiceProvideでイベントとリスナーの関連づけを行わなければ、イベントが発生してもリスナーは検知することができません。）

イベントを発行すれば、そのイベントを受け取るイベントリスナーの処理が実行される、という仕組みです。
それぞれの方法を見ていきます。

###①イベントクラスとリスナークラスの作成方法
イベントとリスナーは、それぞれコマンドで作成することができます。

```
//イベントの作成
$ php artisan make:event イベント名

//リスナーの作成
php artisan make:listener SendWelcomeEmail --event=イベント名
//--event以下は任意
```
リスナー作成時に`--event=イベント名`のオプションを付けると、リスナーファイルにイベントの情報が入力された状態でファイルが作成されます。

###②EventServiceProvider（イベントを管理する専用のサービスプロバイダ）への登録方法
EventServiceProviderは`App/Providers`の中に用意されています。
EventServiceProviderはServiceProviderを継承しており、一般的なサービスプロバイダと同様にbootメソッドを持ちます。
ただしEventServiceProvider特有の要素もあり、それがイベントとリスナーの関連づけの設定をまとめる`$listen`というプロパティです。

$listenには、「イベントクラス => イベントリスナークラス」という連想配列の形で記述します。

```EventServiceProvider.php
protected $listen = [
        'App\Events\イベント名' => [
            'App\Listeners\リスナー名',
            //複数のリスナーを登録することも可能
        ],
    ];
```
イベントに関連付けるリスナーは、配列でいくつでも設定することができます。


###メール送信処理を担う、イベントとリスナーを作成する
では実際に、今回のメール送信処理を担うイベントとリスナーを作成していきます。
ユーザー登録された時（イベント）、メール送信する（リスナー）ので、以下のようにクラス名をつけることにします。
イベント：UserRegistered
リスナー：SendMail

先述の通り、イベントとリスナーのファイルをそれぞれ作成することももちろん可能ですが、**EventServiceProviderに事前にイベントとリスナーの情報を記述しておけば、コマンドで一括でイベントとリスナーのファイルを作成することができる**ので、今回はこの方法をとります。

まずはEventServiceProvider.phpの$listenプロパティに、これから作成したいイベントとリスナーを登録します。

```EventServiceProvider.php
protected $listen = [
        'App\Events\UserRegistered' => [
            'App\Listeners\SendMail',
        ],
    ];
```

下記のコマンドを実行します。

```
$ php artisan event:generate
```
これで、$listenに登録した情報をもとに、

* app/Eventsディレクトリと、その下にUserRegistered.php
* app/Listenersディレクトリと、その下にSendMail.php

が自動生成されます。

##2. イベントファイルの編集

作成されたイベントファイルを編集します。

コンストラクタにてevent関数（後述）によるイベント発行時の引数を受け取ります。
イベントクラスの役割は、そのイベントに関する情報を保持して渡すことです。
ここではユーザー登録したユーザーにメールを送信したいので、該当のユーザーの情報を保持します。

```UserRegistered.php
<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\User;      //追記

class UserRegistered
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    private $user;    //追記

    public function __construct($user)
    {
        $this->user = $user;    //追記
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

```

##3. リスナーファイルの編集

続いてリスナーファイルです。コンストラクタとhandleというメソッドが用意されています。
このhandleメソッドが、イベントの発生により呼び出される処理を記述するメソッドです。

今回のケースでは、メール送信処理をここに記述します。

```SendMail.php
<?php

namespace App\Listeners;

use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Mail;      //追記
use App\Mail\RegisterMail;      //追記


class SendMail
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  UserRegistered  $event
     * @return void
     */
    public function handle(UserRegistered $event)
    {
        Mail::to($event->user->email)->send(new RegisterMail($event->user));      //追記
    }
}

```

##4. イベントを発行する

これでイベントとリスナーが用意できたので、最後にイベントの発行を記述します。

イベントの発行には`event関数`を使います。
下記のように、event関数の引数にイベントクラスのインスタンスを渡すことで、そのイベントが発行されます。

```:php
event(new イベント名（イベントクラスへ渡す値）);
```

では今回は、`app/Http/Controllers/Auth/RegisterController.php`にて、ユーザー登録されたタイミングでイベントを発行します。

```RegisterController.php
//略
use App\Events\UserRegistered;
 
//略
protected function create(array $data)
{
    $user = User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);

    event(new UserRegistered($user));

    return $user;

}
```

以上で、ユーザー登録が行われた際にメールが送信されるようになります。
イベントを利用しても利用しなくても、同じ処理を行うことができるとわかりました。

ただここまでだと、メール送信処理のコードは短いため、イベントの利点どころかむしろコントローラーに直接書いた方が簡単に思えるかもしれません。

しかし例えば、後からここにSlackで管理者に通知を送る機能も加えたいということになった場合はどうでしょうか。
イベントとリスナーを使用していれば、機能追加に必要なのは、

* リスナーを新規作成する
* 既にあるイベントに関連付けるため、EventServiceProvideに登録する
 
のみで、コントローラーや他のイベント・リスナーには何ら影響を与えずに済みます。

このように処理が多くなればなるほど、イベントとリスナーを使って処理を分けておくメリットは大きくなります。

では実際に、管理者へSlack通知を送信する機能を追加してみます。

#リスナーを使って機能を追加する
##1. 事前準備
Slackでのメッセージ送信には、LaravelのNotificationを利用します。

Slackで事前にWebhook URLを取得し、.envファイルに設定を済ませているものとします。
また今回はイベントとリスナーに焦点を当てているので、Notificationについて詳細の説明は割愛します。（Webhook URLの取得・設定や、Notificationについては下記の記事にて。）

https://qiita.com/hinako_n/items/71710583b5b338defab5

##2. slack-notification-channelパッケージのインストール
LaravelでSlack Notificationを利用するためには、slack-notification-channelパッケージをインストールする必要があります。

```
$ composer require laravel/slack-notification-channel
``` 

##3. Notificationクラスの作成
Notificationクラスを作成します。
このクラスの役割は以下の２つです。

* メッセージ内容の生成
* 通知方法の指定

Notificationクラスはコマンドで作成できます。

```
$ php artisan make:notification RegisterSlack   
```
実行すると`app/Notifications`の下に`SendSlack.php`ファイルが作成されます。
このファイルを下記のように編集します。



```RegisterSlack.php
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\SlackMessage;
use App\Models\User;      //追記

class RegisterSlack extends Notification
{
    use Queueable;
    public $user;      //追記

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;      //追記
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['slack'];      //slackに編集
    }

    public function toSlack($notifiable)
    {
        return (new SlackMessage)
            ->from('event_listener')    //Slackへメッセージを送る際の送信者名
            ->content("[". $this->user->name ."]さんが新規登録しました。");
               //新規登録したユーザーの名前を受け取り、メッセージに表示する
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    // public function toMail($notifiable)
    // {
    //     return (new MailMessage)
    //                 ->line('The introduction to the notification.')
    //                 ->action('Notification Action', url('/'))
    //                 ->line('Thank you for using our application!');
    // }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}
```

##4. 通知送信処理を記述する（リスナーの追加）
記述したいのは以下の2点です。

* 通知送信処理の実行
* 通知送信先の指定

これらは、Notifiableトレイトをuseしたクラスであればどこでも書くことができます。

ここでは新たにリスナーを作り、リスナーにNotifiableをuseして、これらの処理を記述します。

```
$ php artisan make:listener SendSlack --event=UserRegistered
```

これで現在、`app/Listeners`の下には`SendMail.php`と`SendSlack.php`の2つのファイルができている状態です。

作成したリスナークラスを編集します。

```SendSlack.php
<?php

namespace App\Listeners;

use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Notifications\RegisterSlack;
use Illuminate\Notifications\Notifiable;      //追記

class SendSlack
{
    use Notifiable;      //追記

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  UserRegistered  $event
     * @return void
     */
    public function handle(UserRegistered $event)
    {
        $user = $event->user;    //Notificationクラスに渡す値
        $this->notify(new RegisterSlack($user));
    }

    public function routeNotificationForSlack($notification)
    {
        return 'https://hooks.slack.com/services/・・・・・;
    }
}
```

##5. リスナーをEventServiceProviderへ登録する
イベントとリスナーの関連付けのため、新たに作成したSlack用のリスナーをEventServiceProviderへ登録します。

```EventServieProvider.php
protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        'App\Events\UserRegistered' => [
            'App\Listeners\SendMail',
            'App\Listeners\SendSlack'
        ],
    ];
```

#まとめ

以上で、ユーザー登録を行った際に、

* ユーザーへ登録完了メールを送信する
* 管理者へSlack通知を送信する

という処理を行うことができるようになりました。

このように、イベントを利用すれば、コントローラーや他の処理のファイルへ手を加えることなく、処理の追加を行うことができます。
処理が不要になればリスナーを削除すれば良いですし、処理の変更を行う際もそのリスナーを編集するのみです。
また一つひとつの処理が独立していることで、処理の使い回しも可能になります。

処理が少ないうちは、コントローラーや特定のファイルに一連の処理を全て書いた方がコードも短く、楽に感じるかもしれません。
ですが、他のメンバーとの共同開発や長期的に運用していくことを考えると、可読性やメンテナンス性を高めるため「単一責任の原則」に従うことは必須なのだなと思いました。


<br>
##参考記事

https://readouble.com/laravel/8.x/ja/events.html

https://readouble.com/laravel/8.x/ja/notifications.html

https://reffect.co.jp/laravel/laravel-event-listener#event

https://noumenon-th.net/programming/2020/06/08/events-listeners/

##参考書籍

https://www.amazon.co.jp/PHP%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF-Laravel%E5%AE%9F%E8%B7%B5%E9%96%8B%E7%99%BA-%E6%8E%8C%E7%94%B0-%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798059072