Lithiumコアライブラリのフィルターを見てみた

昨日の第49回PHP勉強会@関東 - events.php.gr.jpで、@yandoさんがLithiumの現状について発表されたそうで、そのスライドを見てみました。

LithiumはCake PHPの流れを汲むフレームワークですが、PHP 4対応を捨てて先進的な機能を盛り込んでいるので、symfony使いな私でも気になりまくりなフレームワークです。


で、昨日のスライドの中で、コアライブラリの機能を拡張する「フィルター」の話がありました。(スライド25、26ページ)
簡単に言うと、LithiumコアのObjectまたはStaticObjectクラスの派生クラスで、特定の様式(フィルターチェイン方式)
で実行されているメソッドは、applyFilterメソッドでクロージャを設定することでメソッドの動作をカスタマイズできる、ということです。

・メソッドのフィルターを追加するapplyFilterメソッド
http://rad-dev.org/lithium/source/libraries/lithium/core/Object.php

<?php
    public function applyFilter($method, $closure = null) {
        foreach ((array) $method as $m) {
            if (!isset($this->_methodFilters[$m])) {
                $this->_methodFilters[$m] = array();
            }
            $this->_methodFilters[$m][] = $closure;
        }
    }

・フィルター様式でメソッドを実行する_filterメソッド
http://rad-dev.org/lithium/source/libraries/lithium/core/Object.php

<?php
    protected function _filter($method, $params, $callback, $filters = array()) {
        list($class, $method) = explode('::', $method);

        if (empty($this->_methodFilters[$method]) && empty($filters)) {
            return $callback->__invoke($this, $params, null);
        }

        $f = isset($this->_methodFilters[$method]) ? $this->_methodFilters[$method] : array();
        $items = array_merge($f, $filters, array($callback));
        return Filters::run($this, $params, compact('items', 'class', 'method'));
    }

この_filterメソッドでは最終的にFilters::run()が実行されています。Filtersクラスはこのようなフィルターチェーンの実行用のユーティリティクラスで、パラメーターで渡された複数のクロージャに対してその場でチェーン(コレクション)を生成して実行します。

  • Chain of Responsibilityパターンっぽい実装のようです


実際にフィルター様式でメソッドを実行している例:MySQLアダプターの_executeメソッド
http://rad-dev.org/lithium/source/libraries/lithium/data/source/database/adapter/MySql.php

<?php
    protected function _execute($sql, $options = array()) {
        $defaults = array('buffered' => true);
        $options += $defaults;

        $params = compact('sql', 'options');
        $conn =& $this->_connection;

        return $this->_filter(__METHOD__, $params, function($self, $params, $chain) use (&$conn) {
            extract($params);
            $func = ($options['buffered']) ? 'mysql_query' : 'mysql_unbuffered_query';
            $resource = $func($sql, $conn);

            if (!is_resource($resource)) {
                list($code, $error) = $self->error();
                throw new Exception("$sql: $error", $code);
            }
            return $resource;
        });
    }


symfonyではEventDispatherの機能があって、notify型、notifyUntil型、filter型という3種類のイベントがありますが、Lithiumのフィルターはこれらとは少し違いますね。
チェーンの各要素(クロージャ)自身が次の要素を呼び出します。次の要素を呼び出すパラメーターを加工することもできます。また、次の要素を呼び出さずにチェーンを終了するということもできます。(できると思います Chain of Responsibility的に)

「次の」という部分ですらチェーンの要素自身の問題なので、ここを上手くコントロールするとトリッキーなこともできそうですね。
(時間ができたら試してみたいと思います)




というわけで、ソースを眺めただけで実際に動かしたりしていないのですが、この部分だけをとってもLithiumよくできてるな!と思いました。Symfony 2とはまた違った面白さがありますね。



P.S.
これからのフレームワークのソースを読みこなすには、PHP 5.3の知識が必須ですね!