tanaka's Programming Memo

プログラミングについてのメモ。

Laravel(5.3) イベントを試す

Events - Laravel - The PHP Framework For Web Artisans

Laravelの公式ドキュメントのイベントの項目を参考に、動かしてみました。

概要

Laravelのイベントシステムを使うと、様々なイベントを簡単に処理できます。アプリで発生するイベントに対する処理をリスナーとして登録しておくと、イベントを発生させることで、システムが登録されたリスナーを呼び出してくれます。一般的には、Eventクラスは app/Events ディレクトリー内に置き、リスナーは app/Listeners ディレクトリー内に置きます。これらのディレクトリーは最初は存在しません。 Artisan コマンドでイベントやリスナーを生成した時に自動的に作成されます。

イベントにより、複数の処理を切り離して実装することができます。一つのイベントは複数のリスナーを持つことができ、それらはお互いに依存しません。例えば、注文されたものを発送するたびに、ユーザーにSlackの通知を送信する場合を考えます。注文処理にSlackの通知のコードを組み込むこともできますが、OrderShippedイベントを発生させて、それを受け取ったリスナーがSlackの通知処理を行った方がシンプルです。

プロジェクトを準備

ログやエラー表示を実際に動かして、色々と試してみます。以下の手順で、プロジェクトを作成します。

  • ターミナルを起動して、プロジェクトを作成したいフォルダーに移動する
  • 環境を更新
composer global update
npm update -g npm
npm update -g
  • 以下を実行して、Laravelプロジェクトを作成し、プロジェクトフォルダーに移動して、サーバーを起動する
laravel new laravel-event-test
cd laravel-event-test
php artisan serve

以上で準備完了です。Webブラウザを起動して、 http://localhost:8000 にアクセスすると、インストールしたLaravelプロジェクトが表示されれば成功です。

イベントの登録とリスナー

app/Providers/EventServiceProvider.php の $listen プロパティーに、アプリのすべてのイベントとリスナーを登録します。キーにはイベント、値にリスナーを配列で指定します。初期状態では、 SomeEvent というイベントに対して、 EventListener が登録されています。app/EventsフォルダーもApp/Listenersフォルダーも初期状態では存在しないので動作はしません。

    protected $listen = [
        'App\Events\SomeEvent' => [
            'App\Listeners\EventListener',
        ],
    ];

イベントとリスナーを登録する

ここに、自前のイベントやリスナーを登録することができます。公式ドキュメントに従って OrderShipped イベントを作成してみましょう。

  • app/Providers/EventServiceProvider.php を開く
  • $listen プロパティーを以下のように修正
protected $listen = [
    'App\Events\OrderShipped' => [
        'App\Listeners\SendShipmentNotification',
    ],
];

イベントとリスナーの作成

自分でフォルダーやファイルを作成して、イベントやリスナーを作成することもできますが、 artisan の event:generate コマンドを呼び出せば、先ほど $listen に登録したイベントとリスナーのファイルを自動的に生成してくれます。ターミナルで、以下を実行してください。

php artisan event:generate

app/Events/OrderShipped.php と app/Listeners/SendShipmentNotification.php が生成されます。すでにファイルが作成済みの場合は何もしません。

イベントの定義

イベントクラスは、イベントに関する情報を保持するシンプルなデータコンテナです。先ほど作成した OrderShipped イベントに Eloquent ORM オブジェクトを持たせる例を以下に示します(公式ドキュメントのものは古いようで、useや継承などがうまく動作しませんでした)。

<?php

namespace App\Events;

