Laravel5.1のEventの使い方についてまとめてみた。

スポンサーリンク

Laravel5 Advent Calendar 2015の11日目の記事です。
空いていたので、入ってみました。

Laravel5.1のEventの使い方について書いてみた。(ただし、ブロードキャストイベントは含まれません)

Eventの機能は?

LaravelのEvent機能はobserverの実装を提供しています。
observerという単語をあまり聞かない人にはわかりにくいかと思います。observerには監視者とか観察者という意味があり、今回の場合だとイベント状態を監視する仕組みですね。

JavaScriptの場合、Object.observe等が有名ですね。こちらは無くなってしまうみたいですが。
使う感覚としては、NodeJSのSocketIOの機能を内部的にやる感じです。これで分かってしまう人であれば、先に下の方に書いてある簡易的に書く方法から見ていったほうがよいかもしれません。

対象読者

  • Laravel5の基本をある程度わかっている人

Eventを作ってみる

とりあえずイベントを作ってみます。

はっきり言って、回りくどいやり方です。ただこちらのほうがイベントとリスナーを分けることができるから良いと思います。もっとざっくり作りてーよって人は後にざっくり作る方法が載ってます。しかしそちらはライブラリに組み込むとき用です。通常はこっちで作るのがセオリーでしょう。

まずは、作るイベントをEventServiceProviderに登録します。EventServiceProviderapp/Providers/EventServiceProvider.phpに存在します。

初期状態では以下のような形で記述がされていると思います。

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'App\Events\SomeEvent' => [
            'App\Listeners\EventListener',
        ],
    ];

    /**
     * Register any other events for your application.
     *
     * @param  \Illuminate\Contracts\Events\Dispatcher  $events
     * @return void
     */
    public function boot(DispatcherContract $events)
    {
        parent::boot($events);

        //
    }
}

$listenメンバ変数を見てみると一つのイベントが記載されています。
基本的にイベントを作る際には、最初にlistenメンバ変数にkey(イベント)-value(リスナー)の形式で記述をしておきます。
イベント名はどのような名前でも実際問題ないのですが、この後のコマンドを使うためにも上記のソースのようにします。

今回、上記の名前ではわかりにくいので、ユーザが登録されたら(イベント)登録確認メールを送信する(リスナー)イベントを作ってみます。以下のようにします。

protected $listen = [
  'App\Events\UserWasRegistered' => [
    'App\Listeners\EmailUserRegisterConfirmation',
  ]
];

※ 完全修飾名なので::classを使いたいところですが、まだこのクラスは作られていないため文字列で指定する必要があります。

イベントとリスナークラス生成コマンド実行

変更ができたら、イベントとリスナーのクラスを生成できるコマンドが存在するので打ちましょう。
このコマンドは何度打っても大丈夫です。元からクラス等が存在している場合は変更は行われません。

$ php artisan event:generate
Events and listeners generated successfully!

上記のコマンドを打ったことで、app/Eventsapp/Listeners以下に対象のクラスファイルができているかと思います。

イベントの発火方法

では先に、発火する方法だけ伝えておきます。なぜここで教えるかですが、どのように動かすのかわからないまま編集してもよくわからないからです。

今回のイベントの場合、登録された際にEventを発火させる必要があります。登録するメソッドが存在するとして進めると、

<?php
namespace App\Http\Controllers;
use User;

class UserController extends Controller
{
  public function register()
  {
    $user = new User;
    // ...省略
    if($user->save())
    {
      event(new UserWasRegistered($user));
    }
  }
}

と言った感じです。イベントのインスタンスを渡していることを忘れないように。
(なおEvent::fire(new UserWasRegistered($user))でも可能です。その時はuseを忘れないように。)

イベントの定義

どんな感じに呼ぶか分かったので、まずはUserWasRegisteredを編集します。
これは事前にデータを用意するためのクラスと考えて良いと思います。

内容に関してはあまり考えなくていいです。一応言っておくと、SerializesModelsはModelがシリアライズ化されても問題ないように動かすためのトレイトです。中身は気にしなくてもOKです。(もし中身を理解する場合は__sleepメソッドと__wakeupメソッドを理解しておく必要があります)

