Laravel Broadcast Eventを使いこなそう

これは Laravelリファレンス発売記念、販売促進アドベントカレンダー www.adventar.org の2015年12月14日分です(1日遅れ更新中)。

Laravel Events

LaravelのEventはフレームワークに登録されているコアのイベントを監視して、 実行させるイベント、 またはユーザーが実装したイベントを実行できます。 このイベントについては書籍「Laravelリファレンス」でも解説しています。

さらにこのイベントにはブロードキャストイベントがあります。

このブロードキャストはwebsocketを利用して、 イベント実行できますブラウザに通知するなどの リアルタイム性あるwebアプリケーションの開発が簡単に行えます。

ただし、このブロードキャストは言語仕様上PHPのみで実装することが難しく、 Node.jsなどと併用する必要があるということに 注意しておきましょう。 このブロードキャストは書籍の一部で取り上げていましたが、 JavaScriptにも強く依存しているため、 解説を外した悲しい機能です。

公式リファレンスにも簡単な実装方法が載っています。 公式リファレンスを照らし合わせながら、 理解を深めてみましょう。

Broadcastに対応したEventクラス

Eventはmake:eventで作成できます。

$ php artisan make:event Broadcast\\NotificationEvent

上記のコマンドで実行すると、Events配下にPSR-4に沿ったディレクトリを作成し、クラスが配置されます。
(Eloquentクラスも全て同じです。直下が置き場所ではありません。ソースコードに書いてあります:) )

作成されたクラスは次のとおりです。

<?php

namespace App\Events\Broadcast;

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

class NotificationEvent extends Event
{
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return [];
    }
}

Broadcastを利用する場合は、Illuminate\Contracts\Broadcasting\ShouldBroadcastインターフェースを実装します。
実装する必要があるものは、broadcastOnメソッドだけです。
このメソッドはwebsocketで利用するチャンネルを意味します。

SerializesModelsとは?

Illuminate\Queue\SerializesModelsトレイトは、
イベントクラスのプロパティにEloquentORMのインスタンスが利用されている場合にのみ作用します。

このトレイトには、下記のsleep、wakeupメソッドがあり、
これらはQueueでserialize、 unserializeする場合に作用するマジックメソッドです。

<?php

// 一部抜粋
    public function __sleep()
    {
        $properties = (new ReflectionClass($this))->getProperties();

        foreach ($properties as $property) {
            $property->setValue($this, $this->getSerializedPropertyValue(
                $this->getPropertyValue($property)
            ));
        }

        return array_map(function ($p) {
            return $p->getName();
        }, $properties);
    }


    public function __wakeup()
    {
        foreach ((new ReflectionClass($this))->getProperties() as $property) {
            $property->setValue($this, $this->getRestoredPropertyValue(
                $this->getPropertyValue($property)
            ));
        }
    }

イベント実装

作成したクラスを使ってブロードキャストイベントの準備をします。
EloquentORMを利用すれば簡単にできますが、ここではEloquentを利用せずに実装します。

(アプリケーション実装時にデータベースアクセスの全てをEloquentで実装するのはお勧めしません。
実装に利用するのは簡単ですが、イーガーローディングなどを使ったとしても、
発行されるIN句や、その他のクエリについて理解しておくべきだからです。
これについては長くなるのでまた別の機会に・・)

作成されたクラスにあるbroadcastOnメソッドを次のようにします。

<?php

namespace App\Events\Broadcast;

use App\Events\Event;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class NotificationEvent extends Event implements ShouldBroadcast
{
    /**
     * @return array
     */
    public function broadcastOn()
    {
        return [
            'Laravel.Reference'
        ];
    }
}

次にコントローラやサービス、その他のクラスなどからこのイベントを利用してみましょう。 コントローラの場合は次のようになるでしょう。

<?php

namespace App\Http\Controllers;

use Illuminate\Contracts\Events\Dispatcher;
use App\Events\Broadcast\NotificationEvent;

/**
 * Class IndexController
 */
class IndexController extends Controller
{
    /**
     * @param Dispatcher $dispatcher
     *
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function index(Dispatcher $dispatcher)
    {
        $dispatcher->fire(new NotificationEvent());
        return view('index');
    }
}

ファンクショナルテストであれば次のように出力内容が確認できます。

<?php

class BroadcastTest extends TestCase
{
    /** @var \Illuminate\Filesystem\Filesystem */
    private $filesystem;