use App\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class OrderShipped
{
    use InteractsWithSockets, SerializesModels;

    public $user;

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

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

イベントクラスには処理はなく、データを保持しているだけです。 SerializesModels トレイトを利用することで、イベントオブジェクトが Eloquent モデルを保持することができます。

引き続き、リスナーや User モデルの生成を行います。

リスナーの定義

イベントリスナーは、 handle メソッドでイベントのインスタンスを受け取ります。 event:generate コマンドでイベントを生成すれば、適切なイベントクラスを組み込んで、 handle メソッドにコメントと引数が定義されます。 handle メソッド内で、イベントに対応するために必要な様々な処理を行います。

ここでは、イベントを受け取った時にユーザー名をログに出力するようにします。

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;

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

    /**
     * Handle the event.
     *
     * @param  OrderShipped  $event
     * @return void
     */
    public function handle(OrderShipped $event)
    {
        // Access the order using $event->user...
        Log::info('Event Fired : ' . $event->user->name);
    }
}

リスナーの依存関係の解決と、イベントの伝播の停止

  • リスナーの依存関係は、Laravelの service container により、自動的に解決されます
  • 処理中のリスナーを終えたら、それ以降のイベント処理を停止したい場合があります(エラーが発生したなど)。そのような時は、 handle メソッドの戻り値を false にしてください。

データの準備

今回のサンプルを動かすには、User テーブルが必要なので生成します。

  • .env を開く
  • DB_ から始まるデータベースの設定を、各々の環境通りに設定する
  • データベースに接続して、必要なデータベースを作成。以下、MySQLの手順
sudo mysql.server start
mysql -u ユーザー名 -p
  • パスワードを入力して、ログインする
  • 以下でデータベースを作成
create database laravel_event_test;
exit
  • MySQLから出たら、ターミナルで以下を実行する
php artisan migrate

エラーが表示されたら、データベースの作成ミスか、 .env の設定のミスなので、設定を見直してください。

イベントの発動(Firing Events)

イベントを発動させるには、 event ヘルパー関数にイベントのインスタンスを渡します。event 関数はグローバルに定義されているのでどこでも使えます。イベントを発動させると、登録されたリスナーが次々に呼び出されます。

ルートを呼び出したところで、イベントを発動させてみましょう。

  • routes/web.php を開く
  • getメソッドを以下のように修正する
Route::get('/', function () {
    // テスト用のユーザーデータをファクトリーで作成
    $user = factory(App\User::class)->create();

    // イベントを生成
    event(new OrderShipped($user));

    // トップ画面を描画
    return view('welcome');
});

上書き保存をしたら、 http://localhost:8000/ をWebブラウザーで開いてください。Laravelと表示されれば成功です。 storage/logs/laravel.log を開いて一番下を見ると、リスナーの handle メソッドが書き出した以下のようなログが追加されています。

[20xx-xx-xx xx:xx:xx] local.INFO: Event Fired : Dr. Mervin Schaefer IV  

日付や名前は、実行するたびに異なります。

以上で、最低限のイベントやリスナーの登録と発動までできました。これ以降は、イベントをより便利に使うための情報です。

一つのクラスに複数のイベントを定義する(Event Subscribers)

Event Subscribers クラスを使うと、一つのクラスに複数のイベントを定義して、クラス内にイベントハンドラー(リスナー)を持たせることができます。 Subscriberクラスは、 subscribe メソッドを持ち、イベント dispatcher インスタンスが渡されます。渡された dispatcher インスタンスを使って、イベントのリスナーメソッドを呼び出すことができます。

公式ドキュメントでは、Subscriberは app/Listeners ディレクトリーに作成しているので、それに従って、2つのイベントハンドラーを持った UserEventSubscriber を作成してみます。

Subscriberを作成する

  • app/Listeners ディレクトリー内に UserEventSubscriber.php を作成する
  • 作成したファイルを開いて、以下のコードを入力する
<?php

namespace App\Listeners;

use Illuminate\Support\Facades\Log;

class UserEventSubscriber
{
    /**
     * ユーザーのログインイベントを処理するハンドラー
     */
    public function onUserLogin($event) {
        Log::info('ログイン : ' . $event->user->name);
    }

    /**
    * ユーザーのログアウトイベントを処理するハンドラー
     */
    public function onUserLogout($event) {
        Log::info('ログアウト : ' . $event->user->name);
    }

