RitoLabo

Laravelのイベント&リスナを使ってObserverパターンを実装する

  • 公開:
  • 更新:
  • カテゴリ: PHP Laravel
  • タグ: PHP,Laravel,5.5,5.4,5.3,Events,Listeners,Queues,dispatch,5.6,DesignPatterns,Observer

LaravelをはじめとするPHPフレームワークはとても効率的なコードライティングを提供してくれますが、少し気を緩めるとコントローラが肥大化してきて…アプリケーションの規模によってはコードを追うのも嫌になる。なんて事もしばしばあります。

コントローラはその名の通り司令塔として重要な役割を持っているわけですが、なるべくなら機能を独立させて少しでも司令塔の負担をなくしてあげたいところです。

そこで今回、満を持して投入するのが「オブザーバパターン」です。オブザーバパターンとは、デザインパターンの一つで、簡単に言うと監視される側と監視する側の関係を持つプログラム構造で、監視される側の変化(イベント=発行)を、監視する側(リスナー=購読)がキャッチして処理を行う。という流れになります。

という事で今回は、Laravelのイベント&リスナを使ってオブザーバパターンを実装してみたいと思います。この仕組みを使うと、処理の独立化や遅延処理など、結構便利な上に良い事が多く、上手く使いこなすと幸せになれます。

アジェンダ
  1. 開発環境
  2. 機能の決定
  3. イベント&リスナ登録
  4. イベント&リスナ生成
  5. イベントの定義
  6. リスナの定義
  7. 仕上げ前の下準備
  8. イベントの発行
  9. 動作確認

開発環境

今回の開発環境は以下の通りです。

  • 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.phpMakeTextListener.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文字のテキストを生成しています。

動作確認

これで全ての記述が完了しましたので、実際にブラウザからアクセスして動作確認を行いましょう。 処理の流れとしては、

  1. ブラウザからhttp://YOUR-DOMAIN/sample/eventsにアクセスする
  2. イベントが発火する
  3. laravel/storage/texts配下に、現在日時をファイル名としたテキストファイルが生成される
  4. 生成されたテキストファイルの中に100文字のランダム文字列が書き込まれる

となります。

ブラウザからアクセスした画面

storage
└─ texts
└─ 20171115-233354.txt

テキストファイルは生成されたでしょうか? さらに開いて中身を見てみると、100文字のテキストが書き込まれているはずです。

RpkGdPdtqJKWDwPNNTlK1sJRQyhDJGsN80MZF7ljYNp1IMVZMwxOkvMEY6VoEVnH0GP2aOElyaXEmlwUYAUdsyK4VDW96yv21suL

まとめ

以上で作業は完了となります。

今回のイベント&リスナを使うと処理が独立分離され、ソース構造が非常に簡潔になります。

そして、これらをもっともっと有用に稼働させる仕組みがあります。それは、イベントが発火した処理をさらにキューイングという遅延処理、分離処理に投入する事によって、非同期通信でのイベント処理を実装することもできます。
laravelキュー投入&ジョブ処理

イベント&リスナも、キュー&ジョブも便利なので是非試してみてください。