それマグで!

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

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

Laravel でイベントとハンドラを使って通知を送る

Laravel でイベントとハンドラを使って通知を送る

イベント・リスナ

イベント定義を使って、「なにか」が起きたとき「これをやる」を定義出来る。

ジョブキューで分割すると思いがちだけど、別にシングルスレッドで実行しても構わない。

シングルスレッドでイベントとリスナを使った場合でもソースコードがスッキリするので、積極的に使っていきたい。

業務系のプログラミングの登録・削除や、セキュリティ監査プログラミングなど、複数箇所で似たようなコード(動作ログの保存など)が一箇所にまとまるので便利かもしれない。業務系のワークフローなどはイベント発生連鎖で整理して、イベントハンドラを定義したほうがスッキリすると思う。

イベントとリスナを作って使う

イベントとハンドラ(リスナ)をまとめて定義する。

php artisan event:generate
php artisan make:event SampleEvent
php artisan make:listener SampleListener

イベントを起動するコマンドも定義しておく。

php artisan make:command DispatchEventSample

作成されるファイル

イベントとリスナをペアで作ったので、次のファイルとディレクトリ(フォルダ)が作成される。

app/Events/SampleEvent.php
app/Listeners/SampleListener.php

Eventの定義

いまは、特に何もしない。クラスを作ればイベントとして使える。

<?php

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class SampleEvent {
  use Dispatchable, SerializesModels;
}
?>

リスナの定義

リスナでは、イベント発生時の動作を定義する

<?php

namespace App\Listeners;

class SampleListener {
  public function __construct () {
    //
  }  
  public function handle ( $event ) {
    dump(get_class($event).'が発生し、リスナが起動しました');
  }
}

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

イベントとハンドラの紐づけは、event:generateでServiceProviderが作成され、その中で行われる。

EventServiceProvider$lisnen変数に配列で登録してあげればいい。

リスナ紐づけは、キー・バリューで登録を書いていくわけだ。

app/Providers/EventServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use App\Events\SampleEvent;
use App\Listeners\SampleListener;

class EventServiceProvider extends ServiceProvider {
  protected $listen = [ // ここに登録する。
    SampleEvent::class => [SampleListener::class],
  ];
  public function boot () {}
  public function shouldDiscoverEvents () {
    return false;
  }
}

イベントを発生させる

イベントを発生させてみる。

メイン関数の代わりに、コンソール・コマンドを次のように作成した。

app/Console/Commands/DispatchEventSample.php

<?php

namespace App\Console\Commands;

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

class DispatchEventSample extends Command {
  protected $signature = 'my_event:sample';
  public function handle () {
    SampleEvent::dispatch();
    return 0;
  }
}

イベントを発生させる。

takuya@host $ php artisan event:sample
^ "App\Events\SampleEventが発生し、リスナが起動しました"

これで、イベントが発生し、Laravelがソースコードを飛び回ってイベントに対応したハンドラを起動してくれることがわかる。

リスナに情報を渡す。

Eventが発生したときに、詳細情報を渡す必要があるが、この点においては、詳細情報をどこにいれたり、イベント・クラスの構造には特に定義がない。自分で定義する必要がある。コンストラクタで渡すことだけが決まっている。

イベントの発生箇所

<?php
class DispatchEventSample extends Command {
  protected $signature = 'my_event:sample';
  public function handle () {
    SampleEvent::dispatch(['aaaaaa']); // 引数を渡す。
    return 0;
  }
}

イベント・クラス定義

イベント発生時に保存しておく変数をイベント・クラスに定義して保存する。

<?php

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class SampleEvent {
  use Dispatchable, SerializesModels;
    // イベント発生時の変数を保存しておく  
  protected $args;
  
  public function __construct (array $arr) {
        //コンストラクタで保存する
    $this->args = $arr;
  }
  public function getEventArg(){
    return $this->args;
  }
  
}

イベント・リスナ定義

イベントクラスから取り出す。

<?php

namespace App\Listeners;

class SampleListener {  
  public function handle (  $event ) {
    $args = $event->getEventArg();//イベント・クラスから取り出す。
    dump($args);
  }
}

注意点

LaravelではEventの構造に特に指定がないので、ハンドラ側でイベントを受け取るときに、クラス指定ができない。

そのため、IDE補完ができない。 もちろん、クラス指定してもいいのだろうが、今後Laravel本体で指定されるかもしれないので、ソースコードに引数の型を直接に指定するのはちょっと怖いと思ったり。

リスナで次のように書きたくなるが、、、Laravelが公式にEventクラスやFacadeを導入してくると困るので

  public function handle ( SampleEvent $event ) {

doc comment で@paramを使うほうが無難かもしれない。

<?php
    /** 
       @param SampleEvent
   */
  public function handle (  $event ) {

イベント発火・通知

イベント発生から通知の送信は切っても切れない関係にある。

そのために、通知を使うと思う。リスナのなかで通知してもいいし、リスナ自身を通知として登録してもいい。

リスナ・通知でクラスを分ける例

<?php

namespace App\Listeners;


use Illuminate\Support\Facades\Notification;
use App\Notifications\MyFirstNotification;

class SampleListener {
  
  public function handle ( $event ) {
        ///イベント・ハンドラの内部で通知を送る。
    Notification::route( 'mail', 'takuya@example.com' )
                ->notify( new MyFirstNotification() );
  }
}

または、通知を直接にイベントハンドラとして登録する。

<?php

class EventServiceProvider extends ServiceProvider {
  protected $listen = [];// ここでは登録せずに
  public function boot () {
      // boot内部でstatic に登録する。
    \Event::listen( SampleEvent::class, function( $event ) {
        Notification::route( 'mail', 'takuya@9b-p.com' )
                    ->notify( new MyFirstNotification() );
      },
    );
  }
    ///略
}

まとめ

イベント・ハンドラに切り分けることで、フックを簡単に作れる。Wordpressでよく見るイベント・フック系の関数をクラス定義で作れるので、知っていればどこを見ればいいかわかりやすくなる。イベント連鎖や、変化があったときの記録、何がが起きたときに何かするという、人間の行動に近いプログラミングが可能になり、ソースコードの再利用性とメンテナンス性が高まる。オブジェクト指向プログラミングの基本だったりする。

イベント・ハンドラは、Slackやメール通知など、通知機能と組み合わせて使うから役に立つ。

さらなる発展

イベント・リスナが複数のイベントを受け取るときはObserverを使うといい。

イベントの処理をキューイングしてあとに回したり、キュー処理のワーカーと、キューイングを別のインスタンスに分けて強靭化できるかもしれない。