Laravelは、アプリケーション独自のイベントを実装する仕組みをフレームワークとして用意しています(参照: Events)。
5.1からこのイベントシステムに、PusherおよびRedis PubSubに対応したBroadcasting Eventsが追加されました。

Broadcasting Eventsを利用したリアルタイム更新アプリのチュートリアルが、sitepointにあったので、それを試しつつLaravelのEventについて調べてみたことをまとめてみました。

今回試した記事は、「Real-time Apps with Laravel 5.1 and Event Broadcasting」になります。

実装していくのは、以下のような構成で更新をリアルタイムに通知するToDoアプリです。
Laravel5.1 Bloadcasting Pusher Provider アーキテクチャ
新規にToDoの項目が追加されるとLaravelアプリはPusher APIを呼び出します。
Pusherはアプリを参照しているすべてのブラウザに更新を通知します。
通知を受けるとJavascriptからLaravelのAPIを呼び出し新しく追加されたToDoアイテムを表示する流れです。

Laravelの動作環境は、Laravel Homesteadを利用しました。
Homesteadの準備については「Laravel homesteadを利用したLaravel 5 ローカル開発環境の構築」も参照ください。

サンプルアプリの準備

Githubに公開されているチュートリアル用のサンプルアプリをクローンします。
こちらは、リアルタイムの機能を有効にする前の簡単なToDoアプリになります。

$ git clone https://github.com/cwt137/l51-todo-app todo-app

HomesteadのSyncfolderをクローンしたディレクトリにセットします。
ホームディレクトリにプロジェクトをクローンした場合は以下のように設定します。

folders:
    - map: ~/todo-app
      to: /home/vagrant/Code/Laravel

Homesteadを起動して、sshログインします。

$ cd <Homesteadホームディレクトリ>
$ vagrant up
$ vagrant ssh

Homesteadにログインしたら、プロジェクトディレクトリCodeでcomposer installを実行しますが、いくつか前準備をします。

まず、composer.lockを削除

$ cd Code/Laravel
$ rm composer.lock

クローンしたcomposer.lockに含まれるext-mcryptは、現在のHomestead環境には含まれていないので、そのまま実行するとエラーになります。composer.lockは削除して、新たにcomposer installしています。
これは、Laravelは5.1から暗号化に使われるパッケージがext-mcryptからopensslに変更されていました(リリースノート Laravel 5.1 Encryptionを参照)。
このチュートリアルサンプルが安定版になる前に作成されたものなのでext-mcryptが含まれてしまっていると思われます。

同様の理由でconfig/app.phpのcipherの値をMCRYPT_RIJNDAEL_128からAES-256-CBCに変更しておきます。

    'cipher' => 'AES-256-CBC',

上記2点で、準備ができたのでcomposer installを実行します。

$ composer install

Laravelはプロジェクト作成時に.env.exampleをコピーして.envを作成と暗号化キーの作成を実行しますが、クローンしたプロジェクトの場合は別途スクリプトを実行する必要があります。

$ composer run-script post-root-package-install
$ composer run-script post-create-project-cmd

データベースのマイグレーションをして、準備は完了です。

$ php artisan migrate

一旦ここで、アプリの動作を確認します。
Laravel 5.1 Todoアプリ

この段階では、ブラウザを再読み込みしなければ更新は反映されません。

Broadcasting Eventsの追加

次に、更新通知をPusherに伝えるBroadcastig Eventsを追加していきます。

ToDoの追加・更新(完了チェック)・削除時にイベントを発火させるようにします。
Illuminate\Contracts\Broadcasting\ShouldBroadcastを実装する、それぞれ以下のイベントを追加します。

  • ToDoの追加: ItemCreated
  • ToDoの更新: ItemUpdated
  • ToDoの削除: ItemDeleted

Larvalフレームワークは、インタフェースShouldBroadcastを実装したクラスを参照し、broadcastOnメソッドに定義されたチャンネルごとにブロードキャストします。

上記の3つのイベントクラスを、artisanコマンドで作成します。

$ php artisan make:event ItemCreated
$ php artisan make:event ItemUpdated
$ php artisan make:event ItemDeleted

ItemCreated.phpを開き、以下のハイライトの行を追加します。

<?php

namespace App\Events;

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

class ItemCreated extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $id;

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

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

broadcastOnメソッドは、itemActionというチャンネルを返すように実装しています。

ItemUpdated.phpを開き、以下のハイライトの行を追加します。

<?php

namespace App\Events;

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

class ItemUpdated extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $id;
    public $isCompleted;

    /**
     * Create a new event instance.
     *
     * @param Item $item
     * @return void
     */
    public function __construct(Item $item)
    {
        $this->id = $item->id;
        $this->isCompleted = (bool) $item->isCompleted;
    }

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

ItemDeleted.phpを開き、以下のハイライトの行を追加します。

<?php

namespace App\Events;

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

