1.1で始めるYii Framework その3-1 (記事の管理 - Postモデルを変更する)

1.1で始めるYii Framework その2 (モデルの作成、CRUD機能の追加)
から引き続き、Yii Frameworkでのブログ作成を行います。

この章は非常に長いので、分割して投稿します。
今回は
3.記事の管理 - Postモデルを変更する
を実施します。


Postモデルの修正

前に自動生成したPostモデルについて、色々修正していきます。
デフォルトの状態だと、DBからの情報を引っ張って来て設定を行っているので、不要な箇所があったり、サイトを構築する上での
必要な機能がありません。
それを初めからコーディングすると大変な労力になりますが、必要な箇所のみ、追加・修正を行うことによって、自分で実装する
作業量を減らします。
今回は、記事を管理するモデルのPost.phpについて、必要な修正を行います。

バリデーションの修正(rules()メソッドの変更)

入力されたデータについて、正しく入力されているかのチェックを行う設定は、modelのrules()メソッドにて設定します。
これによって、いちいち個別にチェックするという無駄なコーディングが無くなります。
rules()メソッドの変更は以下の通り。

※、変更前
<?php
	/**
	 * @return array validation rules for model attributes.
	 */
	public function rules()
	{
		// NOTE: you should only define rules for those attributes that
		// will receive user inputs.
		return array(
			array('title, content, status, authorId', 'required'),
			array('status, createTime, updateTime, commentCount, authorId', 'numerical', 'integerOnly'=>true),
			array('title', 'length', 'max'=>128),
			array('contentDisplay, tags', 'safe'),
		);
	}

※、変更後
	/**
	 * @return array validation rules for model attributes.
	 */
	public function rules()
	{
		return array(
			array('title, content, status', 'required'),
			array('title', 'length', 'max'=>128),
			array('status', 'in', 'range'=>array(0, 1, 2)),
			array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/',
				'message'=>'タグは文字列のみで入力してくだださい。'),
	    );
	}

ここで指定したチェック項目は以下のとおりです。*1

  • タイトル、コメント、ステータスは必須入力
  • タイトルの文字数は最大128文字まで*2
  • ステータスの入力可能文字は0、1、2のみ
  • タグに付いては、正規表現でマッチをかけて文字列のみ入力可能

エラーチェック1(必須チェック)

エラーチェック2(入力内容チェク)

※1、入力チェックついては、配列順に行われ、エラーがあればそこでチェックを中止
※2、標準のlengthではバイト数をカウントするので注意

エラーメッセージの日本語化

このままだと、メッセージが英語なので使いにくいです。エラーメッセージの日本語化を行います。
以下のファイルに追記を行います。
D:\pdt\xampp\htdocs\blog\protected\config\main.php

追記項目
'language'=>'ja',

これによって、以下のように表示されます。
エラーチェック1(必須チェック)

エラーチェック2(入力内容チェック)

safeAttributes()メソッドの変更について

この設定ついては、1.0から1.1にアップデートした際に不要となりました。*3

他のモデルとの連動(relations()メソッドを変更する)

Modelには、標準で他のモデル連動する機能「リレーショナルアクティブレコード」が装備されています。*4
これによって、SQLによるデータを結合をすることなく、コード上のみでデータの連携を設定することが可能です。
複雑な連動は厳しいですが通常そこまで複雑なDB処理は行わないので、サクっと設定出来るので、設定後はすでに
特定キーにて紐付けされたデータを取り扱うことができます。

複雑なデータ取得を行う場合には、SQLを直接実行する方法にてデータの取得を行います。
rules()メソッドの変更は以下の通り。

※、変更前
<?php
	/**
	 * @return array relational rules.
	 */
	public function relations()
	{
		// NOTE: you may need to adjust the relation name and the related
		// class name for the relations automatically generated below.
		return array(
		);
	}

※、変更後
	/**
	 * @return array validation rules for model attributes.
	 */
	public function relations()
	{
		return array(
			'author'=>array(self::BELONGS_TO, 'User', 'authorId'),
			'comments'=>array(self::HAS_MANY, 'Comment', 'postId', 'order'=>'comments.createTime'),
			'tagFilter'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)',
				'joinType'=>'INNER JOIN',
				'condition'=>'tagFilter.name=:tag'),
		);
	}

※、1.0系では以下のように記述する
	public function relations()
	{
		return array(
			'author'=>array(self::BELONGS_TO, 'User', 'authorId'),
			'comments'=>array(self::HAS_MANY, 'Comment', 'postId',
				'order'=>'??.createTime'),
			'tagFilter'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)',
				'together'=>true,
				'joinType'=>'INNER JOIN',
				'condition'=>'??.name=:tag'),
		);
	}

ここで指定したモデル連携は以下のとおりです。

  • author項目を追加。データ取得先はUserテーブル。PostテーブルのidとUserテーブルのauthorIdで紐付けし、UserテーブルのデータにPostテーブルのデータが所属する
  • commentsを追加。データ取得先はCommentテーブル。PostテーブルのidとCommentテーブルのpostIdで紐付けし、PostテーブルのデータにCommentテーブルのデータが複数該当する。表示順はcommentテーブルのcreateTime順
  • tagFilterを追加。データ取得先はTagテーブル。PostTagテーブルのpostIdとtagIdとで紐付け。テーブルのJOINタイプはINNER JOIN。この項目の詳細については、別途説明を追記します。


この設定によって、次のようなDBアクセスが可能になります。

<?php
// Postモデルからauthor項目取得
$author=$post->author;
// authorからuserテーブルのusername取得
$author->username;


