イントロダクション

Laravelのイベント機能はオブザーバーのシンプルな実装を提供します。アプリケーションの中でイベントを発行し、購読するために使用します。イベントクラスは通常app/Eventsディレクトリーへ保存され、リスナーはapp/Listenerに設置します。

イベント/リスナー登録

Laravelアプリケーションに含まれているEventServiceProviderは、イベントハンドラーを全て登録するために便利な手段を提供しています。listenプロパティは全イベント(キー)とリスナー(値)で構成されている配列です。もちろん、アプリケーションで必要とされているイベントをこの配列に好きなだけ追加できます。たとえばPodcastWasPurchasedイベントを追加してみましょう。

/**
 * アプリケーションのイベントリスナーをマップ
 *
 * @var array
 */
protected $listen = [
    'App\Events\PodcastWasPurchased' => [
        'App\Listeners\EmailPurchaseConfirmation',
    ],
];

イベントとリスナークラス生成

もちろん毎回ハンドラーやリスナーを作成するのは手間がかかります。代わりにハンドラーとリスナーをEventServiceProviderに追加し、event:generateコマンドを使いましょう。このコマンドはEventServiceProviderにリストしてあるイベントやリスナーを生成してくれます。既存のイベントとハンドラーには当然変更を加えません。

php artisan event:generate

イベントを自分で登録する

通常、イベントはEventServiceProvider$listen配列を使用し登録すべきです。しかし、EventファサードやIlluminate\Contracts\Events\Dispatcher契約の実装を使用し、イベントディスパッチャーでイベントを自分で登録することもできます。

/**
 * アプリケーションのその他のイベントを登録
 *
 * @param  \Illuminate\Contracts\Events\Dispatcher  $events
 * @return void
 */
public function boot(DispatcherContract $events)
{
    parent::boot($events);

    $events->listen('event.name', function ($foo, $bar) {
        //
    });
}

ワイルドカードイベントリスナー

*をワイルドカードとしてリスナーを登録することができ、同じリスナーで複数のイベントを補足することが可能です。ワイルドカードリスナーは一つの引数により、イベントデータ全体の配列を受け取ります。

$events->listen('event.*', function (array $data) {
    //
});

イベント定義

イベントクラスはシンプルなデータコンテナで、イベントに関する情報を保持します。たとえば生成したPodcastWasPurchasedイベントがEloquent ORMオブジェクトを受け取るとしましょう。

<?php

namespace App\Events;

use App\Podcast;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;

class PodcastWasPurchased extends Event
{
    use SerializesModels;

    public $podcast;

    /**
     * 新しいイベントインスタンスの生成
     *
     * @param  Podcast  $podcast
     * @return void
     */
    public function __construct(Podcast $podcast)
    {
        $this->podcast = $podcast;
    }
}

ご覧の通り、このクラスは特別なロジックを含みません。購入されたPodcastオブジェクトのコンテナに過ぎません。イベントオブジェクトがPHPのserialize関数でシリアライズされる場合でも、EloquentモデルはSerializesModelsトレイトが優雅にシリアライズします。

リスナーの定義

次にサンプルイベントのリスナーを取り上げましょう。イベントリスナーはイベントインスタンスをhandleメソッドで受け取ります。event:generateコマンドは自動的に対応するイベントクラスをインポートし、handleメソッドのイベントのタイプヒントを行います。そのイベントに対応するために必要なロジックを実行してください。

<?php

namespace App\Listeners;

use App\Events\PodcastWasPurchased;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class EmailPurchaseConfirmation
{
    /**
     * イベントリスナー生成
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * イベントの処理
     *
     * @param  PodcastWasPurchased  $event
     * @return void
     */
    public function handle(PodcastWasPurchased $event)
    {
        // $event->podcastでポッドキャストへアクセス…
    }
}

イベントリスナーは必要な依存をコンストラクターのタイプヒントで指定できます。イベントリスナーは全てLaravelのサービスコンテナで依存解決されるので、依存は自動的に注入されます。

use Illuminate\Contracts\Mail\Mailer;

public function __construct(Mailer $mailer)
{
    $this->mailer = $mailer;
}

イベントの伝播の停止

場合によりイベントが他のリスナーへ伝播されるのを止めたいこともあります。その場合はhandleメソッドからfalseを返してください。

イベントリスナーのキュー投入

イベントハンドラーをキューに投入する必要があるのですか? これ以上ないくらい簡単です。リスナークラスにShouldQueueインターフェイスを追加するだけです。リスナーがevent:generate Artisanコマンドにより生成されている場合は現在の名前空間にこのインターフェイスがインポートされていますので、すぐに使えます。

