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:ビューはもうちょっと特殊なことをしてます。