WebTecNote

[PHP5] OOPで掲示板を作ってみる – Step3: 書き込み

オブジェクト指向プログラミングでフレームワークを使わずに掲示板を作るその3。
ログ手打ちするの面倒だから先に書き込み実装します。

目次

Step3

Step1~2でフォームの送信ボタンを押すとモデルから返されたメッセージが出るようになった。
Controllerを介してModelからViewへ受け渡すという仕組みは出来ている。

この先表示からやるか書き込みからやるかは人によって違うよね。

POST処理の振り分け

前の記事でModelはPOSTで実行する(キリッと言った手前、一応送信したらメッセージが出るようにしてた。
でもただそれだけじゃ削除とか編集とかを付ける時困るから、動作を振り分けるキーとしてmodeを送信する。

template/index.tpl

<div class="button"><button type="submit" name="mode" value="write">Submit</button></div>

送信ボタンにmodeという名前をつけて値にwriteを入れておく。

lib/controller.php (class Controller -function execute)

if(isset($_POST['mode']) && $_POST['mode'] != ''){
	
    switch($_POST['mode']){
		
        case 'write':
		
            $modelInstance->setPostdata($_POST);//送信内容のセット
			
            break;
		
        default:
    }
}

POST配列にmodeがあった時だけ反応するように変更。
編集とか削除が増えることを見越してSwitchで切り替えるようにした。

Modelに送信内容を保持させる

データに関することはModelにお任せなので、POSTをそのまま丸投げ。
あとでバリデーションとか保存とかの仕事を押し付けられることも想定して、
Modelではメンバ変数に投げられたデータを一旦保存しておくことにする。ってことで投げる先はセッター。

コンストラクタでフォームのnameキーとメンバ変数$postの初期化をする。
キーを元に投げられた$_POSTから必要なデータを仕分けて保存。
一緒にバリデートもしときたい、という欲が出てもぐっと我慢。だってセッターだもの。

lib/model.php (class Model)

//初期化
public function __construct()
{
    $this->_inputs = array('name', 'comment'); //フォームのname
    $this->_post   = array();                         //$_POSTが入る
}
/**
 * 送信された内容をメンバ変数にセット
 */
public function setPostdata($posts)
{
	foreach($this->inputs_ as $target){
		$data[$target] = $posts[$target];
	}
	
	$this->setPost($data);//セッターに渡す
	
	return;
}

$data[$target]$this->post_[$target] とせず
抜き出したデータをセットするのは$_POST専用セッターにお任せ。

lib/model.php (class Model)

/**
 * 送信されたデータのセッター
 */
private function _setPost($data)
{
	$this->_post = $data;
}

/**
 * 送信されたデータのゲッター
 */
private function _getPost()
{
	return $this->_post;
}

ついでにゲッターも書いておいた。

データのバリデーション

仕分けが出来たら次はバリデーションで、その後に保存。
こういう感じだろうという流れを思いつくまま書いてみる。

lib/controller.php (class Controller – function execute)

case 'write':

	$modelInstance->setPostdata($_POST);//送信内容のセット
	
	if($modelInstance->validation()===true){

            $modelInstance->write();//ログに書き込み
            $viewInstance->setSystemMessage('保存しました');//メッセージをセット

   }else{
           $viewInstance->setErrorMessage($modelInstance->getError());//エラーをビューに渡す
   }

break;

これでいけそうな気がする 😀 からそのまま突っ走ります。

バリデーションは簡単に空送信だけ防御しておく。細かいことはあとでやればいいしね。
ゲッターで保存されている送信内容を受け取ってチェック。
エラーメッセージはセッターでメンバ変数$_errorに渡す。

lib/model.php (class Model)

/**
 * データのバリデーション
 */
public function validation()
{
	$data = $this->_getPost();
	$error = array();
	
	foreach($data as $key => $value){
		if($key =='name' && $value === ''){
			$error[] = '名前を入力してください';
			
		}elseif($key=='comment' && $value === ''){
			$error[] = 'コメントを入力してください';
			
		}
	}
	
	$this->_setError($error);
	
	if(count($error)>0){
		return false;
	}else{
		return true;
	}
}

エラーのセッターとゲッターを作る。

lib/model.php (class Model)

/**
 * エラーのセッター
 */
public function setError($error)
{
	$this->_error = $error;
}
/**
 * エラーのゲッター
 */
public function getError()
{
	return $this->_error;
}

よし次は保存…と行きたい所だが後回しにして先にエラー表示をやっつける。

エラーメッセージの表示

先にcontrollerに書いたソースではモデルからgetしたエラーをViewにsetするものだった。
エラーでも同じ事だから、まずはViewでエラーのセッターを作る。メッセージのセッターをコピペ改変で。
ゲッターはgetSystemMessageと共通。

lib/view.php (class View)

/**
 * エラーメッセージのセッター
 */
public function setErrorMessage($text)
{
	$this->_systemMessage = $text;
}

そのまま送信ボタン押すと表示されるのはArray。displayメソッドで文字列に変換。

lib/view.php (class View)

/**
 * 画面に表示する
 */
public function display()
{
     $message = '';
        
     if( is_array($this->getSystemMessage()) ){
        $message = '<p class="message">'.implode('<br />', $this->getSystemMessage()).'</p>';
    }else{
        $message = '<p class="message">'.$this->getSystemMessage().'</p>'; 
    }

    $logdata = '';
        
    include($this->getTemplateName());
}

これで送信ボタン押したらエラー出るようになった。
ちゃんと記入して送信してもエラーが出るから次は保存を作る。

データの保存

保存形式はCSVにする。ログに保存するのは名前とコメントのほかに投稿日時も欲しい。
日時を保存するときはタイムスタンプそのままでいいと思う。

保存するとなるとデータディレクトリとデータファイルの設定が要るから、
テンプレートと同じ要領でメンバ変数とセッター・ゲッターを作っておく。

lib/modex.php (class Model)

const  DATA_DIRECTORY = 'dat/';
private $_logFilename;

public function __construct()
{
    $this->_logFilename = '';                  //ログファイル名
}
/**
 * ログファイル名のゲッター
 */
private function _getLogfileName()
{
    return self::DATA_DIRECTORY . $this->_logFilename;
}

データは配列の値だけarray_valuesで抜き出してから先頭にタイムスタンプ追加。
array_filterでタグの除去とかをしてからfputcsvにファイルポインタと共に渡せばCSVに整形して書き込まれる。

lib/modex.php (class Model)

/**
 * データの保存
 */
public function write()
{
    $data = $this->_getPost();
        
    $data = array_map(array($this,'_clean'), $data);//クリーンアップ
    
    array_unshift($data, time());//先頭にタイムスタンプ

    $log = file_get_contents($this->_getLogfileName());//ログ確保
	
    $fp = fopen($this->_getLogfileName(), 'w');

    fputcsv($fp, $data);
    fwrite($fp, trim($log));
	
    fclose($fp);
}
/**
* 値をクリーンアップする
*/
private function _clean($str)
{
    $str = strip_tags(trim($str));
    $str = preg_replace('/<(script|style).*?</(script|style)>/sm','', $str);//script
    $str = preg_replace("/<!--.*-->/sm", '', $str); //comments
    $str = htmlentities($str, ENT_QUOTES, "UTF-8");
    $str = str_replace(array("¥rn", "¥r", "¥nn","¥n", "¥0"), "", nl2br($str));//¥が全角
    $str = str_replace(array("  ", ",", '"'), array("t", '&#044;', '"'), $str);
        
    if (get_magic_quotes_gpc()) {
        $str = stripslashes($str);
    }
    return $str;
}

試しに書き込んでみた後のログはこんな具合になった。

一見ちゃんとCSVになってるように見えるのだけど、fputcsvには凄い落とし穴があって
エスケープすべき文字が入っていないと$enclosureで指定した文字で囲んでくれない。
上の画像にあるログはタグを含むコメントはちゃんと囲まれてるけど、そうでないコメントと名前はそのまんまになってる。
$enclosureで囲まれてない日本語をfgetcsvに通すと文字化けを引き起こすことがあるので、
日本語を含む文字列を囲むfputcsvを自作して使います。

/**
 * $enclosure問題解決用のfputcsv
 *
 * 日本語文字列が囲まれてないとfgetcsvしたとき文字化けする。
 * オリジナルのfputcsvはエスケープ文字が含まれているときしか囲ってくれないから
 * 半角英数以外の文字が含まれていたら$enclosureで囲むようにする。
 * マニュアルのコメントから拝借したソースをカスタマイズ。
 * 
 * @param {fp} $handle ファイルポインタ
 * @param {array} $fields 値の配列
 * @param {string} $delimiter フィールド区切り文字 
 * @param {string} $enclosure フィールドを囲む文字
 * @return {numeric, false} 書き込んだバイト数、またはエラー時に FALSE(fwriteと同じ)
 * @since 1.3 (step3)
 */
private function myFputcsv($handle, $fields, $delimiter=',', $enclosure='"', $mysql_null = false)
{
    $delimiter_esc = preg_quote($delimiter, '/');
    $enclosure_esc = preg_quote($enclosure, '/');
    
    $output = array();
    foreach ($fields as $field) {
        if ($field === null && $mysql_null) {
            $output[] = 'NULL';
            continue;
        }
    
        $output[] = preg_match("/(?:${delimiter_esc}|${enclosure_esc}|\s|[^\x01-\x7e])/", $field) 
                    ? ($enclosure . str_replace($enclosure, $enclosure . $enclosure, $field) . $enclosure)
                    : $field;
    }
    
    return fwrite($handle, join($delimiter, $output) . "\n"); 
}

このメソッドを通せば名前もエスケープされる。

リロード対策とかはまだしてないけど一応書き込み完成 😀
次は表示だ!と意気込みながらStep4に続く。

ディレクトリ構成

Step3 Download

ソースファイルのライセンスは Creative Commons BY-NC です。
ローカルサーバーでテストしてください。

“OOPでBBS Step3” をダウンロード

oop-bbs-step3.zip – 1425 回のダウンロード – 7.51 KB
モバイルバージョンを終了