CodeIgniterにオレオレmatchboxを実装してみる、の巻。

Concrete5のカスタマイズをしていて、クラスのオーバーライドやパッケージ管理が本当に良くできてるなぁ、と感心したところで、
せっかくなのでSeezooにも同じような仕組みが作れないかな?と試行錯誤しました。

そこでMatchbox

CodeIgniterにはMatchboxという便利なものがあります。

これは各機能をモジュールとして開発ができるようになり、とても便利です。

これ使ってもいいなぁ、と思っていましたが、


Loaderクラスが既に拡張されている
アドレス/modules/モジュール名/ とかURIを占有するかもしれない


以上から、何となく相性が悪いかもしれないなぁということで、
勉強も含めて自分で作ってみることにしました。

まずパッケージディレクトリを探索し、無ければコアを見るという挙動

CodeIgniterでモデルやヘルパーをロードするには、


// モデルのロード
$this->load->model('モデル名');

// ヘルパのロード
$this->load->helper('ヘルパ名')

と書きますが、結局独自にこれらの挙動を制御するとなると、Loaderクラスの拡張が必須なわけです。

Seezooは既にSZ_Loaderと拡張済みなので、これらのメソッドもオーバーライドして拡張してみます。


// 拡張後のmodel()メソッド。CI_Loaderをオーバーライドしてます
function model($model, $name = '', $db_conn = FALSE)
{
if (is_array($model))
{
foreach($model as $babe)
{
$this->model($babe);
}
return;
}

if ($model == '')
{
return;
}

// 以下の処理でファイル名とパスが正規化されるので、一時変数に退避
$orig_model = $model;
$orig_name = $name;

// Is the model in a sub-folder? If so, parse out the filename and path.
if (strpos($model, '/') === FALSE)
{
$path = '';
}
else
{
$x = explode('/', $model);
$model = end($x);
unset($x[count($x)-1]);
$path = implode('/', $x).'/';
}

if ($name == '')
{
$name = $model;
$orig_name = $name;
}

if (in_array($name, $this->_ci_models, TRUE))
{
return;
}

$CI =& get_instance();
if (isset($CI->$name))
{
show_error('The model name you are loading is the name of a resource that is already being used: '.$name);
}

$model = strtolower($model);

// 追加部分:拡張先モデルファイルが無ければ、親クラスのmodel()メソッドに移る
if ( ! file_exists(SZ_EXT_PATH.'models/'.$path.$model.EXT))
{
parent::model($orig_model, $orig_name, $db_conn);
return;
}

if ($db_conn !== FALSE AND ! class_exists('CI_DB'))
{
if ($db_conn === TRUE)
$db_conn = '';

$CI->load->database($db_conn, FALSE, TRUE);
}

if ( ! class_exists('Model'))
{
load_class('Model', FALSE);
}

require_once(SZ_EXT_PATH.'models/'.$path.$model.EXT);

$model = ucfirst($model);

$CI->$name = new $model();
$CI->$name->_assign_libraries();

$this->_ci_models[] = $name;
}

追加部分で、もしも拡張先ファイルが無ければ、通常のCI_Lorder::model()に移行する、という単なるフックですね。
SZ_EXT_PATHは別でdefineした拡張ディレクトリ配置先のパスが入ります。

ヘルパも同じ手法で対応できました。*1

モデルやヘルパとかはこれでいいのですが・・・

Controllerはそれだけじゃない

コントローラは同じ方法では上手くいきません。当たり前なのですが。
コントローラはCIのルーティングとも密接に関連しているので、Routerも拡張しないといけません。

Seezooはこれまた既にRouterクラスを拡張していて、Controllerを3階層まで辿ってルーティングするようにしています。

ここまできて諦めるのも悔しいので、頑張って再拡張。以下、SZ_Routerのコードです。


