カテゴリー : Yii

Yii Blog Tutorial PostとCategoryをMANY_MANYで結合する

チュートリアルではタグのインプリメントが説明されているが、独自にカテゴリテーブルを用意して多対多の関係を処理することにトライしてみた。多くの時間を費やしてしまったが、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)

Yii Brog Tutorial ログオン機能とMVCの関係はどうなんだろう?

スキャフォールディングで出力されたログイン処理の入り口は、site コントローラの login (site/login)アクションにあるのでソースを眺めて見る。Definitive Guide を見たおかげで、ようやく何をしているか推測できるようになってきた。

このログイン処理へは、サイト表示上部の Login の他、コントローラで定義された accessRules で、アクションが「admin」に指定されているとリダイレクトされる。これは、フレームワーク動作の中心となるアプリケーションコンポーネントの中の CWebUser が持つパラメータ 「loginUrl」によって、デフォルトが「/site/login」に定められているからのようだ。

コアアプリケーションコンポーネントのプロパティは、config/main.php の中で初期設定できる。Core Application Components をみると、「user: CWebUser」とあるので、config/main.php の componets 指定で、

return array(
	'components'=>array(
		'user'=>array(
			'loginUrl'=>'controller/action',
		),

を設定すれば、ログイン処理は site/login 以外のアクションに変更できそうだ。

ログイン表示のビューは views/site/lgoin.php に置かれているので、タイトルなど日本語に変更する。入力フィールドのタイトルは、モデル(model/LoginForm.php)に定義された attributeLabels() メソッドからアトリビュート名(モデル内のテーブルカラム名など定義)をキーとした配列で渡されるので、次のようにオーバーライドした。

public function attributeLabels() {
	return array(
		'username'=>'ログイン名',
		'password'=>'パスワード',
		'rememberMe'=>'次回ログインを省略',
	);
}

さて、認証処理をする components/UserIdentity.php の authenticate() メソッドへは、

SiteController.php の actionLogin() 内 $model->validate() により
model/LoginForm.php の rules 定義で authenticate() が実行され
authenticate() 内 $identity->authenticate() により
components/UserIdentity.php の authenticate() が実行される

順番で進む。チュートリアルでは UserIdentity.php の authenticate() で、入力データとDB上のユーザテーブルを照合し認証結果を戻す。

ここで、今後のサイト作りでログインした人の名前を表示したいので、次のような仕掛けを作ってみる。

セッションデータに名前を登録し、ログイン時にその名前をナビゲーションに表示するようにする。

user テーブルに dispname カラムを追加し、それを読み込んで表示するようにする。

先ずはUserIdentity クラスに $_dispname プロパティを追加し、getDispname()で読み取れるように準備する。次にLoginForm.php の authenticate()で、ユーザ認証でエラーがなかった際、Yii:app()->user->login() を実行してセッションに登録しているようなので、その下に

Yii::app()->user->setState(‘dispname’, $identity->dispname);

UserIdentity.php の authenticate()でユーザー認証が正常に行なわれた場合、下のように

$this->setState('dispname', $model->dispname);

として名前を登録する。

ナビゲーションの表示は、レイアウトビュー views/layouts/main.php で行なわれている。

Yii::app()->user->name

Yii::app()->user->getState(‘dispname’)

に変更して、名前を表示するようになった。

最後に、ログイン画面のエラー表示を変更してみる。Yii のエラー表示は、入力項目別に行なう方法とページの一か所にまとめて表示する方法を用意している。両方同時表示も可能。エラーメッセージを日本語表示したい場合は、Yii デフォルトコントローラ/アクションの変更を参照して下さい。

エラーが発生した場合 LoginForm.php の authenticate()内で、addError() を利用して CHtml ヘルパーで属性とエラーメッセージを表示している。スキャフォールドで作成された認証エラーは、ご丁寧にログイン名とパスワードのどちらで発生したものか知らせてくれるので、これを止めて、一か所のエラー表示でログインデータの誤りを表示するようにしたい。

そこでエラーを case 処理せず、そして属性を指定しないで登録してみた。

switch($identity->errorCode) {
	case UserIdentity::ERROR_NONE:
	 :
	default:
		$this->addError('', 'ログイン名、パスワードが誤りです。');
		break;
}

これで、views/site/login.php にある CHtml::errorSummary()で、登録したエラーが表示されるようになった。各項目別にエラーを表示したい場合は、CHtml::error()を利用すればよい。

(yii-1.1.0.r1700)

Yii Blog Tutorial スキャフォールディングとモジュールの配置

CakePHPでは bake と呼ばれるツールがあり、MVCモデルにおける各パートのベースモジュールを自動生成してくれるので、大変重宝するツールだ。Yiiにおいても用意してくれており、これまた大変便利である。このような作業を、スキャフォールディングと呼んでいる。

Yiiではサイトの骨格を、そのまま利用できるようなデザインを含んだコードを吐き出してくれる。次のコマンドを実行するだけで、図のような表示の動的サイトが簡単にできる。

framework> yiic.bat webapp \htdocs\test

さて、Webアプリケーションのプログラムやフレームワーク本体を、ストレージ上のどこに配置するかだが、自分は公開ディレクトリ以外に置くようにしている。Yii も CakePHP 同様、フレキシブルに設定できる。今回は以下の通りにした。

\
+- htdocs
|   +- test  (index.php)
|       +- protected
+- yii
    +framework (yiic.bat)

htdocs が公開ディレクトリで、yii\framework がフレームワーク本体。スキャフォールディングは、Windows用に用意された yiic.bat を利用して公開ディレクトリの下に test ディレクトリを作成し、そこを出力先とした。

これで、上のコマンド実行後に「http://www.example.com/test/」をブラウズすると、上のようなサイトが表示される。さて、スキャフォールディングで出力されたアプリケーションのモジュール htdocs\test\protected も、公開ディレクトリから yii のサブディレクトリ yii\blog に移してしまおう。

これで公開ディレクトリに残ったのは、test\index.php だけとなる。index.php では、フレームワークとアプリケーションの設定ファイルパスを指定する必要がある。そこで、次のようにそれぞれ変更する。

$yii = realpath('../../yii/framework/yii.php');
$config = realpath('../../yii/blog/config/main.php');

これで、実際のWebサーバー上での配置を含めた環境が決まった。
(yii-1.1.0.r1700)

Yii デフォルトコントローラ/アクションの変更

「http://xxx.com/」とアクセスされた際、「IndexController/actionIndex」がデフォルト実行される。これを「GameController/actionPlay」に変更する方法は、「config/main.php」と「GameController.php」を以下のようにする。

config/main.php
<?php
return array(
	'language' => 'ja',
	'defaultController' => 'game',
	'components' => array(
		'urlManager' => array(
			'urlFormat' => 'path',
		),
	),

「’language’=>’ja’」の指定で、フレームワークからのエラーメッセージが日本語出力される。「components」の「urlFormat」指定は、「http://xxx.com/index.php/game/play」のパス形式でアクセスするための指定。

controllers/GameController.php
<?php
class GameController extends CController
{
	public $defaultAction = 'play';

	public function actionPlay()
	{

パス形式を「http://xxx.com/game/play」のようにアクセスするには、「urlManager」に「’showScriptName’=>false」を追加し、.htaccess で 以下のように rewrite_mod を利用するとよい。

.htaccess
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ index.php [L]

(yii-1.1.0.r1700)

追補
さくらの共用サーバーでサブドメイン(sub)のサブディレクトリ(subdir)で、「www/sub/subdir/index.php」の場合、上の設定ではwebサーバーのリクエストが「/sub/subdir/index.php」となってしまい、404エラーとなった。そこで「RewriteBase /subdir 」としたところ、動作するようになった。

100326 (yii-1.1.1.r1907)

Yii xreaでリンクに追加されたPHPSESSIDセッションID

Yii のサイトにあるチュートリアルのブログが動作するようになるまで、意外とハードルが高かった。その中で、ブラウザを起動して最初にサイトを表示したとき、メニューのリンクに PHP のセッションIDが付加されてしまうのを回避するのに、時間が掛ってしまった。

xrea サーバーではセーフモードが ON にセットされている。そのためファイル出力やデータアップロードの操作で、ファイルの保護モードに引っかかる事がある(このモードは5.3.0では非推奨、6.0.0で削除のようだ)。

この回避方法は、PHP を CGIモードで動作させるようにするのが手っ取り早い。タスクの起動に時間が掛るようで嫌われることもあるようだが、そこがアクセススピードのボトルネックにならないのなら、気にする必要はないだろう。これは、.htaccess に次のように指定する。

AddHandler application/x-httpd-phpcgi .php

さて、リンクに付加されたセッションIDが表示されないようにするためには、php.ini の「use_cookies、use_only_cookies、use_trans_sid」をそれぞれ「On, On, 0」とすれば良い。で、これも .htaccess に以下のように指定した。

php_flag session.use_only_cookies on
php_value session.use_trans_sid 0

が、これがいろいろと書き変えて見るが、どーもうまくいかない。なのでこの方法は諦めることにした。

CakePHP の config/core.php のようなパラメータ設定ファイルが、Yii では config/main.php でできるようになっている。Yii でも何かできないか試したところ、コアアプリケーションコンポーネントに「session: CHttpSession(framework/web/CHttpSession.php)」が組み込まれており、「savePath」プロパティの設定でセッションデータの保存先を変更できる事が分かった。

「use_only_cookies」を指定する「set_ini」命令がないか探して見ると、同じ「CHttpSession」コンポーネントに「setCookieMode()」メソッドがあり、また「cookieMode」というプロパティもあるので、どうやら核心部に近付いてきたようだ。「cookieMode」には、「none、allow、only」の3つがありデフォルトは「allow」である。ならこれを「only」にすればいい訳だ。早速次のように「savePath」も含めて設定した。

'components'=>array(
 :
	'session'=>array(
		'cookieMode' => 'only',
		'savePath' => '/virtual/xxx/xxx',
	),
),

果たして結果は….. orz

うーん、どうしたものか。悩みながらも「setCookieMode()」メソッドを見ると「use_cookies」と「use_only_cookies」は設定しているが、「use_trans_sid」の設定がない。

     |
\  __  /
_ (m) _ピコン
   |ミ|
 /  `´  \
   ∧ ∧
  (・∀・)
  ノ(  )ヽ
   <  >

これだ!

で、「setCookieMode()」メソッドに「ini_set(‘session.use_trans_sid’, ‘0’);」を次のように追加したところ、目出度くリンクから PHPSESSID が追加されなくなった。

else if ($value==='only')
{
	ini_set('session.use_cookies', '1');
	ini_set('session.use_only_cookies', '1');
	ini_set('session.use_trans_sid', '0');
}

コアソースを弄るのはどうも、ではありますが(バージョンは yii-1.1.0.r1700)。