このイベントの中のメンバ変数をリスナーで利用したりします。今回はイベントの発火時のコードにも書いてる通り、Userモデルの情報を渡してるのでコンストラクタで受け取ってpublicなメンバ変数に入れておきます。
わかりやすくするために、追加、変更した所には+をつけています。

<?php

namespace App\Events;

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

class UserWasRegistered extends Event
{
    use SerializesModels;

+    /**
+     * @var User
+     */
+    public $user;

    /**
     * Create a new event instance.
+     * @param User
     * @return void
     */
+    public function __construct(User $user)
    {
+        $this->user = $user;
    }
... 以降変更なし

これで、あとはListenerの方を変更するだけになりました。

リスナーの定義

それではListenerの方を変更していきます。
EmailUserRegisterConfirmation.phpを開きます。

handleメソッドを見てみると既にUserWasRegisteredクラスが定義されていることがわかりますね。


public function handle(UserWasRegistered $event)
{
    //
}

頭の良い人はすぐわかるかと思いますが、イベントが発火されるとそのイベント名に対応したリスナークラスが存在するかをLaravelは確認しに行きます。今回は$listenメンバ変数で指定してましたね。

Laravelがリスナークラスを発見すると、そのリスナークラスのhandleメソッドに対して、発火するときに渡されたnew UserWasRegistered($user)を引数にして呼び出してくれます。

この仕組み(ソース)を知りたい人がいるかと思います。
イベントが登録される処理はvendor/laravel/framework/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.phpbootメソッドです。
イベントが実行される処理はvendor/laravel/framework/src/Illuminate/Events/Dispacher.phpfireメソッドです。
しかし肝心のhandleメソッドが呼ばれている理由が把握出来ないと思います。
handleメソッドが呼ばれる理由は同じファイルのlistenメソッド時に呼ばれているparseClassCallableメソッドです。ここで"Class@method"のような形式になっていればメソッド名を返し、それ以外であればhandleメソッドが返されますね。

あとは、このhandleメソッドに色々書いていくだけです。
UserWasRegisteredには、userというメンバ変数を定義して作成したUserのインスタンスが渡されていましたので、$event->userで保存したUserのインスタンスにアクセスすることが出来ます。

今回の件だとこんな感じになると思います。


public function handle(UserWasRegistered $event)
{
  $user = $event->user;

  // 確認メールを送信する
  Mail::send('emails.register.confirm', ['user' => $user], function($m) use ($user) {
    $m->from('[email protected]', 'サンプルアプリ');
    $m->to($user->email, $user->name)->subject('【サンプルアプリ】登録の確認');
    });
}

これでイベントの基本的な使い方が分かったかと思います。

イベントをキューに入れたい

今回のようなイベントだと、発火してから待つ必要も無いのでキューに突っ込みたいですよね。
(キューに入れない場合はそのイベントが終わるまで待ってる)

キューに入れる形にしたいと思って、難しいと感じる人も居るかもしれませんが非常にこれが簡単なんです。

リスナークラスであるEmailUserRegisterConfirmationのクラス定義の部分を少し変えます。

class EmailUserRegisterConfirmation implements ShouldQueue

これだけです。implements ShouldQueueを追加するだけです。
これだけで済む理由はlisten時にvendor/laravel/framework/src/Illuminate/Events/Dispatcher.phpcreateClassCallableメソッドが呼ばれているからです。ここでShouldQueueインタフェースを実装しているか判定を行っています。

イベントのキューにアクセスしたい

もし、イベントのキューにアクセスしたい場合はリスナークラスにInteractsWithQueueトレイトをuseさせるだけです。これでキュージョブのdeleteメソッドやreleaseメソッドを呼び出せるようになります。
今回はQueueに関しては詳しく解説しません。

もっと簡易的にイベントを書きたい、ライブラリ内とかで発火させたい時

先程までは、きっちりとしたイベントを作成していました。
これは、上記の方が一般的にフレームワークを利用する上で必要だと感じられたからです。
しかしながら、人によってはLaravelのライブラリを作って、その中にEventを組み込みたい方が居るかと思います。(間違いなくいるでしょう)
そのようなとき、もっと簡易的にイベントを書くことが可能です。

端的に言えば\Illuminate\Contracts\Event\Dispacherクラスをインジェクションするだけで、基本的なイベントの機能が利用できます。

例えばですが、以下の様な感じです。

// 使うインタフェースをuseする
use \Illuminate\Contracts\Events\Dispacher as DispacherContract;

class AAA
{
  /**
   * 以下を指定しておくことで、IDE等で入力補完が効く!
   * @var DispacherContract
   */
  protected $events;