<?php

namespace App\Listeners;

use App\Events\PodcastWasPurchased;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class EmailPurchaseConfirmation implements ShouldQueue
{
    //
}

これだけです!これでこのハンドラーがイベントのために呼び出されると、Laravelのキューシステムを使い、イベントデスパッチャーにより自動的にキューへ投入されます。キューにより実行されるリスナーから例外が投げられなければ、そのキュージョブは処理が済んだら自動的に削除されます。

キューへのアクセス

裏で動作しているキュージョブのdeletereleaseメソッドを直接呼び出したければ可能です。生成されたリスナーではデフォルトでインポートされているIlluminate\Queue\InteractsWithQueueトレイトを呼び出してください。両メソッドへのアクセスを提供します。

<?php

namespace App\Listeners;

use App\Events\PodcastWasPurchased;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class EmailPurchaseConfirmation implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(PodcastWasPurchased $event)
    {
        if (true) {
            $this->release(30);
        }
    }
}

イベント発行

イベントを発行するにはEventファサードを使用し、fireメソッドにイベントのインスタンスを渡してください。fireメソッドはそのイベントを登録されているリスナー全部へディスパッチします。

<?php

namespace App\Http\Controllers;

use Event;
use App\Podcast;
use App\Events\PodcastWasPurchased;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * 指定されたユーザーのプロフィール表示
     *
     * @param  int  $userId
     * @param  int  $podcastId
     * @return Response
     */
    public function purchasePodcast($userId, $podcastId)
    {
        $podcast = Podcast::findOrFail($podcastId);

        // ポッドキャスト購入ロジック…

        Event::fire(new PodcastWasPurchased($podcast));
    }
}

もしくは、グローバルなeventヘルパー関数でイベントを発行します。

event(new PodcastWasPurchased($podcast));

ブロードキャストイベント

多くの近代的なアプリケーションでは、リアルタイムでライブ更新されるユーザーインターフェイスを実装するために、Webソケットを利用しています。サーバーであるデータが更新されたら、処理するクライアントへWebソケット接続を通じて送られます。

こうしたタイプのアプリケーション構築を援助するため、LaravelはイベントをWebソケット接続を通じて簡単に「ブロードキャスト」できます。Laravelのイベントをブロードキャストすることで、サーバーサイドのコードとクライアントサイドのJavaScriptで同じ名前のイベントを共有できるようになります。

設定

イベントブロードキャストの設定オプションはconfig/broadcasting.php設定ファイルの中にあります。Laravelはいくつかのドライバーを用意しており、PusherRedis、それにローカルの開発とデバッグのためのlogドライバーがあります。

ブロードキャスト事前要件

以下の依存パッケージがイベントのブロードキャストに必要です。

  • Pusher: pusher/pusher-php-server ~2.0
  • Redis: predis/predis ~1.0

動作要件:キュー

イベントをブロードキャストする前に、キューリスナーを設定し動かしておく必要があります。全てのイベントブロードキャストはキュー投入されるジョブとして動きますので、アプリケーションの反応時間にシリアスな影響を与えません。

ブロードキャストイベント作成

Laravelにイベントがブロードキャストされることを知らせるためにIlluminate\Contracts\Broadcasting\ShouldBroadcastインターフェイスを実装してください。ShouldBroadcastインターフェイスはbroadcastOnメソッドの実装ひとつだけを求めます。broadcastOnメソッドはブロードキャストされる「チャンネル」名の配列を返す必要があります。

<?php

namespace App\Events;

use App\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ServerCreated extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $user;

    /**
     * イベントインスタンスの生成
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * イベントをブロードキャストするチャンネル
     *
     * @return array
     */
    public function broadcastOn()
    {
        return ['user.'.$this->user->id];
    }
}

それから、通常通りにイベントを発行するだけです。イベントが発行されるとキュージョブで指定されたブロードキャストドライバーにより自動的にブロードキャストされます。

ブロードキャストイベント名のオーバーライド

ブロードキャストイベント名は、デフォルトでそのイベントの完全な名前空間名になります。上の例のクラスでは、ブロードキャストイベントは、App\Events\ServerCreatedになります。broadcastAsメソッドにより、このブロードキャストイベント名を好きなようにカスタマイズできます。

/**
 * ブロードキャストイベント名の取得
 *
 * @return string
 */
public function broadcastAs()
{
    return 'app.server-created';
}

