そうだ、コアを読もう6 -CakePHP-

Behavior と言うのは便利だ。

あたかも Model に存在する関数のように $this->Model->behaviorMethod() と使うことが出来る。

 

しかしこの「存在する関数のように」というのが意外と落とし穴だった。

実際は Model には「存在していない」と言うことだ。

そんなの当たり前じゃないかといわれるかもしれないが、実際に次のようなケースで悩んだことがある。

ある Class にメソッドが存在するかどうかは method_exists と言う PHP の関数を使うのだけれど、これを Model で使用している Behavior に対しては使うことが出来ない。

Behavior にメソッドが存在するかどうかを確認する必要があるのか?という話はとりあえず置いておいて、Behavior 内のメソッドに対して method_exists を使うと必ず false が帰ってくる。

 使い方としてはいくつかあると思う。

Model 内で method_exists ($this, 'behaviorMethod') や、

Controller 内で method_exists ($this->Model, 'behaviorMethod') と言う感じだろうか。

そのどちらにせよ、メソッドが存在していようがいまいが、必ず false が帰ってくる。

これは Model に存在する関数のように振舞うが、実際は存在していないと言うことを意味する。

 

じゃぁ、どうやって存在するかチェックすればいいのだ?

まさか想定されていないなんてことはないはずだ。たぶん。

 

と言うことでコアの Model フォルダを覗いてみよう。

せっかくなので最新の 2.5.2 のバージョンを使う。

さてまずは Model.php から。

まず先頭の方で App::uses() をしていて、ぱっと見 Behavior に関係してそうなのが、25~26行目

App::uses('BehaviorCollection', 'Model');
App::uses('ModelBehavior', 'Model');

呼び出されているクラスは後で見るとして、続いてざっと目を通していくと、490行目に

public $Behaviors = null;

という、いかにも怪しい宣言が出てくる。

じゃぁ、こいつで検索してみましょうかね。

 

671行目から始まる __construct() 内で 725行目に

$this->Behaviors = new BehaviorCollection();

とクラスを呼び出してあり、さらに747行目で init() している。

さらに読んでいくと、794行目にマジックメソッド __call() 内に発見する。

これが「存在する関数のように」振舞うが、method_exists() で false を返す訳だ。

確証は得たものの求めるものはまだ見つかっていない。

さらに読み進めよう。

 

次に出てくるのが 1466~1472行目。

public function hasMethod($method) {
    if (method_exists($this, $method)) {
        return true;
    }

    return $this->Behaviors->hasMethod($method);
}

 これは非常に臭いですね。

まず Model 内に指定された関数があるか調べる、それでも見つからなければ BehaviorCollection::hasMethod() を呼び出している。

おそらく BehaviorCollection::hasMethod() 内でまた関数を捜索し、その結果を返すのだろう。

 

ひとまず結論。

Behavior に関数に関数があるかどうかを調べるには hasMethod()を使うべし

 

せっかくなので BehaviorCollection::hasMethod() も見てみよう。

BehaviorCollection は Model.php と同じフォルダ内にある。

その261~275行目

public function hasMethod($method, $callback = false) {
    if (isset($this->_methods[$method])) {
        return $callback ? $this->_methods[$method] : true;
    }
    foreach ($this->_mappedMethods as $pattern => $target) {
        if (preg_match($pattern . 'i', $method)) {
            if ($callback) {
                $target[] = $method;
                return $target;
            }
            return true;
        }
    }
    return false;
}

まず $this->_methods に指定されたメソッドがあるか調べている。

三項演算子 の結果を return しているが、 $callback が true だとメソッドを、 false だと true を返すようだ。

 

メソッドを返すってどういうことかというと、たとえば以下のように呼び出す。

$behaviorMethod = $this->hasMethod('hogeMethod', true);

if ($behaviorMethod !== false) {

    $behaviorMethod();

}

もしこのメソッドが引数をとるのなら

$behaviorMethod(1, 'foo', array('bar'));

みたいな感じにする。

ところで、 $behaviorMethod(); と言う呼び出し方を気持ち悪いと思う人もいるかもしれないけれども、クラスのメンバにするほど使用頻度が高くないような一時的なメソッドメソッド内などに作るときに良く書く。

$method = function ($val) {

    return 'mumeikansu!' . $val . '!!';

};

echo $method('dayo');

とすると、mumeikansu!dayo!! と出力される。

 

話がそれたので、続きに戻ろう。

$this->_methods にセットされていなければ、 $this->_mappedMethods 内を捜索するらしい。

$this->_mappedMethods を検索してみると、出現箇所は3箇所だけ。

そのうち1箇所はここ、もうひとつは宣言で、実質1箇所にしかない。

103行目から始まる load() 関数の 148行目。

だがしかし、結構ごちゃごちゃやっているので、ここ掘り返したら死にそうなのでざくっと。

呼び出された Behavior のオブジェクトが格納されていると思われる $this->_loaded をループで回し、メソッド名をキーとし、値に Behavior の別名と、そのメソッドの別名の配列を格納してある。

メソッド名をキーとしているので、別の Behavior に同じ名前のメソッドがあれば後から呼ばれた方で上書きされてしまう。

$this->_loaded は127~140行で、ごちゃごちゃとどうやらクラスの呼び出しをしているようだ。

$this->_methods については 150~167行目でセットしている。

  • メソッド名の先頭に _ が付いていないもの
  • すでに登録されていないもの
  • コールバック関数じゃないもの

の条件を満たすものが登録される。

 

$this->_mappedMethods は先に登録したメソッド後からロードされた Behavior のメソッドに上書きされるのに比べ、 $this->_methods は先に登録されたものがそのまま堅持される。

 

実際のところ、こんなところを気にしなくてもとりあえず hasMethod() を呼べば良いし、そもそもめったに Behavior のメソッドの有無を判定することはないと思う。

ただ、チームで開発していると、時々 Behavior が他の人によって削除されたり、名前が変わったりする可能性もあるので、そういう極々まれなケースをハンドリングする場合に必要になるようなならないような。