HABTMの中間テーブルがAppModelオブジェクトになってしまう問題の対応

今回の話はCake1.2で確認しましたが、1.3でも同じだと思います。


CakeのHABTMは中間テーブルのモデル(例えばDivisionsUserモデルなど)を作らなくても動いてくれて便利なのですが、意外とはまりポイントがあるので書きます。

本解決策により、下記の方のような悲鳴もなくなるでしょう。
http://d.hatena.ne.jp/tsugehara/20100213/1266071529
今回説明で利用するサンプルコードは、上記のブログの例を元にしています。

中間テーブルにid以外のカラムを追加したりして(例えばアクティブフラグとか)、中間テーブルのモデルも別途作り、あえてそのモデルをusesやClassRegistry::init()で読み込んだ場合、そのモデルで指定したプロパティなどが全てなくなってしまいます。バリデーションやリレーション設定全てがなくなるので泣きたくなるでしょう。
理由は、モデルのリンクを自動で生成する際に、Cakeが中間テーブルモデルの生成時にnew AppModel()をして入れてしまうから、つまりControllerで使う$this->DivisionsUserの中身がAppModelオブジェクトになってしまう。
今回は、ここに正しくDivisionsUserモデルのオブジェクトを入れる方法を説明します。



まずはDivisionsUserモデルファイルを作っておきます(作らないとCakeがAppModelオブジェクトとしてクラスを生成してしまう)

<?php
class DivisionsUser extends AppModel {
        var $belongsTo = array(
                'Division' => array(
                        'className' => 'Division',
                        'foreignKey' => 'division_id',
                        'conditions' => '',
                        'fields' => '',
                        'order' => ''
                ),
                'User' => array(
                        'className' => 'User',
                        'foreignKey' => 'user_id',
                        'conditions' => '',
                        'fields' => '',
                        'order' => ''
                )
        );
}

あとは、ここのURL(http://d.hatena.ne.jp/tsugehara/20100213/1266071529)にあるとおり、Model::hasAndBelongsToManyの中で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', //ここが重要 値はnullじゃなければ空文字でもOK. 
      'order' => '',
      'limit' => '',
      'offset' => '',
      'finderQuery' => '',
      'deleteQuery' => '',
      'insertQuery' => ''
    )
  );
}

これだけです!!! 極力はまるポイントを避けるために、毎回withキーは空文字でも良いので指定しておいたほうがよいと思います。
[追記] withキーが空文字の場合は、中間テーブルのモデルファイルが存在しない場合はAppModelオブジェクトが使われ、中間テーブルのモデルファイルが存在すればそれをインスタンス化してくれます。


ここから、詳細を解説します。
CakeのCoreコードのcake/libs/model/model.phpが重要で、__generateAssociationメソッドの中にすべての問題があります。
このメソッドの中で、(ポイント1)アソシエーション情報を見て、定義されたキー(例えばwithやjoinTableなど)の存在をチェックし、キーが存在しない・もしくはキーの値がnull指定の場合は、(ポイント2)switch文の中で必要な情報をセットします。そこでwithの場合は、$dynamicWith = true;がセットされてしまうため、(ポイント3)その後のAppModelインスタンスを生成するかどうかの判定で常にnew AppModel()される処理に入ってしまいます(ポイント4)。

<?php

  function __generateAssociation($type) {
    foreach ($this->{$type} as $assocKey => $assocData) {
      $class = $assocKey;
      $dynamicWith = false;

      foreach ($this->__associationKeys[$type] as $key) {

        // ポイント1
        if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) {
          $data = '';

          switch ($key) {

            ///// 省略
			
            // ポイント2
            case 'with':
              $data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable']));
              $dynamicWith = true;
            break;

            ///// 省略
			
          }

        }
      }

      if (!empty($this->{$type}[$assocKey]['with'])) {
         ///// 省略
	  
        // ポイント3
        if (!ClassRegistry::isKeySet($joinClass) && $dynamicWith === true) {

          // ポイント4
          $this->{$joinClass} = new AppModel(array(
            'name' => $joinClass,
            'table' => $this->{$type}[$assocKey]['joinTable'],
            'ds' => $this->useDbConfig
          ));

        } else {
		
          ///// 省略
		

つまり、$dynamicWith = true;になってしまうswitch文の中が問題なので、ここを回避するためにwithキーを指定すれば良いということです。


HABTMのCakeBookにもWithの説明があります。
http://book.cakephp.org/ja/view/83/hasAndBelongsToMany-HABTM