問題

ContainableBehavior を用いながら Model のエイリアスを変更しようとしたときにエラーが出る。(Cake2)

サンプルコード端折りまくってますが、要するにContainableBehaviorに渡したい情報がうまく伝わらない。

// PostsController のアクション例。
// Post に ContainableBehavior を設定しているが、
// 大人の事情でPostのエイリアスを"Article"にしなくてはならなくなった場合
$this->Post->alias = 'Article';
$articles = $this->paginate(); // Containable に正しく設定が渡らない

解説

Behavior の多くは、Model(のエイリアス)ごとのセッティング情報を Model の alias をキーとして保存し必要なときにそれを呼び出して使用します。

class SampleBehavior extends ModelBehavior {

    public $settings;

    public function setup(Model $model, $config = array()) {
        $this->settings[$model->alias] = $config;
    }

    public function sampleLogic(Model $model) {
        $config = $this->settings[$model->alias];
        // something to do
    }
}

たとえば ContainableBehavior の場合は、インスタンス化の時点でまず自動的に Model のアソシエーション情報を取得し自身の $settings[$Model->alias] に保存。さらに Model::contain() で設定された情報もそこに保存さます。そしていよいよ Model->find が呼び出されるという時にbeforeFindのコールバックメソッド内でそのデータを紐解いて良きに計らってくれるというわけです(@see ContainableBehavior::beforeFind())。

こういう流れは他のBehaviorでも多く見られる基本のパターンなのですが、ここで問題になるのはModelのエイリアスを変更したい場合です。お分かりのとおり、セッティングのプロパティはそれをセットした時点のエイリアスにマッピングされていますので、そのあとでエイリアスを変更してしまうと期待する動作が得られないわけです。

ContainableBehavior では Undefined Index の Notice Error が発生しますが、これはつまり ContainableBehavior::beforeFind 内で $settings[$Model->alias] を見失ったことによるもので、肝心の設定を参照できないわけなんですね。

対処法

そこでどうするか。Behavior 側で Model のエイリアスの変更を感知する仕組みがあれば理想的なのですが、そういうのはありませんので次善の策として一旦モデルからBehaviorを取り外して再度ロード(アタッチ)させます。
// CakeEventManager::attach() は @deprecated となっていて、 load() への移行が推奨されています。(2013/06/17 追記)

// PostsController のアクションの例
$this->Post->Behaviors->detach('Containable');
$this->Post->alias = 'Article';
$this->Post->Behaviors->load('Containable');

当然、Modelのインスタンス化での最初のBehaviorの設定は無意味になるため、その部分においてオーバーヘッドは発生します。もっと良い解決策があったら教えて欲しいです。もやもや・・・

追記

直接 Behavior のインスタンスにアクセスしてセッティングを変更することも可能なのでご参考までに記載しておきます。

ただ、後で読み返した時にワケがわからなくなること必至で、しかもBehaviorによって実装はまちまちなので汎用性もないです。正直あまりやりたいとは思わないです(笑)

$aliasOld = $this->Post->alias; // この時点で ContainableBehavior のインスタンスが生成され、"Post"のエイリアスで一旦設定がセットされる
$aliasNew = 'Article';
$containableBehavior = ClassRegistry::getObject('ContainableBehavior'); // 遅延ロードに注意。今回は $this->Post->alias のタイミングで実体化されているのでOK
$containableBehavior->settings[$aliasNew'] = $containableBehavior->settings[$aliasOld];
unset($containableBehavior->settings[$aliasOld']);