Post モデルのカスタマイズ
======================

`Gii` ツールにより生成された `Post` モデルクラスは、主に二つの点において修正する必要があります。

 - `rules()` メソッド: モデルの属性に対する検証ルールを規定
 - `relations()` メソッド: リレーショナルオブジェクトを規定

> Info|情報: [モデル](http://www.yiiframework.com/doc/guide/ja/basics.model) は属性のリストから構成されます。それぞれの属性は、通常は、対応するデータベーステーブルのコラムと関連付けられます。
属性は明示的にクラスメンバ変数として宣言される場合もあり、宣言無しで黙示的に宣言されることもあります。

`rules()` メソッドのカスタマイズ
----------------------------

最初に、検証ルールを指定します。これは、属性の値をデータベースに保存する前に、ユーザによって入力された値が正しいものであることを保証するためのものです。
例えば、`Post` クラスの `status` 属性は、整数の 1, 2, 3 のどれかでなければなりません。
`Gii` ツールもそれぞれのモデルに対して検証ルールを生成します。
しかしながら、それはテーブルのカラム情報に基いたルールであり、妥当なものではないかも知れません。

要求分析に基き、`rules()` メソッドを以下のように修正します。

~~~
[php]
public function rules()
{
	return array(
		array('title, content, status', 'required'),
		array('title', 'length', 'max'=>128),
		array('status', 'in', 'range'=>array(1,2,3)),
		array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/',
			'message'=>'タグは単語構成文字だけを含むことが出来ます。'),
		array('tags', 'normalizeTags'),

		array('title, status', 'safe', 'on'=>'search'),
	);
}
~~~

上記において、`title`, `content`, `status` 属性は必須です。
`title` の長さは 128 を超えてはなりません。
`status` 属性の値は、1 (下書き)、2 (公開)、3 (アーカイブ) のうちのいずれかでなければなりません。
`tags` 属性は単語構成文字とカンマしか含むことはできません。
さらに、`normalizeTags` を使用して、ユーザが入力したタグの文字列を正規化し、ユニークなタグがカンマで正しく分離されている文字列になるようにします。
最後のルールはサーチ機能で使用しますが、これについては後で説明します。