// Postモデルからcomments項目取得
$comments=$post->comments;
// comments項目からcontent情報を取得
foreach($comments as $comment){
	$comment->content;
}
ステータスをテキストで表す

ステータスはデータベースでは整数として保存されるため、ユーザにステータスを表示するには理解しやすいテキスト表現を提供する必要があります。
そのため、Postモデルを以下のように変更します。
定数を設定後、取得メソッドを追加します。

※、変更前
<?php
class Post extends CActiveRecord
{
	/**
	 * The followings are the available columns in table 'Post':
	 * @var integer $id
	 * @var string $title
	 * @var string $content
	 * @var string $contentDisplay
	 * @var string $tags
	 * @var integer $status
	 * @var integer $createTime
	 * @var integer $updateTime
	 * @var integer $commentCount
	 * @var integer $authorId
	 */

	/**
	 * Returns the static model of the specified AR class.
	 * @return CActiveRecord the static model class
	 */
	public static function model($className=__CLASS__)
	{
		return parent::model($className);
	}

	/**
	 * @return string the associated database table name
	 */
	public function tableName()
	{
		return 'Post';
	}

	/**
	 * @return array validation rules for model attributes.
	 */
	public function rules()
	{
		return array(
			array('title, content, status', 'required'),
			array('title', 'length', 'max'=>128),
			array('status', 'in', 'range'=>array(0, 1, 2)),
			array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/',
				'message'=>'タグは文字列のみで入力してくだださい。'),
	    );
	}

	/**
	 * @return array relational rules.
	 */
	public function relations()
	{
		return array(
			'author'=>array(self::BELONGS_TO, 'User', 'authorId'),
			'comments'=>array(self::HAS_MANY, 'Comment', 'postId', 'order'=>'comments.createTime'),
			'tagFilter'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)',
				'joinType'=>'INNER JOIN',
				'condition'=>'tagFilter.name=:tag'),
		);
	}

	/**
	 * @return array customized attribute labels (name=>label)
	 */
	public function attributeLabels()
	{
		return array(
			'id' => 'Id',
			'title' => 'Title',
			'content' => 'Content',
			'contentDisplay' => 'Content Display',
			'tags' => 'Tags',
			'status' => 'Status',
			'createTime' => 'Create Time',
			'updateTime' => 'Update Time',
			'commentCount' => 'Comment Count',
			'authorId' => 'Author',
		);
	}
}

※、変更後
class Post extends CActiveRecord
{
	/**
	 * The followings are the available columns in table 'Post':
	 * @var integer $id
	 * @var string $title
	 * @var string $content
	 * @var string $contentDisplay
	 * @var string $tags
	 * @var integer $status
	 * @var integer $createTime
	 * @var integer $updateTime
	 * @var integer $commentCount
	 * @var integer $authorId
	 */

	const STATUS_DRAFT=0;
	const STATUS_PUBLISHED=1;
	const STATUS_ARCHIVED=2;

	/**
	 * Returns the static model of the specified AR class.
	 * @return CActiveRecord the static model class
	 */
	public static function model($className=__CLASS__)
	{
		return parent::model($className);
	}

	/**
	 * @return string the associated database table name
	 */
	public function tableName()
	{
		return 'Post';
	}

	/**
	 * @return array validation rules for model attributes.
	 */
	public function rules()
	{
		return array(
			array('title, content, status', 'required'),
			array('title', 'length', 'max'=>128),
			array('status', 'in', 'range'=>array(0, 1, 2)),
			array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/',
				'message'=>'タグは文字列のみで入力してくだださい。'),
	    );
	}

	/**
	 * @return array relational rules.
	 */
	public function relations()
	{
		return array(
			'author'=>array(self::BELONGS_TO, 'User', 'authorId'),
			'comments'=>array(self::HAS_MANY, 'Comment', 'postId', 'order'=>'comments.createTime'),
			'tagFilter'=>array(self::MANY_MANY, 'Tag', 'PostTag(postId, tagId)',
				'joinType'=>'INNER JOIN',
				'condition'=>'tagFilter.name=:tag'),
		);
	}

	/**
	 * @return array customized attribute labels (name=>label)
	 */
	public function attributeLabels()
	{
		return array(
			'id' => 'Id',
			'title' => 'Title',
			'content' => 'Content',
			'contentDisplay' => 'Content Display',
			'tags' => 'Tags',
			'status' => 'Status',
			'createTime' => 'Create Time',
			'updateTime' => 'Update Time',
			'commentCount' => 'Comment Count',
			'authorId' => 'Author',
		);
	}

	/**
	 * 新規追加
	 * @return ステータスの状態について、定数をキーとして連想配列を返す
	 */
	public function getStatusOptions()
	{
		return array(
			self::STATUS_DRAFT=>'Draft',
			self::STATUS_PUBLISHED=>'Published',
			self::STATUS_ARCHIVED=>'Archived',
		);
	}

	/**
	 * 新規追加
	 * @return ステータスの状態について、getStatusOptions()からマッチする項目名を返す
	 */
	public function getStatusText()
	{
		$options=$this->statusOptions;
		return isset($options[$this->status]) ? $options[$this->status]
			: "unknown ({$this->status})";
	}

}

これで、テーブル内の整数から、ステータス状態を取得できるようになりました。

今回はここまで。
その3-2では、記事の新規作成と更新の実装を行います。

*1:自分的備考 設定された順にチェックが行われるのか?

*2:自分的備考 文字数については、2バイト文字の場合、文字コードの違いでも正しく文字数で認識するか?

*3:参考サイトUpgrading from Version 1.0 to 1.1

*4:参考サイト リレーショナルアクティブレコード