    public function setUp()
    {
        parent::setUp();
        $this->filesystem = new \Illuminate\Filesystem\Filesystem;
    }

    public function testBroadcastLogHasBeenOutput()
    {
        $this->call('GET', '/');
        $this->assertResponseStatus(200);
        $path = storage_path('logs/laravel.log');
        $log = $this->filesystem->get(storage_path('logs/laravel.log'));

        $this->assertNotFalse(strpos($log, '[Laravel.Reference]'));
        $this->beforeApplicationDestroyed(function() use ($path) {
            $this->filesystem->delete($path);
        });
    }
}

phpunit.xml 記述を忘れないようにしましょう

ブラウザなどで確認するにはconfig/broadcasting.phpのdefaultをlog、または.envファイルでBROADCAST_DRIVERを利用します。

次のようにlogに出力されているはずです。

testing.INFO: Broadcasting [App\Events\Broadcast\NotificationEvent] on channels [Laravel.Reference] with payload:

動作を確認したところで実際にwebsocketで確認してみましょう。

websocket利用の手引き

この機能を利用するには、pusherを利用するか、 Redisのpubsubを利用する必要があります。

忘れずに下記のライブラリをcomposer.jsonに記述するか、コマンドでインストールしてください。

"pusher/pusher-php-server": "~2.0"
"predis/predis": "~1.0"

pusherを利用する場合

Pusher | Leader In Realtime Technologies pusherを利用する場合はユーザー登録を行い、
必要な情報を記述してconfig/broadcasting.phpや.envを使ってKEYなどを指定してください。

つぎのコードを使って動作させてみましょう。

<script src="//js.pusher.com/3.0/pusher.min.js"></script>
<script>
    $(document).ready(function () {
        var pusher = new Pusher('{{{ config('broadcasting.connections.pusher.key') }}}');
        var channel = pusher.subscribe('Laravel.Reference');
        channel.bind('App\\Events\\Broadcast\\NotificationEvent', function (data) {
            console.log(data);
        });
    });
</script>

イベント名を変更

デフォルトのまま利用すると、イベント名が実行クラス名となり、あまり好ましくありません。
必ずbroadcastAsメソッドを使って変更するようにしてください。

送信データ

Eloquentを利用する場合は、自動で値を利用してくれますが、
それ以外の場合は、broadcastWithメソッドを使ってデータを指定します。
サービスクラスやリポジトリなどを利用してもいいでしょう。

これらを踏まえるとサンプルコードは以下の通りです。

<?php

namespace App\Events\Broadcast;

use App\Events\Event;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class NotificationEvent extends Event implements ShouldBroadcast
{
    /**
     * @return array
     */
    public function broadcastOn()
    {
        return [
            'Laravel.Reference'
        ];
    }

    /**
     * @return string
     */
    public function broadcastAs()
    {
        return 'reference.event';
    }

    /**
     * @return array
     */
    public function broadcastWith()
    {
        return [
            'message' => 'laravelリファレンス'
        ];
    }
}

jsのコードも次のように変更しましょう。

<script>
    $(document).ready(function () {
        var pusher = new Pusher('{{{ config('broadcasting.connections.pusher.key') }}}');
        var channel = pusher.subscribe('Laravel.Reference');
        channel.bind('reference.event', function (data) {
            console.log(data);
        });
    });
</script>

Queueと組み合わせる

BroadcastはWebsocketにフォーカスしがちですが、Queueと組み合わせて利用できることを忘れてはいけません。
クラス作成時に実装するShouldBroadcastは、デフォルトに設定されているqueueに登録して実行されます。
つまり、sync以外のものがデフォルトにされている場合は、queueを利用しなければ送信されません。

常にqueueqドライバをsyncとしたい場合は、
Illuminate\Contracts\Broadcasting\ShouldBroadcastNowインターフェースを利用してください。

Queueを利用する場合は、onQueueメソッドを利用します。

public function onQueue()
{
    return 'broadcast';
}

こうすることで、ジョブ登録後、
php artisan queue:work --queue=broadcast
という具合で任意のタイミングで配信などが行えます。

Broadcastの簡単な利用方法と、公式リファレンスに記述されているものに少し解説を付け加えて紹介しました。

アプリケーション作りに役立てみてください。