CodeIgniter3のM/V/Cの$thisは何者か

この記事はCodeIgniter Advent Calendar 2016の22日目です。

CodeIgniter3でのMVCはおまけです。ドキュメントのそこかしこにMVCアーキテクチャであるように書かれている気がしますが、おまけったらおまけなのです。

M/V/Cの$thisとは何者か

CodeIgniter3のアーキテクチャを語る上で、$thisは欠かせない存在です。$this->loadで読み込んだインスタンスは$thisにアサインされますが、Model / View / Controller のどこでも使えます。

viewではクラスを定義しないので意外に思われるかもしれませんが、viewの中でヘルパを読み込むのに$this->loadが使えます。実はマニュアルでは非推奨のように書かれているのですが、とにかく使えます。

<?php $this->load->helper('router'); ?>

MVCとは何ぞやという話は今は横に置かせてください。重要なことは、$this->loadで読み込んだインスタンスは$thisにアサインされ、M/V/Cのいずれでも使うことができる、ということです。
おかしな話です。M/V/Cの$thisはそれぞれ別のものではないのでしょうか。

それでは、それぞれの$thisは何者なのか、見ていきます。

コントローラの$this

Controllerの$thisは一番直観的です。つまり、$thisはそのクラスのインスタンスそのものです。ひねりはありません。

$this->loadコントローラの中でアサインされています。

$this->load =& load_class('Loader', 'core');

コントローラのコードは非常に簡潔で、メソッドは2つしかありません。はて、ではどうして$this->loadで読み込んだlibraryやmodelが$thisで使えるのでしょうか。

まず、get_instance()の返すものはControllerのインスタンスです

function &get_instance()
{
    return CI_Controller::get_instance();
}

そして、$this->load->library()などの処理を追っていくと、CI_Loaderはこのget_instance()で返すインスタンスのプロパティにロードしたものを突っ込んでいます

 // Instantiate the class
 $CI->$object_name = isset($config)
        ? new $class_name($config)
        : new $class_name();
コントローラはCI_Loaderの入れ物になっているのです。

モデルの$this

Modelの$thisはもちろんそのクラスのインスタンスですが、Controllerと違いひねりが加わっています。CI_Modelクラスの役目はたったひとつ、マジックメソッド__get()を定義しているだけです。
public function __get($key)
{
    return get_instance()->$key;
}

このコードはつまり、Modelに定義されていないプロパティについてはget_instance()がModelに成り代わるというものです。そして、get_instance()の返すものはControllerのインスタンスです。

Modelのプロパティを実質的にControllerが兼ねているのですが、__call()は定義していないのでメソッドは呼び出せません。
つまり、プロパティの見た目だけ実質Controllerです。そして、ControllerのプロパティはCI_Loaderの入れ物です。__get()を使っているので、Model内で新しくloadしたものも利用できます。

ビューの$this

Viewの$thisは、そもそもクラスを定義していないので何のクラスのインスタンスか分かりにくいです。しかし、この流れなら予想がつくと思います。そうです、この$thisはController――ではありません。CI_Loaderです。$this->load->view()ではCI_Loader::view()を呼び出しているからです。

Viewでvar_dump($this)してみましょう。

object(CI_Loader)[13]

しかし、var_dumpされた大量の変数の下のほうを見ていくと、loadしたオブジェクトがずらっと並んでいることに気がつくと思います。

先ほどの確認ではCI_LoaderがロードしたものはControllerに突っ込んでいました。CI_Loaderではなかったはずです。

これらは$this->load->view()が呼び出された後、ビューファイルをinclude()する前にget_instance()から$thisへコピーされたものです。

// This allows anything loaded using $this->load (views, files, etc.)
// to become accessible from within the Controller and Model functions.
$_ci_CI =& get_instance();
foreach (get_object_vars($_ci_CI) as $_ci_key => $_ci_var)
{
    if ( ! isset($this->$_ci_key))
    {
        $this->$_ci_key =& $_ci_CI->$_ci_key;
    }
}

結果としてControllerのパブリックプロパティにアクセスできます。しかも明示的に参照コピーをしています。Viewの$thisもプロパティの見た目は実質Controllerです。

ただ、Modelと違うのは、__get()ではなくコピーで行っているため、viewで$this->load->XXX()を呼び出しても基本的に$thisに追加はされません。viewでロードはできない、ということです。例外は$this->load->helper()で、これはインスタンス操作ではなく関数定義なので動いてしまいます。使えないほうが一貫性がありますが使えなくする方法がなく、やむなくドキュメントで非推奨扱いにしているのでしょう。

きっとこれこそCodeIgniter

Controllerがnewされてからメソッドが呼び出されるまで、プロパティに何かを入れるなど特殊な処理はしていません。CodeIgniter本体がコントローラに与えた追加の役割は、ローダの入れ物というだけです。

ということは、ViewとModelに成り代わるものはコントローラではなくCI_Loaderそのものでも良さそうです。そうすればCI_Loaderがロードの際に自分自身ではなくコントローラにインスタンスを突っ込んでいき、viewを読むときに改めて自分自身にコピーし直す、そんな非合理なこともなくなります。

おそらくですが、これはユーザがControllerのプロパティに設定したものをModelでもViewでも使えるようにするためと思います。単に使えるだけでなく、「Controllerのようにそのまま使える」、です。

これはMVCの考え方ではありません。M/V/Cが分離されていません。

しかし、CodeIgniterの特徴(not特長)としてよく挙げられるのが「ピュアなPHPのように書ける」ことです。つまりPHP4時代のような、1ファイルの上の方で処理を書いて下の方でHTML出力するような、昔の書き方です。

Controllerがファイルの上の方、Viewは下の方、Modelはその中間、というところでしょうか。そう考えると上記で追ってきた内容に一貫性が出てきます。

実際、M/V/Cの間でコードをコピペ移動したとしてもそれなりに動いてしまいます。MVCのフレーバーは残しつつ、コードの流儀を強制しない、それを支えているのがこの合理的とは言えない作りです。

CodeIgniter4では「フィーリングを引き継ぐ」というようなことですが、今風の作りも目指しています。このPHP4のようなフィーリングは残るのかどうか、あるいは残すべきなのかどうか。個人的には残ってほしいです。この「MVCは気持ちだけ」な状態がCodeIgniterの良いところと思います。

comments powered by Disqus