Yiiでも中間テーブルにアクセスしたい

またこのネタです。

YiiFrameworkのCManyManyRelationは中間テーブルへのアクセスをサポートしていません。ならばオレオレリレーションによってそれを可能にしてみます。前エントリのまとめと反するのですが、まあ、あそびばってことで。
実現のためにCJoinElementを改造します。まだまだ中途半端な状態ですが載せておきます。気がむいたら更新するかもしれません。

2012.10.25 実験段階 とりあえずレイジーローディング

リレーション

まずオレオレリレーションモデルです。

protected/components/ManyManyPivotRelation.php

<?php
class ManyManyPivotRelation extends CManyManyRelation
{
    public $pivotModel = null;
    public $pivotWith = array();
}

モデル

続いてモデル

<?php
class Viewer extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'viewer';
    }
    public function relations()
    {
        return array(
            'movies_with_pivot' => array("ManyManyPivotRelation", 'Movie', 'viewer_watched_movie(viewer_id, movie_id)',
                'pivotModel' => 'ViewerWatchedMovie', // <- 中間テーブルモデル
                'pivotWith' => array('id','viewer_id', 'movie_id', 'liked',), // <-中間デーブルのカラム
            ),
        );
    }
}

viewer_watched_movie テーブルのPKはIDです。

<?php
class ViewerWatchedMovie extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'viewer_watched_movie';
    }
}
<?php
class Movie extends CActiveRecord
{
    public $pivot;   // <- ここに中間テーブルのモデルが入る
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'movie';
    }
}

CJoinElement

次はCJoinElementに手を入れます。hack begin - hack end を挿入してください。
リレーションがManyManyPivotRelationならば、selectするカラムを追加し、select結果からpivotインスタンスを生成し、子モデルの $record->pivot にセットしています。

YiiFramework/framework/db/ar/CActiveFinder.php

<?php
class CJoinElement
{
	private function applyLazyCondition($query,$record)
	{
		$schema=$this->_builder->getSchema();
		$parent=$this->_parent;
		if($this->relation instanceof CManyManyRelation)
		{
        ....
			if($parentCondition!==array() && $childCondition!==array())
			{
				$join='INNER JOIN '.$joinTable->rawName.' '.$joinAlias.' ON ';
				$join.='('.implode(') AND (',$parentCondition).') AND ('.implode(') AND (',$childCondition).')';
				if(!empty($this->relation->on))
					$join.=' AND ('.$this->relation->on.')';
				$query->joins[]=$join;
				foreach($params as $name=>$value)
					$query->params[$name]=$value;
			}
			else
				throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
					array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name)));

            // hack begin ----------
            if($this->relation instanceof ManyManyPivotRelation) {
                // add pivot columns 
                // TODO table.column -> t0_c0
	            foreach ($this->relation->pivotWith as $col) {
		            $query->selects[]=$joinAlias.'.'.$schema->quoteColumnName($col);
	            }
            }
            // hack end ----------
		}
		else
		{
        ....
		}
	}

	private function populateRecord($query,$row)
	{
        ....
        
		if(isset($this->records[$pk]))
			$record=$this->records[$pk];
		else
		{
			$attributes=array();
			$aliases=array_flip($this->_columnAliases);
			foreach($row as $alias=>$value)
			{
				if(isset($aliases[$alias]))
					$attributes[$aliases[$alias]]=$value;
			}
			$record=$this->model->populateRecord($attributes,false);
			foreach($this->children as $child)
			{
				if(!empty($child->relation->select)) {
					$record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation);
				}
			}
			$this->records[$pk]=$record;
			
            // hack begin ----------
            if($this->relation instanceof ManyManyPivotRelation) {
                // make pivot instance
	            //  TODO t0_c0 -> column_name
	            $p = new $this->relation->pivotModel;
 	            $record->pivot = $p->populateRecord($row,false);
            }
            // hack end ----------
			
		}

        ....
    }
}

ビュー

<?php
$viewers = Viewer::model()->findAll();
foreach($viewers as $viewer) {
    foreach ($viewer->movies_with_pivot as $movie) {
  	    echo $viewer->name . ($movie->pivot->liked ? ' liked ' : ' didn’t like ')
 	        . $movie->title;
    }
}

結果

hoge didn’t like foo
hoge liked bar
piyo liked foo
piyo didn’t like bar