Laravelにイベントディスカバリーが実装されたので試してみる

こんにちはみなさん

Laravelって「こうやって書けたらいいなぁ」みたいなものがなんでか書けちゃうみたいな感じになっていて、よく驚かされるわけですが、一方で、「かったるいなぁ」って思うような書き方も残っているわけです。
イベントの仕組みはその中の一つだったのですが、それがLaravel5.8.9になってから解消されたようなので、紹介していきましょう。
https://laravel.com/docs/5.8/events

# Laravelのイベント
Laravelにはイベントの仕組みがあって、特定のイベントが発火すると、それに対応したコールバックを実行する事ができます。さらに、このコールバックはQueueの仕組みと組み合わせることで、非同期で実行することができるため、重たい処理を後回しにしてレスポンスを返すこともできます。

## イベントの作成

まず、普通にイベント・リスナーのセットを作ってみましょう。公式の作り方に倣ってみましょう。

まず、`EventServiceProvider`には以下のように書きます。

```php
    protected $listen = [
        'App\Events\SomethingEvent' => [
            'App\Listeners\SomethingEventListener'
        ]
    ];
```
この設定は、`SomethingEvent`というイベントが発火すると、コールバックとして`SomethingEventListener`のハンドラが実行されることを意味しています。
とはいえ、これを書いただけでは発火するイベントもそれを受け取るイベントもありません。この設定をもとに、実際のイベントとリスナーのクラス定義を生成する必要があります。

```sh
$ php artisan event:generate
Events and listeners generated successfully!
```
すると、`app/Events/SomethingEvent.php`と`app/Listeners/SomethingEventListener.php`が生成されます。
あとは、適当にリスナーを書いていくわけです。

## 微妙なところ
こんな感じでイベントを書いていくわけですが、そうすると`EventServiceProvider`がどんどん膨らんでいきますし、いちいち`App\Events\...`みたいなフルパスのクラスをいちいち書かないといけません。

# イベントディスカバリー
イベントディスカバリーは`EventServiceProvider`を使わないで済む仕組みです。なぜか、Laravel5.8.9という、パッチバージョンで入れ込んできましたが、これを使うとリスナー側の`handler`に指定されているイベントの型宣言を読んで、その型のイベントのリスナーに自動で登録されるという仕組みです。

## イベントディスカバリーを試す
ではイベントディスカバリーを試してみましょう。まず、イベントディスカバリーを有効にする必要があります。

```app/Providers/EventServiceProvider.php
//...中略
    public function shouldDiscoverEvents()
    {
        return true;
    }
```
これを`EventServiceProvider`にて設定することで、イベントディスカバリーが有効になります。

### イベントとリスナーを作る

イベントを作りましょう。これは、コマンドを作って作れます。

```
php artisan make:event DiscoveryEvent
```
次に、リスナーを作ります。これもコマンドを使うのですが、監視するイベントを設定する必要があります。

```
php artisan make:listener DiscoveryEventListener --event DiscoveryEvent
```
あとで動作確認したいので、リスナーにコードを足しましょう。

```app/Listeners/DiscoveryEventListener.php
//...中略
    /**
     * Handle the event.
     *
     * @param  DiscoveryEvent  $event
     * @return void
     */
    public function handle(DiscoveryEvent $event)
    {
        \Log::debug('聞き届けました！')
    }
```
ここで、handleの引数に型宣言がしてありますが、これがあることで、イベントディスカバリーにより、自動でイベントのリスナーに登録されます。

### 動作を確認する

簡単に動作確認しましょう。
まず、適当なアクションにおいてイベントを発火させてみましょう。

```routes/web.php
<?php
use App\Events\DiscoveryEvent;

Route::get('/', function () {
    DiscoveryEvent::dispatch();
    return view('welcome');
});
```
トップページアクセスでイベントが発火するようになりました。
こいつを動かすのですが、ブラウザ使わなくとも、テストで簡単に確認できます。
幸い、Laravelのプロジェクトを作ると、`tests`というディレクトリができていて、そこにはすでにサンプルのテストがあります。

```tests/Feature/ExampleTest.php
    public function testBasicTest()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
```
なので、その場でテスト流すだけでイベントの発火を確認できます。