  // EventのDispacherをコンストラクタ/メソッドインジェクションで取得
  // これはサービスコンテナのaliasの機能。
  function __construct(DispacherContract $events)
  {
    $this->events = $events;
  }

  public function boot()
  {
    $this->events->listen('event.name', function($foo, $bar) {
      // イベントで行う内容
      });
  }
  public function handle()
  {
    $foo = 'foo';
    $bar = 'bar';
    // イベントを発火(第2引数に配列で渡せばlisten時に引数として取得できる)
    $this->events->fire('event.name', [$foo, $bar]);
  }
}

色々とソースは端折っていますが、基本的にはこんな感じです。
上記のようにfireするのはフレームワークのコア部分でも多用されています。

例えば、viewComposerの仕組みで使われている物だとvendor/laravel/framework/src/Illuminate/View/Factory.phpcallComposerメソッドが発火メソッドになってますよね。
じゃあ、Eventを登録しているのはどこだろうと思って上を辿って行くと、

  1. composerメソッドの中でaddViewEventメソッドが呼ばれ、
  2. このaddViewEventではaddEventListenerメソッドが呼ばれてますよね。
  3. このaddEventListenerメソッドを見てみたらlistenメソッドを動かしてるのがわかりますね!

なお、Laravelのフレームワークのイベントの名前は以下のURLに載ってます。
http://laravel.com/docs/5.1/events#framework-events

イベントが増えてきて、リスナーが大量になりそう、なった時

イベントが増えてきて、1つのリスナーで1つしか処理が出来ないのはきつい!と思い始めたら、イベント購読クラスと言うものを利用すると良いです。

イベント購読クラスはコマンドで生成出来ないので自分で作る必要があります。
これに関しては公式マニュアルを参考に、app/Listeners/UserEventListener.phpを作成します。

↑どうやら、php artisan make:listener リスナー名でイベントリスナーの基本ファイルは作れる模様です

<?php
namespace App\Listeners;

class UserEventListener
{
    /**
     * Handle user login events.
     */
    public function onUserLogin($event) {}

    /**
     * Handle user logout events.
     */
    public function onUserLogout($event) {}

