チュートリアルではタグのインプリメントが説明されているが、独自にカテゴリテーブルを用意して多対多の関係を処理することにトライしてみた。多くの時間を費やしてしまったが、Yii を紐解いて行く中で、テーブルやHTMLヘルパーの扱いで分かった事もいくつか出てきたので、それについての備忘録です。
post, category テーブルと、多対多の関係を表す post_category テーブルの定義は次の通り。文章を短くするため、テーブルの項目は省略してる。
CREATE TABLE `tbl_post` ( `id` int(11) NOT NULL AUTO_INCREMENT, `author_id` int(11) DEFAULT '0', `title` varchar(255) NOT NULL, `content` text, `created` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `author_id` (`author_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 CREATE TABLE `tbl_category` ( `id` int(10) NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 CREATE TABLE `tbl_post_category` ( `post_id` int(10) NOT NULL DEFAULT '0' COMMENT 'CONSTRAINT FOREIGN KEY (post_id) REFERENCES tbl_post(id)', `category_id` int(10) NOT NULL DEFAULT '0' COMMENT 'CONSTRAINT FOREIGN KEY (category_id) REFERENCES tbl_category(id)', PRIMARY KEY (`post_id`,`category_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8
MySQL のバージョンは 5.1.42 で MyISAM エンジンである。
Post データから Category データの参照は tbl_post_category を通して行なうが、上述のようにコメントで Foreign Key の設定を記述しておくことが重要である。Yii のコアコンポーネントは、外部キーが設定できない MySQL の場合、このコメントを読み込んで Category データを読み出すようにしているらしい。いい加減、読み込みができなくて投げ出したくなった。
post モデルで定義した関係は次の通り。
public function relations() { return array( 'categories'=>array(self::MANY_MANY, 'category', 'tbl_post_category(post_id, category_id'), ); }
DB の設定で大文字小文字を判別している場合、テーブルの名前も注意が必要。ここでは、Category ではなく category で、実際のテーブル名は接頭語 tbl_ を付けて、tbl_post, tbl_category, tbl_post_category としている。
リレーションを表す post_category の指定もうっかり「tbl_」を落としていたため、しばらく迷子になっていた。
次に新規登録や変更で利用するフォームである。カテゴリの選択は、全カテゴリの中からチェックボックスを利用して行いたい。
モデルの属性に直接入力項目を割り当てる場合、Htmlヘルパーで「CHtml::activeTextField()」のような「active」が付いたものを利用するが、それでは「CHtml::activeCheckBoxList()」を利用すればいいのだろうか?アトリビュートに categories を指定していろいろと試してみたが挫折した。結局、post モデルに $category アトリビュートを追加し、tbl_post_category から読み取られた category_id を配列にして収めることで、動作するようになった。
$category に category_id の配列を収めるタイミングは、post モデルで「afterFind()」メソッドのオーバーライドで行なった。
class post extends CActiveRecord { public $category; protected function afterFind() { $this->category = array(); foreach ($this->categories as $category) { $this->category[] = $category->id; } return parent::afterFind(); }
ビューの _form.php でカテゴリの選択をするのに、checkBoxList() へ全カテゴリのリストを渡さなければならない。そこで、category モデルに以下のような 「categories()」メソッドを用意し、id をキーとした名前の配列を戻すようにする。
class category extends CActiveRecord { private static $_categories = array(); public static function categories() { if (empty(self::$_categories)) { self::loadCategories(); } return self::$_categories; } public static function loadCategories() { self::$_categories = array(); $categories = self::model->findAll(array( 'order' => 'id', )); foreach ($categories as $category) { self::$_categories[$category->id] = $category->name; } }
これでようやく「CHtml::activeCheckListBox()」メソッドに渡すパラメータの取得の準備が完了。「_form.php」で次のようにカテゴリのチェックボックスリストを表示するようにする。
<div class="row"> <?php echo CHtml::activeLabelEx($model, 'category'); ?> <?php echo CHtml::activeCheckBoxList($model, 'category', category::categories(), array('labelOptions'=>array('style'=>'display:inline',), 'separator'=>' ' )); ?>
Yii で提供される CSS では、labelタグが 「display:block」で チェックボックスが改行表示されてしまう。 labelOptions はガイドに出ていないが、ソースを眺めてみるとコメントに「1.0.10から利用可」とあったので使ってみた。「separator」は、デフォルトが br タグで改行するようになっていたので、漢字の空白にして改行しないようにした。
最後は、多対多の関係テーブル tbl_post_category の更新だ。これは、post テーブルで 「afterSave()」メソッドをオーバーライドして、post データ更新後に以下のようにして書き換えるようにした。
class post extends CActiveRecord { protected function afterSave() { post_category::model()->deleteAll('post_id=:post_id', array(':post_id'=>$this->id)); if (!empty($_POST['post']['category'])) { foreach ($_POST['post']['category'] as $category_id) { $post_category = new post_category; $post_category->post_id = $this->id; $post_category->category_id = $category_id; $post_category->save(); } } return parent::afterSave(); }
これでようやく動作するようになった。
多対多を扱う場合、CakePHP の HABTM ではもっと手軽に利用できたように思ったのだが。Yii も、もっと簡単に扱えるのかもしれない。今の所、自分が試した限りではここまでだ。Google 先生に頼ってみるが、情報がなかなか見つからないので寂しい。
(yii-1.1.0.r1700)