ブロードキャストデータ

イベントをブロードキャストする時、全publicプロパティーは自動的にシリアライズされ、イベントの本体(ペイロード)としてブロードキャストされます。それによりJavaScriptアプリケーションでパブリックデータへアクセスできるようになります。たとえばイベントにEloquentモデルのpublicの$userプロパティがあったとすると、ブロードキャストの本体は次のようになります。

{
    "user": {
        "id": 1,
        "name": "Jonathan Banks"
        ...
    }
}

しかしブロードキャストされる本体をより良く調整、コントロールしたければ、broadcastWithメソッドをイベントに追加してください。このメソッドは、そのイベントでブロードキャストしたいデータの配列を返す必要があります。

/**
 * ブロードキャストするデータ取得
 *
 * @return array
 */
public function broadcastWith()
{
    return ['user' => $this->user->id];
}

イベントブロードキャストの利用

Pusher

PusherのJacaScript SDKを使い、Pusherドライバーでイベントブロードキャストを便利に使いたいと思うでしょう。たとえば前例のApp\Events\ServerCreatedイベントを使ってみましょう。

this.pusher = new Pusher('pusher-key');

this.pusherChannel = this.pusher.subscribe('user.' + USER_ID);

this.pusherChannel.bind('App\Events\ServerCreated', function(message) {
    console.log(message.user);
});

Redis

Redisブロードキャストを使用する場合、メッセージを受け取るために自分でRedisのpub/subコンシューマーを書く必要があり、自分で選んだWebソケットのテクノロジーを使いブロードキャストしなくてはなりません。たとえばNodeで書かれ人気のあるSocket.ioライブラリーを使うことを選択できます。

socket.ioioredis Nodeライブラリーを使い、Laravelアプリケーションからブロードキャストされた全イベントを発行するイベントブロードキャスターを簡単に書けます。

var app = require('http').createServer(handler);
var io = require('socket.io')(app);

var Redis = require('ioredis');
var redis = new Redis();

app.listen(6001, function() {
    console.log('Server is running!');
});

function handler(req, res) {
    res.writeHead(200);
    res.end('');
}

io.on('connection', function(socket) {
    //
});

redis.psubscribe('*', function(err, count) {
    //
});

redis.on('pmessage', function(subscribed, channel, message) {
    message = JSON.parse(message);
    io.emit(channel + ':' + message.event, message.data);
});

イベント購読

イベント購読クラスは一つのクラスで多くのイベントを購読するためのものです。購読クラスはイベントデスパッチャーインスタンスが渡されるsubscribeメソッドを実装しなくてはなりません

<?php

namespace App\Listeners;

class UserEventListener
{
    /**
     * ユーザーログインイベントの処理
     */
    public function onUserLogin($event) {}

    /**
     * ユーザーログアウトイベントの処理
     */
    public function onUserLogout($event) {}

    /**
     * イベント購入リスナーの登録
     *
     * @param  Illuminate\Events\Dispatcher  $events
     */
    public function subscribe($events)
    {
        $events->listen(
            'App\Events\UserLoggedIn',
            'App\Listeners\UserEventListener@onUserLogin'
        );

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

}

イベント購読クラスの登録

購読クラスを定義したらイベントディスパッチャーに登録します。EventServiceProvider$subscribeプロパティを使い、後続クラスを登録します。たとえばUserEventListenerを追加してみましょう。

<?php

namespace App\Providers;

use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * アプリケーションのイベントリスナーをマップ
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * 登録する購読クラス
     *
     * @var array
     */
    protected $subscribe = [
        'App\Listeners\UserEventListener',
    ];
}

フレームワークのイベント

Laravelはフレームワークが実行するアクションに対して発行する、様々な「コア」イベントを提供しています。皆さんが独自に用意するカスタムイベントと同じ方法で、これらのコアイベントも購入できます。

イベント パラメータ
artisan.start $application
auth.attempt $credentials, $remember, $login
auth.login $user, $remember
auth.logout $user
cache.missed $key
cache.hit $key, $value
cache.write $key, $value, $minutes
cache.delete $key
connection.{name}.beganTransaction $connection
connection.{name}.committed $connection
connection.{name}.rollingBack $connection
illuminate.query $query, $bindings, $time, $connectionName
illuminate.queue.after $connection, $job, $data
illuminate.queue.failed $connection, $job, $data
illuminate.queue.stopping null
mailer.sending $message
router.matched $route, $request
composing:{view name} $view
creating:{view name} $view