第8章 メッセージボードの追加

この章から読み始める方へ:

前章までの成果物はこちらに置いてありますので、ダウンロードして使ってください。使い方はxamppのhtdocs以下にファイルを展開してください。またyiiという名前の空のデータベースの下にダウンロードしたuser.sqlをインポートしてください。

また設定項目としてprotected/config/main.phpを設定する必要があります。

  • データベース名をyiiとしていますが、それ以外の名前を使った場合は本ファイル内で設定します。またMySQLのパスワードをunko9314以外に設定した方はこちらで設定してください。
  • また前章chap7よりメール送信機能が追加されました。Gmailのメールアドレスを使うことを想定しています。ご自身のGmailのアドレスとパスワードに変更してください。(UsernameとPassword)
		// chap7: メール送信ライブラリを追加
		'mailer'=>array(
			    'class' => 'application.extensions.mailer.EMailer',
			    'From' => 'xxxx@gmail.com', // 送信者のアドレス
			    'FromName' => 'yii-juncheng',// 名前
			    'CharSet' => 'iso-2022-jp',  //文字コード
			    'Encoding' => '7bit',
			    'Mailer'=>'smtp',
			    'Port'=>'587',
			    'SMTPSecure'=>'tls',
			    'Host'=> 'smtp.gmail.com',
			    'SMTPAuth' => true,
			    'Username' => 'xxxx@gmail.com', // gmailのメールアドレスがユーザ名となる
			    'Password' => 'xxxx',                // ここにGmailのアドレスを入力する

この章の目標:

この章では掲示板のようなメッセージボードを設置します。仕様としては

  • 閲覧自体はログインしていないユーザでも可能
  • 投稿にはログインしたユーザのみが必要

 設計方針:

  • まずは投稿用のフォームのモデルの設計
  • 投稿処理を実装
  • 表示部分を作成

というようにします。


モデル・データベースの追加

まずはデータベース関連の設計と変更から行っていきます。以下のようにprotected/modelsの中にMessage.php を追加します。これはメッセージボードの投稿内容を保存するときのデータの定義を行います。

モデルの追加は慣れてきたと思いますのでここでは簡単にプロパティの内容だけ紹介するにとどめます。

  • uid : 投稿者のidを記録します。userのテーブルのidに対応しています。
  • subject: 投稿の件名です。
  • message : 投稿のメッセージの内容です。
  • time : 投稿の投稿時間を表すタイムスタンプです。

なおこのデータベースのMySQL上でのテーブル名は以下のソースにも記述されているようにbbsとします。

<?php

class Message extends CActiveRecord
{
 /**
 * @return string the associated database table name
 */
 public function tableName()
 {
     return 'bbs';
 }

 /**
 * @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('uid, subject, message, time', 'required'),
         // subject
         array('subject', 'length', 'min'=>8, 'max'=>64),
         // time
         array('message','length', 'min'=>32, 'max'=>2048),
 
         // The following rule is used by search().
         // @todo Please remove those attributes that should not be searched.
         array('id, uid, time', 'safe', 'on'=>'search'),
     );
 }

 /**
 * @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(
     );
 }
 
 /**
 * 検証前に投稿時間を挿入する
 */
 public function beforeValidate() {
     if($this->isNewRecord) {
         $this->time = time(); // 現在の時刻を挿入する
     }
     return parent::beforeValidate();
 }

 /**
 * @return array customized attribute labels (name=>label)
 */
 public function attributeLabels()
 {
     return array(
         'id'=>'ID',
         'uid' =>'ユーザID', // 投稿者ユーザID
         'subject' =>'件名', // 件名
         'message'=>'メッセージ', // メッセージ内容
         'time'=>'投稿時間', // 投稿時間
     );
 }

 /**
 * Retrieves a list of models based on the current search/filter conditions.
 *
 * Typical usecase:
 * - Initialize the model fields with values from filter form.
 * - Execute this method to get CActiveDataProvider instance which will filter
 * models according to data in model fields.
 * - Pass data provider to CGridView, CListView or any similar widget.
 *
 * @return CActiveDataProvider the data provider that can return the models
 * based on the search/filter conditions.
 */
 public function search()
 {
     // @todo Please modify the following code to remove attributes that should not be searched.
     $criteria=new CDbCriteria;
     $criteria->compare('id',$this->id,true);
     $criteria->compare('uid',$this->uid,true);
     $criteria->compare('time',$this->time,true);
     return new CActiveDataProvider($this, array(
         'criteria'=>$criteria,
     ));
 }
 /**
 * Returns the static model of the specified AR class.
 * Please note that you should have this exact method in all your CActiveRecord descendants!
 * @param string $className active record class name.
 * @return User the static model class
 */
 public static function model($className=__CLASS__)
 {
     return parent::model($className);
 }
}

さて次にデータベースにphpmyadminからbbsのデータベースを追加していきます。データベースの構造は以下のようになります。

chap8-1

しかし実際にそのまま同じものを作るのは面倒だと思いますので以下にダンプしたデータを置いておきます。こちらのbbs.sqlをダウンロードしてインポートしてください。インポートの方法はphpmyadminの「インポート」の項目からダウンロードしたSQLファイルを指定すればOKです。


 メニューの変更

次にサイトのメニューの項目のリンクをいじっておきます。protected/views/layoutsmain.phpを開きます。メッセージ閲覧メッセージ投稿のリンクを追加しておきます。なおメッセージ投稿はログイン済みのユーザしかできないので、ログイン後のユーザのみが表示されるようにYii::app()->user->isGuesttrueの場合のみ表示という条件が入っています。

<div id="mainmenu">
		<?php $this->widget('zii.widgets.CMenu',array(
			'items'=>array(
				array('label'=>'ホーム', 'url'=>array('/site/index')),
				array('label'=>'当サイトについて', 'url'=>array('/site/page', 'view'=>'about')),
				array('label'=>'お問い合わせ', 'url'=>array('/site/contact')),
				array('label'=>'ユーザ登録','url'=>array('/site/registration'),'visible'=>Yii::app()->user->isGuest),
				array('label'=>'メッセージ閲覧','url'=>array('/site/bbs'),), // chap8
				array('label'=>'メッセージ投稿','url'=>array('/user/bbs'),'visible'=>Yii::app()->user->isUser), // chap8
				array('label'=>'ログイン', 'url'=>array('/site/login'), 'visible'=>Yii::app()->user->isGuest),
				array('label'=>'ログアウト', 'url'=>array('/site/logout'), 'visible'=>!Yii::app()->user->isGuest)
			),
		)); ?>
	</div>

この変更によりページのメニュー部分が以下のように増えます。(以下はログインしていない場合)

chap8-2

 


投稿処理の追加

次にデータを受け取ってデータベースに反映をする投稿処理を追加していきます。ユーザから入力されるデータのチェックはModelを利用して実行します。protected/models の中にMessageForm.php というファイルを作ってください。とりあえず細かいところは後で解説するとして以下のないようをコピーしてください。

<?php

/**
 * MessageForm class.
 */
class MessageForm extends CFormModel
{
	    public $subject;
	    public $message;

    	/**
	     * Declares the validation rules.
	     */
	    public function rules()
    	{
		        return array(
			            // name, email, subject and body are required
		            	array('subject, message', 'required'),
		            	// タイトルは64文字まで
			            array('subject','length', 'min'=>8,'max'=>64),
			            // メッセージは2048文字まで
			            array('message','length', 'min'=>32,'max'=>2048),
		        );
	    }
	
	
	    /**
	     * データ検証の前に投稿される文字の処理を行う
	     */
	    protected function beforeValidate() {
        		// HTMLタグ等の変換を行う
		        $this->subject = htmlspecialchars($this->subject,ENT_QUOTES);
		        $this->message = htmlspecialchars($this->message,ENT_QUOTES);
		        return parent::beforeValidate();
	    }
	
    	/**
	     * Declares customized attribute labels.
	     * If not declared here, an attribute would have a label that is
	     * the same as its name with the first letter in upper case.
	     */
	    public function attributeLabels()
    	{
		        return array(
			            'subject'=>'件名',
			            'message'=>'メッセージ',
		        );
    	}
}

投稿者は事前にユーザ登録を行いログインしている人に限りますので、このフォーム(Model)で受け取るデータは各投稿の件名とメッセージ内容です。

既に先ほどデータベース側のモデルの定義をしましたが、そこは

  • uid : 投稿者のID
  • subject : 件名
  • message : メッセージ内容
  • time : 投稿時間

でした。ただしこの中でユーザが実際に入力するのはsubject(件名) と messsage(内容)だけだからです。

さてここからモデルの定義のソースを詳細に見ていきます。rules()のメソッドですがここは以前にも出てきたように単純に各要素に対して入力文字数の制限を行っているだけです。minは最小入力文字数を表しmaxは最長入力文字数を表し、入力データの文字長はこの間でなければなりません。

さて次にprotected function beforeValidate() というメソッドを見てください。このメソッドはルールの検証を行う前に自動的に実行されるものです。すなわち上記のrules()の文字数制限のチェックを行う直前でこのメソッドが実施されます。ではなぜこのようなメソッドを使っているのでしょうか?

それは、ユーザから受け取るデータにはHTMLのソースが含まれる可能性があるので、悪意のあるユーザからの攻撃を防ぐために、HTMLのコードを無害な形に変換してやる必要があります。その処理をbeforeValidateのメソッド内で行っています。またそれによってデータの文字数が変化してしまいます。例えば「<」や「>」は「&gt;」「&lt;」といった記号に変化するので、1文字が3文字に変化する場合もあります。そのためrules()に記載したデータ検証が行われる前に、その処理を行ってやる必要があったわけです。

~~

それでは次にフォームの表示と投稿処理の部分を見て行きましょう。protected/controllers/UserController.php こちらに以下のメソッドを追加しましょう。

	// chap8: BBSの投稿フォームの表示と投稿処理
	public function actionBBS() {
		$model = new MessageForm();
		
		// Ajax検証のための処理
		if(isset($_POST['ajax']) && $_POST['ajax']==='bbs-form')
		{
			echo CActiveForm::validate($model);
			Yii::app()->end();
		}
		
		// フォームに対して入力があった場合
		if(isset($_POST['MessageForm']))
		{			
			$model->attributes=$_POST['MessageForm'];
			// MessageFormのデータ検証を行う
			if($model->validate()) {
				// データベースに記録するデータ
				$bbs = new Message();
				$bbs->subject = $model->subject;
				$bbs->message = $model->message;
				$bbs->uid     = Yii::app()->user->id;
				// Messageの検証を行う
				if($bbs->validate()) {
					$bbs->save();
					$this->render('bbs_finish'); // 書き込み終了のviewを表示
					Yii::app()->end();
				}
			}
		}
				
		// フォームviewを表示
		$this->render('bbs_form',array('model'=>$model));
	}

また上記のメソッドが実行できるようにアクセス権を上記のように与えてやる必要があります。アクセスルールに以下のようにbbsのメソッドを追加しておきましょう。

	// アクセスルール
	public function accessRules()
	{
    	return array(
    		// 一般ユーザに対して以下に列挙するactionの実行を許可する
			array('allow',
				'actions'=>array('index',
				                 'test',
				                 'bbs', // chap8 : メッセージ機能
				                 ),
				'expression'=>'$user->isUser',
			),
			// 上記のルールに適合しないすべてのユーザを排除
			array('deny',
				'users'=>array('*'), // *: 全ユーザ ?: 匿名ユーザ @: 認証済みのユーザ
			),
		);
	}

上記のソースコードについて簡単に解説します。上記のようにAjaxの検証システム付のフォームを含む処理は以前の章でも制作したことがありますので、おおまかなプログラムの形式は前と同じです。今後もフォームの表示と処理を担うプログラムを書くときはこの形式をコピーして使えばいいと思ってください。

ここでは「フォームに対して入力があった場合」というコメントが書かれた部分について解説します。上記で作った入力検証用のフォームMessageForm のオブジェクト$modelにユーザからの入力を代入します。その後、$model->validate() で入力データに誤りがないかをチェックし、問題がなければそれがtrueになるので、Messageで定義した投稿データを格納するデータベースにデータを格納していく流れになります。

Messageuidに現在のアクセスしているユーザのidを格納し、subjectmessageMessageFormsubjectmessageをそのまま格納します。そして記憶力の良い方ではMessageの中には投稿時間を表すtimeがありましたが、それはどこで記録されているのかと疑問を持つかもしれません。たしかにここのソースコードでは書いていませんね。

実はMessageのモデルの方でこっそり定義していました。protected/models/Message.php を見て頂ければbeforeSaveのメソッドがあることに気づくかと思います。ここではデータが、新しくデータベースに登録される新規データの場合はtimeに現在時間を格納しなさいという命令が書かれています。save(データベースへの保存)を実行する直前に何かを処理させたい場合は、beforeSaveのメソッドが使えることを覚えておいてください。

 

さて次に表示部分ですが、protected/views/userの中に以下のようにbbs_form.phpbbs_finish.phpを作ってください。先ほどのUserControllerに対して追加したメソッドを見てもらえば分かるかと思いますが・・・・前者は入力フォームに対して何も入力がなかった時や、入力フォームのデータに対して誤りがあったときに表示されるフォームのデータです。後者は入力フォームから入力があり、なおかつそのデータに対して誤りがなく、無事にデータベースに書き込みができた際に表示されるものです。

bbs_form.php

<?php
/* @var $this SiteController */
/* @var $model ContactForm */
/* @var $form CActiveForm */

$this->pageTitle=Yii::app()->name . ' - メッセージボード';
$this->breadcrumbs=array(
	'メッセージボード',
);
?>

<h1>メッセージボード</h1>

<div class="form">

<?php $form=$this->beginWidget('CActiveForm', array(
	'id'=>'bbs-form',
	'enableAjaxValidation'=>true, // ajaxで処理
	'clientOptions'=>array(
		'validateOnSubmit'=>true,
	),
)); ?>

	<p class="note"><span class="required">*</span>の項目は入力が必須となります</p>
	<p class="note">HTMLタグは自動的に変換されます。</p>

	<?php echo $form->errorSummary($model); ?>

	<div class="row">
		<?php echo $form->labelEx($model,'subject'); ?>
		<?php echo $form->textField($model,'subject'); ?>
		<?php echo $form->error($model,'subject'); ?>
	</div>

	<div class="row">
		<?php echo $form->labelEx($model,'message'); ?>
		<?php echo $form->textArea($model,'message',array('rows'=>6, 'cols'=>50)); ?>
		<?php echo $form->error($model,'message'); ?>
	</div>

	<div class="row buttons">
		<?php echo CHtml::submitButton('書き込み'); ?>
	</div>

<?php $this->endWidget(); ?>

</div><!-- form -->

このフォームも毎回使いまわしているAjax検査入りのフォームと同じものです。今後、自分で新しいフォームを定義するときもだいたいこの形をコピーして使ってもらえれば大丈夫です。

次にbbs_finish.php ですが単純に書き込みが終わりましたという旨を表示するだけです。

<?php
$this->pageTitle=Yii::app()->name . ' - メッセージボード';
$this->breadcrumbs=array(
	'メッセージボード',
);
?>

<h1>メッセージ書き込み完了</h1>
<p>書き込みが完了いたしました。</p>

 


表示部分の処理

最後に表示部分の処理を作りましょう。表示に関してはログインをしていなくても外部から見ることができるという仕様でした。protected/controllers/SiteController.phpに以下のようにbbsというアクションを追加します。

	/**
	 * メッセージを表示させる (chap8)
	 */
	public function actionBBS() {	
		// データ表示用のモジュール
		$dataProvider = new CActiveDataProvider('Message',array('criteria'=>array('order'=>'time DESC'))); // 新しい物から表示する
		
		// 描画
		$this->render('bbs',array('dataProvider'=>$dataProvider));
	}

 

はい、ここで見慣れないものが出てきました。CActiveDataProviderというものが出てきましたね。これは何をするものかと言いますと、データベースのレコードの表示を若干楽にしてくれるものです。

new CActiveDataProvider('Message',array('criteria'=>array('order'=>'time DESC')));

ここではMessageのデータを「時間が新しいものから順に表示するのを手伝ってください」というおまじないだと思ってください。今は時間が新しい順番にという条件しか書いていませんが、本当はさらにより多くの細かい条件を指定し表示することもできます。いまは‘order’=>’time DESC’はtimeの数字が大きい物から順番に並べ替えよという意味になるため、時間が新しいモノから順に並べよということになります。

細かい条件の指定法としては、例えばそれが会員のデータだったとすると、年齢が30歳未満の女性の会員を年齢が若い順にならべよ といった指定などもできます。それにはcondition (=条件の意味)という指定を追加しますが、ここでは説明しません。今後機会があれば触れるかもしれません。必要な方はこちらのページを参照していただくと分かるかもしれません。

さて次にrenderで表示するviews/site/bbs.phpに以下のように記述をします。

<?php
$this->pageTitle=Yii::app()->name . ' - メッセージボード';
$this->breadcrumbs=array(
	'メッセージボード',
);
?>

<h1>メッセージボード</h1>
<?php $this->widget('zii.widgets.CListView', array('dataProvider'=>$dataProvider,'itemView'=>'bbs_table')); ?>

<p>メッセージを投稿する<a href="/user/bbs">こちら</a>からどうぞ。ログインしている必要があります。</p>

ここソースコード内において以下の処理が具体的にデータを表示している部分です。

<?php $this->widget('zii.widgets.CListView', array('dataProvider'=>$dataProvider,'itemView'=>'bbs_table')); ?>

 

‘itemView’ => ‘bbs_table’ の部分ですが、これは一つ一つの投稿データをどのように表示するかというHTMLのソースを定義している部分を指定しており、protected/vies/site/bbs_table.php を参照せよという意味です。

それではbbs_table.phpはどのように記述をするかというと以下を見てください。

<?php
	/* 表示するデータを予めここで処理する */
	$date = Date("Y-m-d",$data->time); // タイムスタンプを投稿時刻に変換
	$user = User::model()->findByPk($data->uid); // 投稿者のユーザデータ
?>
<ul>
	<li><b><?php echo $data->subject;?></b> <?php echo $date;?></li>
	<li><b>投稿者</b> <?php echo $user->name;?></li>
	<li><b>内容</b> <?php echo $data->message;?></li>
</ul>
<hr size="1">

これが一件、一件のデータに対して行われる処理です。各投稿データに対してforeachのループによって上記のソース内の処理されるというイメージを持ってもらえればよいかと思います。

 CActiveDataProviderを使うことで自分でデータベースに対してアクセスしてforeach文でループを回しながら各データを表示するといった処理をしなくてもよいのです。またデータ数が多くなれば自動的にページごとに表示を区切ってくれます。


今回の成果物

以下に今回作ったソースコードやデータベースのダンプデータを置いておきます。

使い方としてはhtdocsの中にchap8のソースコードをそのままコピーします。次にconfig/main.phpの中のGmailのメールアドレスとパスワードをご自身のgmailのデータに書き換えてください。その後、yiiという名前の空のデータベースに対して上記の2つのダンプデータをインポートしてやってください。そうすれば今回作ったものが実行できるはずです。流れとしてはまずはじめにユーザ登録で適当にユーザを追加して、ログインしてから何かメッセージを書き込んで、表示できるか確認するという流れになるかと思います。