    /**
     * Subscriberに、リスナーを登録する
     *
     * @param  Illuminate\Events\Dispatcher  $events
     */
    public function subscribe($events)
    {
        $events->listen(
            'App\Events\UserLogin',
            'App\Listeners\UserEventSubscriber@onUserLogin'
        );

        $events->listen(
            'App\Events\UserLogout',
            'App\Listeners\UserEventSubscriber@onUserLogout'
        );
    }
}

対応するイベントを作成する

イベントの内容は先に作成した OrderShipped.php と同じなので、複製して、クラス名だけ変更して利用します。

  • app/Events/OrderShipped.php を同じフォルダーに複製する
  • ファイル名を UserLogin.php に変更する
  • UserLogin.php を開いて、クラス名を UserLogin に変更する
  • 同様に、 app/Events/OrderShipped.php を同じフォルダーに複製する
  • ファイル名を UserLogout.php に変更する
  • UserLogout.php を開いて、クラス名を UserLogout に変更する

Subscriber を登録する

Subscriber を登録します。

  • app/Providers/EventServiceProvider.php を開く
  • 以下のプロパティーをクラス内に追加する
     protected $subscribe = [
         'App\Listeners\UserEventSubscriber',
     ];

イベントを発動させるルートを作成する

  • routes/web.php を開く
  • 先頭の方に、以下の use を追加する
use App\Events\UserLogin;
use App\Events\UserLogout;
  • 以下の2つのルートを追加する
Route::get('/login', function () {
    // テスト用のユーザーデータをファクトリーで作成
    $user = factory(App\User::class)->create();

    // イベントを生成
    event(new UserLogin($user));

    // トップ画面を描画
    return 'ログインしました。';
});

Route::get('/logout', function () {
    // テスト用のユーザーデータをファクトリーで作成
    $user = factory(App\User::class)->create();

    // イベントを生成
    event(new UserLogout($user));

    // トップ画面を描画
    return 'ログアウトしました。';
});

以上ができたら、 http://localhost:8000/login をWebブラウザーで開いてください。「ログインしました。」と表示されたら、 storage/logs/laravel.log を開いて、以下のようなログが残っていることを確認してください(日付や名前は、毎回異なります)。

[2016-10-09 08:41:59] local.INFO: ログイン : Mrs. Marjorie Klein Jr.  

次に、 http://localhost:8000/logout をWebブラウザーで開いてください。「ログアウトしました。」と表示されたら、先ほどと同じログファイルに、ログアウトのログが書き込まれます。

リスナーがそれほど規模が大きくなく、機能をまとめておきたい場合は、Subscriber を利用すると良いでしょう。

キューされたイベントリスナー(Queued Event Listeners)

e-mailを送ったり、HTTPリクエストを作成するなど、時間のかかるようなリスナーを実装する場合は、 キュー を使うリスナーが便利です。これを使う場合は、事前にキューの設定をして、 sync ドライバー以外の場合は、キューのdaemon を開始する必要があります。キューの記事はこちら(Laravel(5.3) のキューを試す)

ローカルのテスト用キューである sync ドライバーでは同期処理になるので、本来のキューの目的である非同期処理は行われません。

リスナーをキューに登録させる

リスナーをキューに登録するには、 ShouldQueue インターフェースをリスナークラスに実装( implements )します。 event:generate を使って作成したリスナーには、あらかじめ ShouldQueue のネームスペースが定義されているので、すぐに利用することができます。

キューへのアクセス

リスナーをキューに登録した後、それが実行される前に削除や解放をしたい場合は、 Illuminate\Queue\InteractsWithQueue トレイトを利用します。このトレイトは、Artisanでqueue:generateした場合、自動的に設定されています。

hander メソッド内で、 $this->release(30); などのように、キューへの指示が可能になります。

詳細は、キューのドキュメントをご確認ください。 →
Queues - Laravel - The PHP Framework For Web Artisans