    /**
     * Register the listeners for the subscriber.
     *
     * @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'
        );
    }
}

このソースですが、subscribeメソッドにeventのDispatcherが渡されるので、listenメソッドを利用してイベントとリスナーメソッドを登録すればOKです。

また、このイベントを登録をするときにはsubscribeメンバ変数に記述する必要があります。($listenに登録じゃないので注意)
メンバ変数が存在しない場合は追加しましょう。

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * The subscriber classes to register.
     *
     * @var array
     */
    protected $subscribe = [
        'App\Listeners\UserEventListener',
    ];
}

まとめ

Eventの仕様をしっかりと見ていくことで、よりEventの仕組みの把握に役立ちました。
Enjoy Event!

参考URL

コメント

',b.captions&&s){var u=J("figcaption");u.id="baguetteBox-figcaption-"+t,u.innerHTML=s,l.appendChild(u)}e.appendChild(l);var c=J("img");c.onload=function(){var e=document.querySelector("#baguette-img-"+t+" .baguetteBox-spinner");l.removeChild(e),!b.async&&n&&n()},c.setAttribute("src",r),c.alt=a&&a.alt||"",b.titleTag&&s&&(c.title=s),l.appendChild(c),b.async&&n&&n()}}function X(){return M(o+1)}function D(){return M(o-1)}function M(e,t){return!n&&0<=e&&e=k.length?(b.animation&&O("right"),!1):(q(o=e,function(){z(o),V(o)}),R(),b.onChange&&b.onChange(o,k.length),!0)}function O(e){l.className="bounce-from-"+e,setTimeout(function(){l.className=""},400)}function R(){var e=100*-o+"%";"fadeIn"===b.animation?(l.style.opacity=0,setTimeout(function(){m.transforms?l.style.transform=l.style.webkitTransform="translate3d("+e+",0,0)":l.style.left=e,l.style.opacity=1},400)):m.transforms?l.style.transform=l.style.webkitTransform="translate3d("+e+",0,0)":l.style.left=e}function z(e){e-o>=b.preload||q(e+1,function(){z(e+1)})}function V(e){o-e>=b.preload||q(e-1,function(){V(e-1)})}function U(e,t,n,o){e.addEventListener?e.addEventListener(t,n,o):e.attachEvent("on"+t,function(e){(e=e||window.event).target=e.target||e.srcElement,n(e)})}function W(e,t,n,o){e.removeEventListener?e.removeEventListener(t,n,o):e.detachEvent("on"+t,n)}function G(e){return document.getElementById(e)}function J(e){return document.createElement(e)}return[].forEach||(Array.prototype.forEach=function(e,t){for(var n=0;n","http://www.w3.org/2000/svg"===(e.firstChild&&e.firstChild.namespaceURI)}(),m.passiveEvents=function i(){var e=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e=!0}});window.addEventListener("test",null,t)}catch(n){}return e}(),function a(){if(r=G("baguetteBox-overlay"))return l=G("baguetteBox-slider"),u=G("previous-button"),c=G("next-button"),void(d=G("close-button"));(r=J("div")).setAttribute("role","dialog"),r.id="baguetteBox-overlay",document.getElementsByTagName("body")[0].appendChild(r),(l=J("div")).id="baguetteBox-slider",r.appendChild(l),(u=J("button")).setAttribute("type","button"),u.id="previous-button",u.setAttribute("aria-label","Previous"),u.innerHTML=m.svg?f:"<",r.appendChild(u),(c=J("button")).setAttribute("type","button"),c.id="next-button",c.setAttribute("aria-label","Next"),c.innerHTML=m.svg?g:">",r.appendChild(c),(d=J("button")).setAttribute("type","button"),d.id="close-button",d.setAttribute("aria-label","Close"),d.innerHTML=m.svg?p:"×",r.appendChild(d),u.className=c.className=d.className="baguetteBox-button",function n(){var e=m.passiveEvents?{passive:!1}:null,t=m.passiveEvents?{passive:!0}:null;U(r,"click",x),U(u,"click",E),U(c,"click",C),U(d,"click",B),U(l,"contextmenu",A),U(r,"touchstart",T,t),U(r,"touchmove",N,e),U(r,"touchend",L),U(document,"focus",P,!0)}()}(),S(e),function s(e,a){var t=document.querySelectorAll(e),n={galleries:[],nodeList:t};return w[e]=n,[].forEach.call(t,function(e){a&&a.filter&&(y=a.filter);var t=[];if(t="A"===e.tagName?[e]:e.getElementsByTagName("a"),0!==(t=[].filter.call(t,function(e){if(-1===e.className.indexOf(a&&a.ignoreClass))return y.test(e.href)})).length){var i=[];[].forEach.call(t,function(e,t){var n=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1,H(i,a),I(t)},o={eventHandler:n,imageElement:e};U(e,"click",n),i.push(o)}),n.galleries.push(i)}}),n.galleries}(e,t)},show:M,showNext:X,showPrevious:D,hide:j,destroy:function e(){!function n(){var e=m.passiveEvents?{passive:!1}:null,t=m.passiveEvents?{passive:!0}:null;W(r,"click",x),W(u,"click",E),W(c,"click",C),W(d,"click",B),W(l,"contextmenu",A),W(r,"touchstart",T,t),W(r,"touchmove",N,e),W(r,"touchend",L),W(document,"focus",P,!0)}(),function t(){for(var e in w)w.hasOwnProperty(e)&&S(e)}(),W(document,"keydown",F),document.getElementsByTagName("body")[0].removeChild(document.getElementById("baguetteBox-overlay")),w={},h=[],o=0}}})
タイトルとURLをコピーしました