Skip to content

Instantly share code, notes, and snippets.

@Leko
Last active August 29, 2015 14:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Leko/6c57c365ce4736078ff6 to your computer and use it in GitHub Desktop.
Save Leko/6c57c365ce4736078ff6 to your computer and use it in GitHub Desktop.
PHPでもBackboneちっくなオブザーバパターンを利用できるトレイト
<?php
namespace Observer;
require_once __DIR__.'/model.php';
/**
* FuelのModelクラスを継承し(という想定で => 継承枠を開けて)
* 更にEventModelの機構を使用するシンプルなモデル
*/
class Model/* extends \Model*/
{
use EventModel;
}
class Process
{
private static $model;
private static $callCount = 0;
public static function _init()
{
self::$model = new Model(array(
'a' => 1,
'b' => 2,
));
// モデルの全てのプロパティの変更を監視
self::$model->on('change', function($model) {
self::$callCount++;
// 2回呼び出されたらイベントを解除する
if(self::$callCount >= 2) {
$model->off();
}
echo "`change` triggered!! ".self::$callCount." times!!\n";
});
// モデルの全てのプロパティの変更を「1度だけ」監視
self::$model->once('change', function($model) {
echo "`change` triggered ONCE!!\n";
});
// モデルのaプロパティのみ監視
self::$model->on('change:a', function($model, $new_value) {
echo "`change:a` triggered!!: $new_value\n";
});
}
public function execute()
{
// プロパティへの代入でもイベント発火
self::$model->a = 'foo';
// 値が同じならイベントは発生しない
self::$model->a = 'foo';
// set()を明示的に呼び出しても発火
self::$model->set('b', 'bar');
// set()には連想配列も渡せる
self::$model->set(array(
'c' => 'buzz',
));
}
}
Process::_init();
$process = new Process();
$process->execute();
// => `change` triggered!! 1 times!!
// => `change` triggered ONCE!!
// => `change:a` triggered!!: foo
// => `change` triggered!! 2 times!!
<?php
namespace Observer;
require_once __DIR__.'/observe.php';
/**
* データのモデリングを行うトレイト。
* イベント駆動型のスタイルができる。
*/
trait EventModel
{
// トレイト(Observer)を使用
use Observer;
/**
* モデル属性値はメソッドとぶつからないために、この中に格納する
* @var array
*/
protected $attributes = array();
/**
* 連想配列で任意のプロパティを追加することができる
*
* @param array $attrs
*/
public function __construct($attrs = array())
{
foreach($attrs as $name => $value) {
$this->set($name, $value);
}
}
/**
* マジックメソッド。
*
* こいつを設定しておくことで、
* `$model->hoge = 'foo';`
* のようにプロパティに直接代入されても、setメソッドにフックすることができる。
*
* @param string $property 代入を行うプロパティ
* @param mixed $value 代入された値
*/
public function __set($property, $value)
{
$this->set($property, $value);
}
/**
* マジックメソッド。
*
* こいつを指定しておくことで、$attributesに格納されているプロパティに
* `$model->foo`という形で自然にアクセスできるようになる。
*
* @param string $property
* @return mixed $attributes[$property]の値、存在しないキーならnull
*/
public function __get($property)
{
if(array_key_exists($property, $this->attributes)) {
return $this->attributes[$property];
} else {
return null;
}
}
/**
* 明示的に値の設定を行う。
* マジックメソッド__setだと気持ち悪いと思ったらこちらのみ使用する規約にするなど。
*
* プロパティを変更した前後で値が変わっていた場合、`change`と`change:$property`イベントを発火する。
* changeイベントで呼び出されるコールバックは、`モデル`, `setに渡されたオプション`が引数として渡される。
* change:$propertyイベントで呼び出されるコールバックは、`モデル`, `新しい値`, `setに渡されたオプション`が引数として渡される。
*
* @param mixed $property stringならプロパティ名として処理、arrayなら連想配列として複数の"プロパティ => 値"として処理
* @param mixed $value 設定する値。
* @param array $options
*/
public function set($property, $value = null, $options = array())
{
// 連想配列で指定されたらプロパティ名, 値のsetに分解
if(is_array($property)) {
foreach($property as $key => $value) {
$this->set($key, $value);
}
// プロパティ名, 値で指定された
} else {
// 存在しないプロパティはnullで初期化
if(!isset($this->attributes[$property]))
$this->attributes[$property] = null;
$old_value = $this->attributes[$property];
$this->attributes[$property] = $value;
// 値が変更されたらchangeイベントを発火する
if($old_value !== $value) {
$this->trigger('change', $this, $options);
$this->trigger('change:'.$property, $this, $this->attributes[$property], $options);
}
}
}
}
<?php
namespace Observer;
/**
* カスタムイベントの設定・監視・発火を行うトレイト
*/
trait Observer
{
/**
* 監視しているイベント一覧
* @var array
*/
protected $_events = array();
/**
* 一度だけ監視するイベント一覧
* @var array
*/
protected $_once_events = array();
/**
* イベントを設定する
*
* @param string $event_name 設定するイベント名(任意)
* @param callable $callback 呼び出すコールバック関数
* @param object $context コールバック関数のコンテキストを上書きする
* @return void
*/
public function on($event_name, $callback, $context = null)
{
$this->_add($this->_events, $event_name, $this->_bind($callback, $context));
}
/**
* 一度だけ監視するイベントを設定する
*
* @param string $event_name 設定するイベント名(任意)
* @param callable $callback 呼び出すコールバック関数
* @param object $context コールバック関数のコンテキストを上書きする
* @return void
*/
public function once($event_name, $callback, $context = null)
{
$this->_add($this->_once_events, $event_name, $this->_bind($callback, $context));
}
/**
* バインドされたイベントの監視を解除する
*
* ## 引数と動作
* | 引数の組み合わせ | $event_name | $callback |
* |----------------|-----------------------|-------------------------------|
* | null , null | 全てのイベントを解除 | - |
* | 非null , null | 指定されたイベントのみ解除 | そのイベントのコールバック全て解除 |
* | 非null , 非null | 指定されたイベントのみ解除 | 指定されたコールバックのみ削除 |
*
* @param string $event_name 監視を解除するイベント名
* @param callable $callback 監視を解除するコールバック関数
* @return void
*/
public function off($event_name = null, $callback = null)
{
$this->_remove($this->_events, $event_name, $callback);
$this->_remove($this->_once_events, $event_name, $callback);
}
/**
* イベントを発火する
*
* @param string $event_name 発火するイベント名
* @param mixed $args コールバックに与える引数(可変長)
* @return void
*/
public function trigger($event_name/*, $args...*/)
{
$args = array_slice(func_get_args(), 1);
$this->_trigger($this->_events, $event_name, $args);
// NOTE: onceは一度発火したら削除する
// FIXME: 1個ずつpopして発火した方が良い?複数のイベントが設定されてた時の動作がややこしそう。
$this->_trigger($this->_once_events, $event_name, $args);
$this->_once_events[$event_name] = null;
}
/**
* コールバック関数にコンテキストをバインドする。
* コールバック関数がクロージャのときのみバインドを行う。
*
* @param callable $fn コールバック関数
* @param object $context バインドするコンテキスト
* @return callable クロージャならコンテキストをバインドされたクロージャ、それ以外なら$fnがそのまま返る
*/
protected function _bind(callable $fn, $context)
{
if(is_object($context) && $context instanceof \Closure) {
$fn = $fn->bind($context);
}
return $fn;
}
/**
* イベント一覧にコールバックを追加する
*
* @param array $events コールバックを格納する配列
* @param string $event_name 監視するイベント名
* @param callable $callback 呼び出すコールバック関数
* @return void
*/
protected function _add(&$events, $event_name, callable $callback)
{
// NOTE: 各イベントリスナーは空配列で初期化
if(!isset($events[$event_name]))
$events[$event_name] = array();
$events[$event_name][] = $callback;
}
/**
* イベント一覧からコールバックを削除する
*
* @param array $events コールバックを格納する配列
* @param string $event_name 監視するイベント名
* @param callable $callback 呼び出すコールバック関数かnull
* @return void
*/
protected function _remove(&$events, $event_name, $callback)
{
$delete_all_events = is_null($event_name) ? true : false;
$delete_all_listeners = is_null($callback) ? true : false;
// NOTE: バインドされた全てのイベントを破棄
if($delete_all_events) {
$events = array();
// NOTE: $event_nameにバインドされた全てのリスナーを破棄
} elseif($delete_all_listeners) {
unset($events[$event_name]);
// NOTE: $event_nameにバインドされた$callbackのみを破棄
} else {
if(isset($events[$event_name])) {
$idx = array_search($callback, $events[$event_name]);
array_splice($events[$event_name], $idx, 1);
}
}
}
/**
* イベントの発火を行う
*
* @param array $events イベントを格納している配列
* @param string $event_name 発火するイベント名
* @param array $args コールバックに与える引数
* @return void
*/
protected function _trigger($events, $event_name, $args)
{
if(!isset($events[$event_name])) return;
foreach($events[$event_name] as $fn)
call_user_func_array($fn, $args);
}
// public function listenTo($other, $event_name, $callback)
// {
// }
// public function listenToOnce($other, $event_name, $callback)
// {
// }
// public function stopListening($other = null, $event_name = null, $callback = null)
// {
// }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment