それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

laravel でevent(イベント・リスナ) を扱う

laravel でevent を扱う

laravel にはイベントとリスナを組み合わがある、重複コード解消に役立つ。データが更新されたタイミングで行う処理を、リストに記入し、共通化する。更新タイミングをイベントとして発火させるように設計する。これを使うとドメイン駆動に近づき、似たような処理が担当者ごとに何度も書かれる事態を防ぐことができる。

イベント・リスナをコンソールから実行する。

今回は、artisanコマンドを作成し、イベントを発火し、リスナで受け取る処理を書いてみる。

イベント発火コマンド

php artisan app:sample-run

今回は、コマンドを使って見通しを良くすることを考える。

  • イベントの作成
  • リスナの作成
  • イベントとリスナの関連付け
  • イベントの発火

これらをを見ていく

世にあふれるLaravelの解説記事はWEB-UIを使っていて煩雑である。あんなに言葉を使わなくてもいいはずだ。

プロジェクトの作成

最初に、実験用プロジェクトの初期設定

dir=event-listener-sample  
composer create-project laravel/laravel $dir
cd $dir
sed -r 's/^(VITE|PUSHER|AWS|MAIL|REDIS|DB|MEMCACHE|QUEUE)/#\1/' -i .env  
sed -e '/DB_CONNECTION/i QUEUE_CONNECTION=database' -i .env
sed -e '/DB_CONNECTION/i DB_CONNECTION=sqlite'     -i .env
touch database/database.sqlite
sed '/indent_size/d' -i .editorconfig # インデントはエディタ設定を優先する。

イベントとリスナを作る

コマンドからEvent / Listener を作成

php artisan make:event MyEvent
php artisan make:listener MyEventListener

イベントとリスナを紐付ける。

app/Providers/EventServiceProvider.php

<?php
namespace App\Providers;

use App\Listeners\MyEventListener;
use App\Events\MyEvent;

class EventServiceProvider extends ServiceProvider{
  protected $listen = [
    MyEvent::class => [MyEventListener::class]
  ];
}

コマンド(イベント発火)を作成

イベントを発火するコマンドを作る

php artisan make:command SampleCommand        

app/Console/Commands/SampleCommand.php

イベントを発火する箇所( event() ) を作る

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Events\MyEvent;

class SampleCommand extends Command {
  protected $signature = 'app:sample';
  public function handle () {
    event(new MyEvent('Hello world'));//イベント発火
  }
}

イベントの中身を書く

とりあえず、Stringを受け取るように書く。

app/Events/MyEvent.php

<?php
namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;

class MyEvent {
  use Dispatchable, InteractsWithSockets, SerializesModels;
  public function __construct ( public string $msg ) { }

}

イベントを受け取るリスナの中身を書く

イベントを受ける箇所は、引数1つを受け取る。

app/Listeners/MyEventListener.php

<?php
namespace App\Listeners;

class MyEventListener {
  public function handle ( object $event ): void {
    dd($event);
  }
}

使ってみる

準備ができたので、使ってみよう

イベントを発火ささせる

php artisan app:sample-run

実行結果

App\Events\MyEvent^ {#625
  +msg: "Hello world"
  +socket: null
} // app/Listeners/MyEventListener.php:9

よくある失敗

イベントが発火しない(起きない)場合。

イベントを紐付ける箇所は、イベントとリスナを書き間違えるミスをよく起こす。変数名が$listenだから、リスナを先に書いてしまうミスで、イベントが通知されてないとかよくミスする。

紐づけの書き方

<?php
  $listen = [
    'イベント'=> [
      'リスナ',
      'リスナ',
      ...]
  ];

よくあるミス

<?php
## イベントとリスナのKey-Valueが逆
$listen = ['リスナ'=> ['イベント',]];
## 配列のネストがおかしい
$listen = [['イベント'=> ['リスナ',]]];
## クラスの指定ミス (namespace 漏れ)
$listen = [['MyEvent'=> ['App\Listeners\MyEventListener',]]];

イベントが増えてくると、ネストでミスることが多い。

より良い書き方

<?php
use App\Events\MyEvent;
use App\Listeners\MyEventListener;
//略
$listen = [[MyEvent::class=> [MyEventListener::class,]]];

クラスの指定ミスを減らすために、文字列でなく::classを使うべき。