```sh
./vendor/bin/phpunit
```
テストを実行すると、ログに先程のリスナーで設定したものが記載されています。

```log
[2019-07-17 13:02:32] local.DEBUG: 聞き届けました！ 
```
無事、動いたようです。

### リストの表示
`EventServiceProvider`による集中管理をやめたため、どのイベントがどのリスナーに監視されているのかわからなくなりそうですが、ちゃんとこれをリスト化して提供する仕組みがあるので問題ありません。

```shell-session
$ php artisan event:list
+-----------------------------------+-------------------------------------------------------------+
| Event                             | Listeners                                                   |
+-----------------------------------+-------------------------------------------------------------+
| App\Events\DiscoveryEvent         | App\Listeners\DiscoveryEventListener@handle                 |
| App\Events\SomethingEvent         | App\Listeners\SomethingEventListener@handle                  |
|                                   | App\Listeners\SomethingEventListener                         |
| Illuminate\Auth\Events\Registered | Illuminate\Auth\Listeners\SendEmailVerificationNotification |
+-----------------------------------+-------------------------------------------------------------+
```
ここで`SomethingEvent`にリスナーが二重に登録されてしまっていますが、これは`EventServiceProvider`の`$listen`にリスナーが登録されたままになっており、`EventServiceProvider`の設定とイベントディスカバリーの動作がかぶったためでしょう。

```app/Providers/EventServiceProvider.php
    protected $listen = [];
```
こんな感じでリスナーから取り除いてしまえば

```
php artisan event:list
+---------------------------+---------------------------------------------+
| Event                     | Listeners                                   |
+---------------------------+---------------------------------------------+
| App\Events\DiscoveryEvent | App\Listeners\DiscoveryEventListener@handle |
| App\Events\SomethingEvent | App\Listeners\SomethingEventListener@handle  |
+---------------------------+---------------------------------------------+
```
こんな感じになります。

### キャッシュする
アクセス頻度が高いサイトで、いちいちイベントディスカバリーの操作が走るのは精神衛生的によろしくないので、本番環境などではキャッシュさせちゃいましょう。

```shell-session
$ php artisan event:cache
Cached events cleared!
Events cached successfully!
```
このコマンドでキャッシュを作ると、イベントディスカバリーの処理は走らなくなりますので、新しくイベントを追加しても、イベントの追加はなされません。
本番以外ではこのコマンドは使わないようにするか、`php artisan event:clear`コマンドでキャッシュを消すのが良いでしょう。

## 小ネタ

上のリストを見るとわかりますが、イベントに対するリスナーについては、ハンドラーメソッドまで指定されています。ハンドラーメソッドはその名称が`handle`で始まっていれば良いらしく、

```app/Listeners/DiscoveryEventListener.php
    public function handle2(DiscoveryEvent $event)
    {
        \Log::debug('ここにも届きました!');
    }
```
こんなものを書いておくと、リストにも追加され、実行時のログも出ます。面白いっちゃ面白いんですが、使いすぎるとリスナーが変に太りまくる可能性があったり、リスナーの役割が不明瞭になるので、あまり乱用しないほうがいいでしょう。

## イベントディスカバリーの欠点
欠点と言っていいのかどうかわかりませんが、複数のイベントに共通のリスナーを登録するということはできません。例えば、こんなやつです。

```app/Providers/EventServiceProvider.php
    protected $listen = [
        SomethingEvent::class => [
            AllListener::class
        ],
        DiscoveryEvent::class => [
            AllListener::class
        ]
    ];
```
`SomethingEvent`および`DiscoveryEvent`のどちらについても発火すると`AllListener`の`handler`が実行されます。

一応、先の小ネタで書いたように、ハンドラーメソッドを変えれば、リスナークラスを複数のイベントに関連付けることはできますが、ハンドラーメソッド自体を共通化することは、イベントディスカバリーでは無理っぽいです。

# まとめ
そういうわけで、マニュアルを読んでいたら目に入ったので、イベントディスカバリーとやらを調べてみました。
リスナーが監視するイベントが一つだけであるのならば、かなり有用ではないかと思います。

ただ、パッチバージョンで導入するのってどうなのかしらんって思った感じです。

今回はこんなところです。