`required`, `length`, `in`, `match` のようなバリデータは全て Yii が提供する備え付けのものです。
`normalizeTags` バリデータはメソッドベースのバリデータであり、`Post` クラスにおいて定義する必要があります。
検証ルールを定義する方法に関するより詳細な情報は、[ガイド](http://www.yiiframework.com/doc/guide/ja/form.model#sec-3) を参照してください。

~~~
[php]
public function normalizeTags($attribute,$params)
{
	$this->tags=Tag::array2string(array_unique(Tag::string2array($this->tags)));
}
~~~

ここで、`array2string` と `string2array` は、`Tag` モデルクラスで定義しなければならない新しいメソッドです。

~~~
[php]
public static function string2array($tags)
{
	return preg_split('/\s*,\s*/',trim($tags),-1,PREG_SPLIT_NO_EMPTY);
}

public static function array2string($tags)
{
	return implode(', ',$tags);
}
~~~

`rules()` メソッドで宣言されたルールは、モデルインスタンスの [validate()|CModel::validate] メソッドまたは [save()|CActiveRecord::save] メソッドをコールする際に、ひとつひとつ実行されます。

> Note|注意: 覚えて欲しい重要なことがあります。`rules()` に出現する属性は、エンドユーザによって入力される属性でなければなりません。`Post` モデルの `id` や `create_time` など、プログラムやデータベースによって設定される属性は、`rules()` に入れるべきではありません。詳しくは、[属性への代入を安全にする](http://www.yiiframework.com/doc/guide/ja/form.model#sec-4) を参照して下さい。

以上の変更をしたら、記事の作成ページを再び開いて、新しい検証ルールが機能していることを確認することができます。

`relations()` メソッドのカスタマイズ
-----------------------------------

最後に `relations()` メソッドをカスタマイズして、記事に関連するオブジェクトを指定します。
`relations()` で関連オブジェクトを宣言することによって、[リレーショナルアクティブレコード (RAR)](http://www.yiiframework.com/doc/guide/ja/database.arr) のパワフルな機能を引き出すことが出来ます。
すなわち、RAR を使って、複雑な SQL JOIN 構文を書くことなく、執筆者やコメント一覧などの記事に関連するオブジェクトの情報にアクセスできるようになります。

`relations()`メソッドを次のようにカスタマイズします。

~~~
[php]
public function relations()
{
	return array(
		'author' => array(self::BELONGS_TO, 'User', 'author_id'),
		'comments' => array(self::HAS_MANY, 'Comment', 'post_id',
			'condition'=>'comments.status='.Comment::STATUS_APPROVED,
			'order'=>'comments.create_time DESC'),
		'commentCount' => array(self::STAT, 'Comment', 'post_id',
			'condition'=>'status='.Comment::STATUS_APPROVED),
	);
}
~~~

同時に、上記メソッドで使用されている二つの定数を、`Comment` モデルに加えます。

~~~
[php]
class Comment extends CActiveRecord
{
	const STATUS_PENDING=1;
	const STATUS_APPROVED=2;
	......
}
~~~

`relations()` で宣言したリレーションは次のような意味です。

 * 1つの記事は、1つの執筆者に所属する。執筆者のクラスは `User` で、記事の `author_id` 属性で結び付けられる。
 * 1つの記事は、多数のコメントを持つ。コメントのクラスは `Comment` で、コメントの `post_id` 属性で結び付けられる。コメントは作成日時順にソートされ、承認済み(APPROVED)のコメントだけで構成される。
 * `commentCount` は集計結果を返す少し特殊なリレーションで、記事が持つコメントの数を表す。

上記のリレーションを宣言することで、以下のように簡単に記事の執筆者やコメントにアクセスできます。

~~~
[php]
$author=$post->author;
echo $author->username;

$comments=$post->comments;
foreach($comments as $comment)
	echo $comment->content;
~~~

リレーションの宣言と使い方の詳細については、[ガイド](http://www.yiiframework.com/doc/guide/ja/database.arr) をご覧ください。

`url`プロパティの追加
---------------------

記事には、内容を閲覧するためのユニークな URL が結び付いています。この URL を取得するために、コードのいたるところで [CWebApplication::createUrl] を書くのではなく、`Post` モデルに `url` プロパティを追加することで、URL を生成する同一のコードを再利用できます。後で URL を美しくする方法を説明するときに、このプロパティの追加が非常に便利なことが分かります。

`url` プロパティを追加するために、以下のように `Post` クラスを修正して getter メソッドを追加します。

~~~
[php]
class Post extends CActiveRecord
{
	public function getUrl()
	{
		return Yii::app()->createUrl('post/view', array(
			'id'=>$this->id,
			'title'=>$this->title,
		));
	}
}
~~~

URL の GET パラメータとして、post の ID に加えて title を追加していることに注目して下さい。
これは、後ほど [URL をきれいにする](/doc/blog/final.url) で説明するように、主として検索エンジン最適化 (SEO) を目的としています。

`Post` の最上位の親クラスは [CComponent] なので、`getUrl()` という getter メソッドを追加することで `$post->url` という書き方ができるようになります。`$post->url` にアクセスすると getter メソッドが実行されて、その結果が式の値として返されます。このようなコンポーネントの機能の詳細については [ガイド](/doc/guide/ja/basics.component) を参照して下さい。

ステータスをテキストで表現する
------------------------------

記事のステータスは整数でデータベースに保存されるので、エンドユーザに分かりやすく表示するために、テキスト形式での表現を提供する必要があります。大きなシステムでは、このような要求はよくあることです。

汎用的な解決策として、ここでは `tbl_lookup` テーブルを使います。このテーブルに整数値とテキスト表現の対応表を持たせ、このテキスト表現を他のデータオブジェクトから利用します。`Lookup` モデルクラスを以下のように修正し、テーブル内のテキストデータに容易にアクセスできるようにします。


~~~
[php]
class Lookup extends CActiveRecord
{
......

	private static $_items=array();

	public static function items($type)
	{
		if(!isset(self::$_items[$type]))
			self::loadItems($type);
		return self::$_items[$type];
	}

	public static function item($type,$code)
	{
		if(!isset(self::$_items[$type]))
			self::loadItems($type);
		return isset(self::$_items[$type][$code]) ? self::$_items[$type][$code] : false;
	}

	private static function loadItems($type)
	{
		self::$_items[$type]=array();
		$models=self::model()->findAll(array(
			'condition'=>'type=:type',
			'params'=>array(':type'=>$type),
			'order'=>'position',
		));
		foreach($models as $model)
			self::$_items[$type][$model->code]=$model->name;
	}
}
~~~

新しいコードは主に二つの静的メソッド、`Lookup::items()` と `Lookup::item()` を提供します。前者は、指定したデータタイプに属する文字列のリストを返します。後者は、指定したデータのタイプと値に対応する特定の文字列を返します。

ブログのデータベースには、Lookup のタイプとして `PostStatus` と `CommentStatus` が事前に登録されています。前者は記事のステータスが取り得る値を示し、後者はコメントのステータスが取り得る値を示します。

さらに、コードを読みやすくするために、一連のステータスを表す整数値の定数を宣言します。コード中で各ステータス値を参照するときはこの定数を使うべきです。

~~~
[php]
class Post extends CActiveRecord
{
	const STATUS_DRAFT=1;
	const STATUS_PUBLISHED=2;
	const STATUS_ARCHIVED=3;
	......
}
~~~

従って、`Lookup::items('PostStatus')` を呼ぶと、記事のステータスが取り得る値のリスト (対応する整数値をインデックスとするテキスト表現のリスト) を取得できます。また、`Lookup::item('PostStatus', Post::STATUS_PUBLISHED)` を呼ぶと、公開済みというステータスのテキスト表現を取得できます。

<div class="revision">$Id$</div>