class ItemDeleted extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $id;

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

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

モデルの更新時にイベントを発火する

Eloquentモデルには、レコードのCRUDイベント発生時にフックするデータベーストリガー機能があります。
参照: Events | Eloquent: Getting Started

この仕組を利用して、Itemモデルの追加時にcreatedイベント、更新時にupdated、削除時はdeletedイベントを受け付け、定義した各ブロードキャストイベントを発火させます。

モデルのイベントを受け付けるリスナーは、サービスプロバイダーに登録します。
ここでは、app/Providers/AppServiceProvider.phpにのbootメソッドに以下のようにコードを追記します。

<?php

namespace App\Providers;

use Event;
use App\Item;
use App\Events\ItemCreated;
use App\Events\ItemUpdated;
use App\Events\ItemDeleted;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Item::created(function ($item) {
            Event::fire(new ItemCreated($item));
        });

        Item::updated(function ($item) {
            Event::fire(new ItemUpdated($item));
        });

        Item::deleted(function ($item) {
            Event::fire(new ItemDeleted($item));
        });
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

以上で、サーバー側の実装は完了です。

Pusherアプリケーションの準備

Pusherでアカウントを作成して、チュートリアルサンプル用のアプリケーションを作成します。

[New App]ボタンをクリックしてアプリの作成を始めます。
Laravel 5.1 Pusher管理コンソール

以下のフォームが表示されます。
Laravel 5.1 Pusher管理コンソール - アプリケーションの作成

1. Name Your app

適当なアプリ名を入力して、[us-each-1 blue]を選択します。

2. What’s your fronted tech?

JSを選択します。

3. What’s your backend tech?

Larvalを選択します。

[Create My App]ボタンをクリックするとアプリが作成されます。

laravel5-pusher-show-app-credentials

アプリの詳細ページの右側に、app_id、key、secretが表示されます。
これらの値を、Laravelプロジェクトに設定します。

Pusherをプロジェクトに設定

Laravalフレームワークには、PusherのPHPライブラリの呼び出しが予め含まれており(Illuminate/Broadcasting/BroadcastManager.phpあたりを参照)、config/broadcasting.phpを確認するとデフォルトでブロードキャストの設定にはPusherが選択されています。

そのため、PusherをLaravelプロジェクトに組み込むには、ライブラリの追加とサービスの画面で作成したPusherアプリケーションの情報を設定するだけです。

Pusherのライブラリは、Composerでインストールできます。

$ composer require pusher/pusher-php-server
You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug
Using version ^2.3 for pusher/pusher-php-server
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing pusher/pusher-php-server (v2.3.0)
    Downloading: 100%         

Writing lock file
Generating autoload files
> php artisan clear-compiled
> php artisan optimize
Generating optimized class loader
Compiling common classes

Pusherのサービス画面で作成したアプリケーションを開きapp_id、key、secretをコピーして.envの最後に追加します。

PUSHER_KEY=YOUR_PUSHER_KEY
PUSHER_SECRET=YOUR_PUSHER_SECRET
PUSHER_APP_ID=YOUR_PUSHER_APP_ID

クライアント側の実装

最後に、ビューでPusher Javascriptライブラリの読み込み、Pusherからの通知の受信を実装していきます。

resources/views/index.blade.phpにライブラリの読み込みを追加します。

    ...
    <script src="js/app.js"></script>
    <script src="https://js.pusher.com/3.0/pusher.min.js"></script>
    <script>
        var pusher = new Pusher("{{ env('PUSHER_KEY') }}");
    </script>
    <script src="js/pusher.js"></script>
  </body>
</html>

public/js/pushuer.jsを作成して、itemActionチャンネルのリスナーを実装します。

( function( $, pusher, addItem, removeItem ) {

var itemActionChannel = pusher.subscribe( 'itemAction' );

itemActionChannel.bind( "App\\Events\\ItemCreated", function( data ) {

    addItem( data.id, false );
} );

itemActionChannel.bind( "App\\Events\\ItemUpdated", function( data ) {

    removeItem( data.id );
    addItem( data.id, data.isCompleted );
} );

itemActionChannel.bind( "App\\Events\\ItemDeleted", function( data ) {

    removeItem( data.id );
} );

} )( jQuery, pusher, addItem, removeItem);

以上で、実装は完了です。
ブラウザを2つ以上立ち上げてToDoの追加/削除を行うと、ブラウザの更新なしに反映されることが確認できます。

まとめ

以上のように、Laravelはイベント機構と通知の仕組みをPusherを利用することで無理なく実装することができます。

また、Laravelのブロードキャストイベントは、Pusherの他にRedisのPubSubを利用したブロードキャストも対応しています。
Regisとnode.jsのsocket.ioを組み合わせた例については、「Laravel 5.1- Broadcasting Events using Redis Driver, Socket.io」がよくまとまっていそうなので、こちらも試してみたいところです。