Laravelのイベント&リスナを使ってObserverパターンを実装する
- 公開日
- 更新日
- カテゴリ:Laravel
- タグ:PHP,Laravel,Events,Listeners,Queues

Laravel をはじめとする PHP フレームワークはとても効率的なコードライティングを提供してくれますが、少し気を緩めるとコントローラが肥大化してきて…アプリケーションの規模によってはコードを追うのも嫌になる。なんて事もしばしばあります。
コントローラはその名の通り司令塔として重要な役割を持っているわけですが、なるべくなら機能を独立させて少しでも司令塔の負担をなくしてあげたいところです。
そこで今回、満を持して投入するのが「オブザーバパターン」です。オブザーバパターンとは、デザインパターンの一つで、簡単に言うと監視される側と監視する側の関係を持つプログラム構造で、監視される側の変化(イベント=発行)を、監視する側(リスナー=購読)がキャッチして処理を行う。という流れになります。
という事で今回は、Laravel のイベント&リスナを使ってオブザーバパターンを実装してみたいと思います。この仕組みを使うと、処理の独立化や遅延処理など、結構便利な上に良い事が多く、上手く使いこなすと幸せになれます。
Contents
開発環境
今回の開発環境は以下の通りです。
- Linux CentOS7
- Apache 2.4
- PHP 7.2/7.1
- Laravel
Laravel のバージョンについては、5.6/5.5/5.4/5.3 にて動作確認済みです。
Laravel のルートディレクトリを「laravel/」とします。
上記環境でなくても(例えば XAMPP など)、artisan コマンドが叩ける環境であれば OK です。
機能の決定
まずは、「どんな機能を作るか」を先に決めます。
「最初に機能を決めなければ、そもそもオブザーバパターンのロジックは作れない」
というのはこの際置いておいたとして、Laravel では先にイベント名とリスナ名を決めておくと手順として良いです。
今回は簡単に、
「アクセスをトリガー(イベント)として、テキストファイルを生成する」
こんな感じの処理でいきたいと思います。
という事で、イベントは「AccessDetection イベント」、リスナは「MakeText リスナ」にしました。
イベント&リスナ登録
それではここから手を動かしていきます。まずは、イベントとリスナを登録します。
「登録」というのは、Laravel のサービスプロパイダにイベントとリスナを登録する。という事です。
まだ実態(ソースやファイル)が無いのに登録とは…と思うでしょうが、手順に若干の違和感があるにせよ、Laravel 先生からはこの手順で最高の恩恵を受けられますので、お付き合いください。(結果は後からついてくる!)
そして前述した「先に名前を」というのはこの為でした。
イベントとリスナは
laravel/app/Providers/EventServiceProvider.php
で管理されています。
app
├─ Providers
│ ├─ AppServiceProvider.php
│ ├─ AuthServiceProvider.php
│ ├─ BroadcastServiceProvider.php
│ ├─ EventServiceProvider.php
│ └─ RouteServiceProvider.php
このファイルを開いて以下のように記述します。
初期のソース
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\Event' => [
'App\Listeners\EventListener',
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
parent::boot();
//
}
}
↓ここに AccessDetection イベントと MakeText リスナを登録します。
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\Event' => [
'App\Listeners\EventListener',
],
// アクセス時にイベントを発行する側
'App\Events\AccessDetection' => [
// テキストを生成&書き込みを行うリスナー側
'App\Listeners\MakeTextListener',
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
parent::boot();
//
}
}
これでイベント&リスナーの登録は完了です。
イベント&リスナ生成
ここでようやく、イベントとリスナのファイルを作成します。 laravel ルートディレクトリに移動し、以下の artisan コマンドを叩きます。
# laravel ルートディレクトリへ移動
cd /path/to/laravel
# artisan コマンドでイベントとリスナを生成する
php artisan event:generate
# 実行結果
[demo@localhost laravel]$ php artisan event:generate
Events and listeners generated successfully!
laravel/app配下に Events ディレクトリと Listeners ディレクトリが生成され、AccessDetection.php と MakeTextListener.php が生成されます。
app
├─ Events
│ ├─ AccessDetection.php
│ └─ Event.php
├─ Listeners
│ ├─ EventListener.php
│ └─ MakeTextListener.php
イベントの定義
まずはイベントから定義していきます。
laravel/app/Events/AccessDetection.php を開いて、三か所追記します。
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class AccessDetection
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $param; // ← ココ
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($value) // ← ココ
{
$this->param = $value; // ← ココ
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
ここで記述しているのは、イベントを発行した際に値を受け渡す流れです。
メンバ変数「 $param 」を定義して、コンストラクタで引数「 $value 」を受け取ってそこへ格納している。という単純なものになっています。
リスナの定義
続いて、リスナーを定義します。
laravel/app/Listeners/MakeTextListener.php を開いて、handle() メソッドに以下を記述します。
<?php
namespace App\Listeners;
use App\Events\AccessDetection;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class MakeTextListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param AccessDetection $event
* @return void
*/
public function handle(AccessDetection $event)
{
// テキストファイル作成
$file = sprintf('%s/%s.txt', storage_path('texts'), date('Ymd-His'));
touch($file);
// 書き込み
$current = file_get_contents($file);
$current .= $event->param;
file_put_contents($file, $current);
}
}
リスナ定義 ここも大した事は書いていなくて、テキストファイルを作成する処理と、そこへ書き込みを行う処理になります。
イベントでキャッチさせた「param」は「 $event->param 」のようにしてここで使う事が出来ます。
仕上げ前の下準備
ここで一旦イベント処理から離れて、コントローラなどの下準備を行います。
最初に
「アクセスをトリガーとして、テキストファイルを生成する」
と決めた通り、ここでは「アクセスしたら発火」という流れにしますので、軽くその辺を準備します。
簡単なコントローラとビューを用意してルーティングを行います。(あくまでも一連の流れを重視したいので、ルーティングでイベント処理はしません。)
アクセスできれば良いので、コントローラにはビューのみを渡し、ビューには特に特別な事は書きませんので、ここはダイジェストでソースだけを載せます。
- ルーティング laravel/routes/web.php
-
Route::get('sample/events', 'SampleController@events');
- コントローラ laravel/app/Http/Controllers/SampleController.php
-
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class SampleController extends Controller { public function events() { return view('sample_events'); } }
- ビュー laravel/resources/views/sample_events.blade.php
-
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Sample Events</title> </head> <body> <p> Event test!! </p> </body> </html>
これで http://XXX.com/sample/events にアクセスしたら「Event test!!」というテキストが表示される一連の流れが出来上がりました。
最後に、生成したテキストファイルを格納するためのディレクトリを laravel/storage配下へ「texts 」というディレクトリを作成してください。(権限設定を忘れずに)
イベントの発行
最後の仕上げです。ここまで実装してきたイベント処理を発火させる記述を追加します。
先ほど作成したコントローラに以下を追記します。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
// イベントをuseする
use App\Events\AccessDetection;
class SampleController extends Controller
{
public function events()
{
// イベントをディスパッチする
event(new AccessDetection(str_random(100)));
return view('sample_events');
}
}
記述した部分を解説します。
// イベントを use する
use App\Events\AccessDetection;
イベントファイルを use しています。
// イベントをディスパッチする
event(new AccessDetection(str_random(100)));
event ヘルパでイベントクラス(AccessDetection)のインスタンスを渡しています。
一般に、イベントをディスパッチすると言いますが、要はここでイベントを発火させています。
ちなみに「str_random(100) 」は、テキストへ書き込む用の文字列です。ランダムに 100 文字のテキストを生成しています。
動作確認
これで全ての記述が完了しましたので、実際にブラウザからアクセスして動作確認を行いましょう。 処理の流れとしては、
- ブラウザから http://YOUR-DOMAIN/sample/events にアクセスする
- イベントが発火する
- laravel/storage/texts配下に、現在日時をファイル名としたテキストファイルが生成される
- 生成されたテキストファイルの中に 100 文字のランダム文字列が書き込まれる
となります。
storage
└─ texts
└─ 20171115-233354.txt
テキストファイルは生成されたでしょうか? さらに開いて中身を見てみると、100 文字のテキストが書き込まれているはずです。
RpkGdPdtqJKWDwPNNTlK1sJRQyhDJGsN80MZF7ljYNp1IMVZMwxOkvMEY6VoEVnH0GP2aOElyaXEmlwUYAUdsyK4VDW96yv21suL
まとめ
以上で作業は完了となります。
今回のイベント&リスナを使うと処理が独立分離され、ソース構造が非常に簡潔になります。
そして、これらをもっともっと有用に稼働させる仕組みがあります。それは、イベントが発火した処理をさらにキューイングという遅延処理、分離処理に投入する事によって、非同期通信でのイベント処理を実装することもできます。
laravel キュー投入&ジョブ処理
イベント&リスナも、キュー&ジョブも便利なので是非試してみてください。