Last active
August 29, 2015 14:02
-
-
Save Leko/6c57c365ce4736078ff6 to your computer and use it in GitHub Desktop.
PHPでもBackboneちっくなオブザーバパターンを利用できるトレイト
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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!! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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