イベントとリスナを厳密に書く

リスナが受け取るイベント・クラスを限定したい

リスナのhandle(object $event) でイベントが渡されるだけである。変数を渡したいときはEventオブジェクトに入れる。

(*1 個人的には、ここはLaravelだからリスナの引数にクラス指定で、非宣言で自動関連付けしてほしいところではある。)

リスナが受け取るイベントを限定したいときは、クラスを指定指定引数を書く

-public function handle ( object $event ): void 
+public function handle ( MyEvent $event ): void 

まとめ

覚えること

作成方法。

php artisan make:event MyEvent
php artisan make:listener MyEventListener

紐づけ。(サービスプロバイダー)

# app/Providers/EventServiceProvider.php
<?php 
class EventServiceProvider extends ServiceProvider{
    $listen = [MyEvent::class=> [MyLisnterner::class,]];
}

イベント・リスナを書くメリット(ポエム)

ここはポエムです。

冒頭にも書いたが、イベント+ハンドラで書くことで重複するコードを排除できる。

共通処理を関数(イベントハンドラ)に切り出すことができる。

非同期キューと組み合わせると良いかもと思うかもしれないが、大事なことは整理。重複コードをリスナ側に移動する事ができる。またリスナ側で特定のクラスを呼び出すだけに書いておけば、コードの整理を徹底的に行える。

たとえば、API側とWEB側で共通処理とか、重複コードをリスナ側に押し出してしまえる。

そして、整理を行っておけば、将来的にジョブキューにして処理を非同期化を試みるときに、とても簡単になる。

ドメイン駆動設計を考えたとき、「実装の処理」と「設計の処理」を「実データの更新」で考えないといけないとも思うのですね。「ユーザが作られたら〇〇する」のような処理は、「ユーザが作られたイベントを発火する・ユーザー作成時の処理をリスナで受け取る・リスナで実際の処理プロバイダを呼び出す」ようにわけて書けば、依存箇所をDB(モデル)から分離する。ロジック(設計)、トリック(実装)、マジック(設定)に分けられる。これでパニックを防ぎ、ブラックならないようにワークできる。

僕的にわかりやすくいえば、Eloquent\Model::save()に処理を書かずにイベントとリスナで依存を外し、save()をぶった斬るイメージですね。じゃあ、リスナが肥大化しそうじゃん。そのまま書くとリスナが肥大化しちゃう。よく考えると、リスナに実装を書くのかというと、そうでもない。リスナは他サービスやレポジトリの機能を呼び出すだけが実際に書くコードになる思うんですよね。リスナを使っておくと結果としてDDD(ドメイン駆動設計)に近くなるよね。ドメイン駆動の図(下参照)のオニオンの境界を飛び超えるときにイベントを使うとスッキリするように感じる。

イベントは境界を超えるときなので、「〇〇をするときに△△する」いうのは、「ユーザが作られたらメールを送る」の例ではlaravelのイベント+リスナを説明にするには不十分だろう。QiitaやZennを見てて感じる違和感はここにある。

 よく見かけてるのがユーザがログインしたら「ログインイベント」に紐づけた処理を起動する。そして「ログインに関するチェックをする、問題がアレば、通知を送る」。それも「ログイン・チェック問題あり」の別イベントで書いておき「発火させる」わけです。これら処理はログインのControllerへは戻り(値)が不要だから、結合して書く必要がない。そこがポイントじゃないかな。

 だから、全く別の名前空間に処理を書いて組み合わせる。とすれば便利。これなら単体テストも行いやすいよね。

 昭和・平成時代のプログラミング教科書だと、言葉で書いてフローチャート的に書き起こし、それをプログラミング・ソースコードに起こすって習う。ミレニアム時代はオブジェクト指向での相互関係を使うように教わる。現代ではできるだけマイクロに依存を切り離す、このときイベントを使うほうがスッキリすよね、以前の教科書で習うフローチャート・プログラミングだとエクセルでマクロを書くようなものだから、処理を追いかけられるけどひどくメンテしづらい。イベントを使えば、そうじゃないんだよね。

なので、Laravelのイベントを簡潔に言うなら、「laravelイベントは、DDDにおけるドメイン・イベント」である。これが適切だろう。そしてオブジェクト指向プログラミングでは「laravel イベントはObserverパターン」で実装されている。といえる。