次世代CakePHPとも言うべきフレームワークLithiumのフィルタシステムを見てみました。
Lithiumは対象がPHP5.3以上ということで、5.3ならではの機能を活用したアーキテクチャになっています。中でも特徴的なのがフィルタシステムです。
全体のアーキテクチャとしては、CakePHPの流れを汲んで標準的なMVCフレームワークになっています。ただそれを実現する手段としてフィルタシステムを多用しています。これまでのフレームワークとは異なる点があり、いざフレームワークの動きを掴もうとすると戸惑います。
そこでLithiumのフィルタシステムをざっくりと見てみましょう。
サンプルソース
サンプルとして、SampleControllerとそのビューテンプレートを用意します。
フィルタの動きを見るだけなので、indexアクションでは、ログに__METHOD__を記録するだけです。
[app/controllers/SampleController.php]
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace app\controllers; class SampleController extends \lithium\action\Controller { public function index() { \lithium\analysis\Logger::debug( __METHOD__ ); } } [/php] <p>[app/views/sample/index.html.php]</p> <h1>Sample</h1> |
ログをファイル(app/resources/tmp/logs/debug.log)に出力するようにしておきます。
[app/webroot/index.php]
1 2 3 4 5 6 | // ログをファイルに出力 \lithium\analysis\Logger::config( array ( 'default' => array ( 'adapter' => 'File' ))); echo lithium\action\Dispatcher::run( new lithium\action\Request()); ?> |
これでブラウザから「http://localhost/sample」にアクセスすると以下の内容がログに記録されます。
2010-09-30 00:11:16 app\controllers\SamplesController::index
フィルタを追加する
では、Dispatcher#runメソッドにフィルタを追加してみましょう。
まず、フィルタをクロージャで定義します。(__invokeメソッドを定義したクラスでも可)
そしてフィルタを追加する際は、applyFilterメソッドを利用します。この際に、第一引数にフィルタを追加する対象のメソッド名を、第二引数にフィルタ(クロージャ)を指定します。
[app/webroot/index.php]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // フィルタを定義 $c = function ( $self , $params , $chain ) { \lithium\analysis\Logger::debug( 'Before' ); // 次フィルタを実行 $res = $chain ->next( $self , $params , $chain ); \lithium\analysis\Logger::debug( 'After' ); // 次フィルタの戻り値を返す return $res ; }; // フィルタを追加 lithium\action\Dispatcher::applyFilter( 'run' , $c ); echo lithium\action\Dispatcher::run( new lithium\action\Request()); |
フィルタの内容は単純で、lithium\action\Dispatcher::runの処理の前後でログに文字列を出力しています。
これを実行すると以下のログが出力されます。フィルタで定義したログ出力部分が実行されており、フィルタとして機能しているのが分かります。
2010-09-30 00:11:16 Before 2010-09-30 00:11:16 app\controllers\SamplesController::index 2010-09-30 00:11:16 After
フィルタ定義でのポイントは、$chain->nextの箇所です。LithiumのフィルタシステムはChain of Responsibilityパターンになっており、フィルタ自らが次ぎのフィルタを呼び出す構造になっています。
あくまでもフィルタ自身が次のフィルタを実行するかどうかを決定できるので、サンプルのように前後に処理を実行することもできますし、あえて次のフィルタを実行させないということもできます。
CakePHPのフックメソッド(beforeSave()/afterSave()等)をフィルタとして書くというように考えるとイメージしやすいかもしれません。フックメソッドと異なるのは、まずサブクラスと作ってメソッドを継承する必要が無いと言う点があります。さらに大きく違うのはフィルタは複数追加することできるので多彩な処理をプラグインのように加えることが可能という点です。
次は、複数のフィルタを追加してみます。
複数フィルタを追加する
3つのフィルタを追加します。フィルタの内容は単純で、文字列をログに出力するだけです。ただ、どのフィルタから出力された文字列かを識別するためにフィルタごとに番号を追加しています。
ここでは、applyFilterの第二引数に直接クロージャ定義を記載しています。
[app/webroot/index.php]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | $no = 1; lithium\action\Dispatcher::applyFilter( 'run' , function ( $self , $params , $chain ) use ( $no ) { \lithium\analysis\Logger::debug( 'Before' . $no ); $res = $chain ->next( $self , $params , $chain ); \lithium\analysis\Logger::debug( 'After' . $no ); return $res ; }); $no = 2; lithium\action\Dispatcher::applyFilter( 'run' , function ( $self , $params , $chain ) use ( $no ) { \lithium\analysis\Logger::debug( 'Before' . $no ); $res = $chain ->next( $self , $params , $chain ); \lithium\analysis\Logger::debug( 'After' . $no ); return $res ; }); $no = 3; lithium\action\Dispatcher::applyFilter( 'run' , function ( $self , $params , $chain ) use ( $no ) { \lithium\analysis\Logger::debug( 'Before' . $no ); $res = $chain ->next( $self , $params , $chain ); \lithium\analysis\Logger::debug( 'After' . $no ); return $res ; }); echo lithium\action\Dispatcher::run( new lithium\action\Request()); |
実行すると以下のログが出力されます。これを見ると$chain->next前の処理は、フィルタ追加順に実行され、$chain->next後の処理はその逆順で実行されているのが分かります。
2010-10-01 01:20:46 Before1 2010-10-01 01:20:46 Before2 2010-10-01 01:20:46 Before3 2010-10-01 01:20:46 app\controllers\SampleController::index 2010-10-01 01:20:46 After3 2010-10-01 01:20:46 After2 2010-10-01 01:20:46 After1
このように複数の処理を任意に追加できるのがフィルタシステムの特徴です。
フィルタシステム
ここまでフィルタがどのように動作するかを見てきました。
ではフィルタがどのように呼ばれているかを掴むためにフレームワークのソースを見てみましょう。
まずlithium\action\Dispatcher#runメソッドのソースを見てみましょう。
20行弱のソースなのですが、実質は3行しかありません。まず2行でパラメータのセットを行います。あとはstatic::_filter()を実行して終わりです。
lithium\action\Dispatcher#runメソッドとしての実質の処理(MVCの実行)は、static::_filter()の第3引数にあるクロージャの中に記載されています。
[libraries/lithium/action/Dispatcher.php]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public static function run( $request , array $options = array ()) { // パラメータセット $router = static :: $_classes [ 'router' ]; $params = compact( 'request' , 'options' ); // static::_filter()を実行 // クロージャの中身が、本当の処理 return static ::_filter( __FUNCTION__ , $params , function ( $self , $params ) use ( $router ) { $request = $params [ 'request' ]; $options = $params [ 'options' ]; if (( $result = $router ::process( $request )) instanceof Response) { return $result ; } $params = $self ::applyRules( $result ->params); if (! $params ) { throw new DispatchException( 'Could not route request' ); } $callable = $self ::invokeMethod( '_callable' , array ( $result , $params , $options )); return $self ::invokeMethod( '_call' , array ( $callable , $result , $params )); }); } |
では、次はstatic::_filter()のソースです。lithium\action\Dispatcherはlithium\core\StaticObjectを継承しているので、実際に実行されるのはlithium\core\StaticObject#_filterメソッドです。
第三引数の$callbackにlithium\action\Dispatcher#runメソッドで定義したクロージャが格納されています。applyFilterメソッドによってフィルタが追加されていれば、フィルタチェイン(フィルタリスト)の最後に追加され、Filters::run()でフィルタ実行が開始します。フィルタが無い場合は、直接$callbackを実行します。
つまりポイントは、lithium\action\Dispatcher#runメソッドの処理自体もフィルタとして扱われるということです。これによりフィルタチェインを順々に実行すれば、メソッド自身の処理が実行できるというわけです。
[libraries/lithium/core/StaticObject.php]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | protected static function _filter( $method , $params , $callback , $filters = array ()) { $class = get_called_class(); $hasNoFilters = empty ( static :: $_methodFilters [ $class ][ $method ]); if ( $hasNoFilters && ! $filters && !Filters::hasApplied( $class , $method )) { // フィルタが空ならそのまま$callbackを実行 return $callback ( $class , $params , null); } if (!isset( static :: $_methodFilters [ $class ][ $method ])) { static :: $_methodFilters += array ( $class => array ()); static :: $_methodFilters [ $class ][ $method ] = array (); } // $callbackをフィルタチェインの最後に追加する $data = array_merge ( static :: $_methodFilters [ $class ][ $method ], $filters , array ( $callback )); return Filters::run( $class , $params , compact( 'data' , 'class' , 'method' )); } |
最後にフィルタチェインを実行している\lithium\util\collection\Filters#runメソッドです。
最後の3行で、フィルタチェインの先頭にあるフィルタを実行して終了します。ここではあくまで先頭のフィルタだけを実行しています。それ以降のフィルタを実行するかどうかはフィルタ自身に委ねられています。
[libraries/lithium/util/collection/Filters.php]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public static function run( $class , $params , array $options = array ()) { $defaults = array ( 'class' => null, 'method' => null, 'data' => array ()); $options += $defaults ; $lazyFilterCheck = ( is_string ( $class ) && $options [ 'method' ]); if (( $lazyFilterCheck ) && isset( static :: $_lazyFilters [ $class ][ $options [ 'method' ]])) { $filters = static :: $_lazyFilters [ $class ][ $options [ 'method' ]]; unset( static :: $_lazyFilters [ $class ][ $options [ 'method' ]]); $options [ 'data' ] = array_merge ( $filters , $options [ 'data' ]); foreach ( $filters as $filter ) { $class ::applyFilter( $options [ 'method' ], $filter ); } } // 先頭のフィルタを実行 $chain = new Filters( $options ); $next = $chain -> rewind (); return $next ( $class , $params , $chain ); } |
フィルタ実行が可能なメソッド
このようにフィルタシステムでは、メソッド自身の処理をクロージャにして、フィルタとして動作させることで実現しています。つまりどのメソッドでもフィルタシステムが利用できるわけではなく、あらかじめそれを想定して実装されたメソッドのみにフィルタが適用できます。
lithium\core\StaticObjectもしくは\lithium\core\Object(同様のフィルタ実行メソッドがあります)を継承しており、メソッド内部でstatic::_filter()もしくは$this->_filter()を実行しているメソッドが対象となります。
具体的には、\lithium\action\Controller、\lithium\data\Model、\lithium\data\source\Database、\lithium\util\Validator、他といったクラスに該当メソッドがあります。クラス名を見るとCakePHPでフックメソッドが定義されていたクラスですね。
フィルタシステムは便利?
Lithiumのフィルタシステムを見てきました。なんとなくイメージできたでしょうか。
フレームワークの主要な箇所はフィルタシステムを利用しているので、ここをおさえておくと処理が追いやすくなります。(反対に理解しないと?になります。。。)
このようなフィルタシステムをフレームワークの中核に利用しているフレームワークは、PHPではあまりありませんでした。使ってみると上手く実装されているなあというのが率直な印象です。利用の仕方では柔軟な実装ができそうなので楽しみです。
ただ実際問題、不満というかマイナスな点もあります。
まず、理解しにくということ。フレームワークのソースを読むならCakePHPのようにフックメソッドになっている方がgrepもしやすいし、一目瞭然で分かります。
次に、フィルタチェインの中身が掴めないということ。フィルタが格納される変数をvar_dump()してもクロージャが入っていることしか分からず、どのフィルタが入っているのかが分かりません。特にlithium\core\StaticObjectのサブクラスでは、フィルタを様々な箇所で追加できるので、どのフィルタがどの順序で入っているかは掴んでおきたいところです。
最後に、フィルタならではの利用方法がまだ見えてません。CakePHPのフックメソッドは実用には十分なものでした。正直、私自身は実用でフィルタならではの有効性がそれほど見えていないところです。
今後、有効な利用方法を見ていきたいを思います。
PHP5.3を体感するフレームワーク
冒頭にも触れましたが、LithiumはPHP5.3の機能が多くの箇所で活用されています。
ここで取り上げたフィルタシステムはまさに5.3の機能を利用しているので、5.3を理解するにはもってこいの教材です。言語仕様と簡単なサンプルソースを触ったあとは、実際に動くシステムを触ることでさらに理解を深めることができます。
PHP5.3を体感したい方は、Lithiumに触れてみるのはいかがでしょうか。
=> Lithium
- Newer: スクリプト言語間における「lexical closure」の違い – PHPの場合
- Older: PHP5.3+古いCakePHPのDeprecated表示をPHPコードを書き換えずに抑制する
トラックバック:0
- このエントリーのトラックバックURL
- /blog/2010/10/lithium_filter_system.html/trackback
- Listed below are links to weblogs that reference
- Lithiumのフィルタシステム from Shin x blog