// 拡張後の_validate_requestメソッド。CI_Router::_validate_requestをoverride
function _validate_request($segments)
{
// 拡張ロード先のコントローラがあるかどうかをフックする
$customed_segments = $this->_custom_validate_request($segments);
if (is_array($customed_segments))
{
            // もしフックルーティングでコントローラがセットされていれば、ここで終了
return $customed_segments;
}

        // 以下、今までのコード

_validate_request()で使用するコントローラをとメソッドを決めるのですが、その処理の前に_custom_validate_request()というメソッドを呼んでフックします。

で、フック先で拡張ディレクトリからコントローラを検索します。


    // フックして呼び出すメソッド。拡張先からコントローラのロードを試みる
function _custom_validate_request($segments)
{
if ( file_exists(SZ_EXT_PATH . 'controllers/' . $segments[0].EXT))
{
$this->set_directory_ext();
return $segments;
}

if ( is_dir(SZ_EXT_PATH . 'controllers/' . $segments[0]))
{
$this->set_directory_ext($segments[0]);
$segments = array_slice($segments, 1);

if (count($segments) > 0)
{
if (is_dir(SZ_EXT_PATH . 'controllers/' . $this->fetch_directory() . $segments[0]))
{
// Set temp directory_name
$tmp_dir = $this->fetch_directory() . $segments[0];
$deep_segments = array_slice($segments, 1);

if (count($deep_segments) > 0)
{
if ( file_exists(SZ_EXT_PATH . 'controllers/' . $tmp_dir . '/' . $deep_segments[0] . EXT))
{
$this->set_directory_ext($tmp_dir);
return $deep_segments;
}
else
{
$this->directory = '';
return FALSE;
}
}
else
{
// Does the requested controller exist in the sub-folder?
if ( ! file_exists(SZ_EXT_PATH.'controllers/'.$this->fetch_directory().$segments[0].EXT))
{
$this->directory = '';
return FALSE;
}
}
}
}
}
$this->directory = '';
return FALSE;
}

// コントローラを拡張先ディレクトリにセットする
function set_directory_ext($dir = FALSE)
{
$this->directory = '../' . SZ_EXT_DIR . 'controllers/';

if ($dir !== FALSE)
{
$this->directory .= $dir . '/';
}
}


やっていることは_validate_request()と同じですが、大事なのは戻り値です。

拡張コントローラが見つかれば、返却値はそのパスを取った配列、見つからなければFALSEを返します。
これにより、フックメソッドの結果が配列なら、そのままreturnしてルーティング完了。
FALSEなら、CIのデフォルトルーティングを再開する方式です。

あと、set_directory_ext()というメソッドも追加してますが、これにも理由があるのです。

CodeIgniterが最終的にコントローラをロードするのはコアファイルで、


system/codeigniter/CodeIgniter.php:line150くらい

// Load the local application controller
// Note: The Router class automatically validates the controller path. If this include fails it
// means that the default controller in the Routes.php file is not resolving to something valid.
if ( ! file_exists(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().EXT))
{
show_error('Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.');
}

としています。$RTRはRouterクラスのインスタンスで、それぞれfetch_directory()とfetch_class()でコントローラを決めています。
つまり、


この時点までに、Routerクラスに読み込めるコントローラのパスがセットされていればいい

わけです。そこで、set_directory_ext()でしかるべきパスをセットしておけば、コアをハックせずに拡張コントローラを読める設計です。

あとは、SZ_EXT_PATHを上手くdefineしてやれば、拡張パッケージだけ外に置くこともできます。

多分Matchboxも同じような事をしてるんだと思う。ソース読んでませんが。

と、いうわけで

プラグインはこれらの拡張ディレクトリに設置することで、CI、さらにはSeezooのコアにも影響を与えず、独自の開発ができるようになるはずです。
開発者も楽ですし、コアのバージョンアップも楽で、一石二鳥ですね。

まだベンチマークを取って無いのですが、体感速度はさほど変わらないような気がします。
メモリ使用量も問題なさそうです。


もう少し動作検証して、いけそうなら次のバージョンに含めます。

総括とか

ちょっとOverRideから外れてしまったけど、コアとプラグインの切り分けができたのでよしとしましょうか。


多分普通に実装するならこうなるだろう、という想像でやりました。
もっと良い方法がありそうな気もするけど、これくらいが自分の限界ですかね。


今まで色々拡張してきましたが、CodeIgniterはどんな要望にも応える仕組みが提供されていて、やっぱりすごいですね。
しかもコアをまったくハックしない。うん、素晴らしい。

*1:ビューはもうちょっと特殊なことをしてます。