HABTMの中間テーブルのモデルがAppModelになる問題再び
や、、、奴が帰ってきたぜ!
CakePHP1の頃に一度は解決した問題、また別の場所で勃発しました。。。
「HABTMの中間テーブルがAppModelオブジェクトになってしまう問題の対応」
今回は、HTBTMを持ってるモデルでfind()を実行して(recursive=1)、その後に別の場所で中間テーブルのモデルをClassRegistry::initで取得してメソッドを実行したら、そんなメソッドありませんというエラーがでて、何故かそれがAppModelクラスのインスタンスだったという流れ。
デバッグにはかなり時間がかかりましたが、原因を特定しました。
まずは結論から。
今回もHABTMのwithで指定し忘れてたのが原因だったので、前回と同様に、withキーに中間テーブルのクラス名の文字列を入れて対応完了。
<?php class Division extends AppModel { var $hasAndBelongsToMany = array( 'User' => array( 'className' => 'User', 'joinTable' => 'divisions_users', 'foreignKey' => 'division_id', 'associationForeignKey' => 'user_id', 'unique' => true, 'conditions' => '', 'fields' => '', 'with' => 'DivisionsUser', //ここが重要 'order' => '', 'limit' => '', 'offset' => '', 'finderQuery' => '', 'deleteQuery' => '', 'insertQuery' => '' ) ); }
AppModelが使われる原因
一度モデルクラスのインスタンスを生成してしまうと、ClassRegistry::init()では再度生成せずにキャッシュとして持ってる最初のインスタンスを返します。つまり、最初にAppModelとしてインスタンスが作られてしまうと、ClassRegistry::removeObject()しない限りは目的のインスタンスが取得できなくなります。
CakePHP2でも、モデルのコンストラクタでアソシエーションを構築する際に、HABTMなどの設定情報を走査して、足りない情報を付与します(Model::_generateAssociation())。このときに、withに何もキーがないとDynamicWithにフラグを立てるので、その後自動でモデルが生成されるタイミング(Model::__isset())で、hasManyやbelogsToにそのテーブルの情報が無い場合は、HABTMを見に行って、中間テーブルの情報があればモデルを生成します。このときに、dynamicWithが指定されているとAppModelがnewされてしまいます。dynamicWithがfalseだったり、すでに事前にClassRegistry::init()で該当モデルのインスタンスを生成していればAppModelからインスタンスが生成されません。
なぜModel::__isset()かというと、CakePHP2から導入された遅延ローディングで、モデルのオブジェクトに始めてアクセスするまでモデルのインスタンスは生成されなくなりました。コントローラにいくら $uses = array(A, B, C);って書いても、$this->A->find()ってやる瞬間までAモデルのインスタンスは生成されないということです。
何を言ってるか分からないと思うので、下記のコードにコメントしたので、それを見て汲み取ってください。。。
今回は、中間テーブルのDivisionsUserモデルがどのように生成されるのかの流れを要点だけで解説。コードの不要な箇所はすべて省略してます。
DivisionsUserはhasAndBelongsToManyのjoinTableで定義されるものです。
<?php //lib/Cake/Model/Model.php public function __construct($id = false, $table = null, $ds = null) { $this->_createLinks(); //ここでリレーションモデルの生成 } protected function _createLinks() { //アソシエーション生成 $this->_generateAssociation($type, $assoc); } protected function _generateAssociation($type, $assocKey) { foreach ($this->_associationKeys[$type] as $key) { //hasManyやhasAndBelongsToManyなどをループで見ていく //そのプロパティの定義で、定義が無いものはデフォルトの定義値を入れていく if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) { //'with'に何も値がないと、中間テーブルのテーブル名からモデル名を作成(DivisionsUser) // ついでにdynamicWithのフラグをon(てめー!) switch ($key) { case 'with': $data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable'])); $dynamicWith = true; break; } } //dynamicWithのキーをtrueにしてしまう(てめー!) if ($dynamicWith) { $this->{$type}[$assocKey]['dynamicWith'] = true; } } } public function __isset($name) { //$nameはクラス名が今回の流れでは入ります(DivisionsUser) $className = false; foreach ($this->_associations as $type) { //$typeにはhasManyやhasAndBelongsToManyなどの文字列が入ります if (isset($name, $this->{$type}[$name])) { //ここでhasManyなどにDivisionsUserがあればループ終了して、通常通りモデル生成 $className = empty($this->{$type}[$name]['className']) ? $name : $this->{$type}[$name]['className']; break; } elseif ($type == 'hasAndBelongsToMany') { //hasManyなどに定義がなければ、hasAndBelongsToManyを走査 foreach ($this->{$type} as $k => $relation) { // withキーはコンストラクタで中間テーブルのクラス名(DivisionsUser)が入っている if (is_array($relation['with'])) { if (key($relation['with']) === $name) { $className = $name; } if ($className) { $assocKey = $k; //ここでコンストラクタでセットされたdynamicWithが評価される $dynamic = !empty($relation['dynamicWith']); break(2); } } } } if (!ClassRegistry::isKeySet($className) && !empty($dynamic)) { //まだ該当モデル(DivisionsUser)が存在せず、dynamicキーがtrueなら、AppModelをnewする(てめー!) $this->{$className} = new AppModel(array( 'name' => $className, 'table' => $this->hasAndBelongsToMany[$assocKey]['joinTable'], 'ds' => $this->useDbConfig )); } else { //dynamicがfalseなら、ClassRegistryを使ってモデルを生成する(AppModelにはならない) $this->_constructLinkedModel($name, $className, $plugin); } }
ちなみに、このdynamicWithの何がメリットかと考えると、中間テーブルのモデルファイルを作っておかなくてもモデルのインスタンスがAppModelで生成されて、基本的なモデルのメソッドが使えるということ。
それぐらいかな。。。。