[PHP5] OOPで掲示板を作ってみる – Step6:編集&削除画面

オブジェクト指向プログラミングでフレームワークを使わずに掲示板をcreateその6。
編集と削除の機能を作る前に画面を作らねばならぬ!

目次

  • 序章
  • Step1 : MVCを揃える
  • Step2 : テンプレート作成
  • Step3 : 書き込み
  • Step4: ログの表示
  • Step5: 設定ファイルと細かな修正
  • Step6: 編集&削除画面 ← 今ここ
  • Step7: 編集&削除機能
  • Step8: ページング

Step5でメッセージの色分けとかしたよー

step5-message

2重送信も防止しといたし、とりま足りないのは編集削除機能だ。
編集削除をユーザーにやらせるか管理者がやるかで実装内容がかなり変わってくるんだけど
手始めにユーザーが弄れるようにしてみようと思う。

ユーザーが編集・削除をするとなると、新たに必要になってくるものがいくつか出てくる。

  1. 投稿を区別する一意なID
  2. その投稿がユーザーのものであるという証

IDは記事の投稿順に連番で数字を付けるのが妥当かな。掲示板だしね。
ユーザーの証は記事を投稿するときに秘密の鍵を一緒に投稿させるようにして、それをハッシュ化してログに一緒に保存すれば簡単ですね。

ユーザーに操作させるかどうかは任意で決めれたほうが便利だろうから、
iniファイルに新たな設定を追加しときます。

lib/bbs.ini

;ユーザーによる編集・削除操作
user_controll = true

trueまたは1なら許可するって感じに。

記事にIDを付与する

データベースなら「おーといんくりめんと」っていう便利なものが存在するけど、ファイル関数にそんな甘ったるい機能は無いから自前で何とかするしかない。
思いつくのは2つ。単純にログをカウントしてIDにする方法と、ID用のログファイルを作って保存しておく方法。
より厳密にするなら後者。そうでないなら前者かな。今回は説明の手間を省きたいので前者にしとくw

書き込みをする関数のfile_get_contentsをfileに変更
一番上が最新の書き込みなので、新しくIDを計算するメソッドを作ってそこで次のIDを算出。

lib/mode.php (class Model)

/**
 * ログIDの算出
 */
private function _calcPostID($log)
{
	$id = 0;
	
	if(!empty($log)){
		list($id, $str) = split(',',$log[0]);
	}
	
	return $id + 1;
	
}

/**
 * データの保存
 */
public function write()
{
	$data = $this->_getPost();
	$id = 0;
	
	$data = array_map(array($this,'clean'), $data);//クリーンアップ
	
	$log = file($this->_getLogfileName());//ログ確保
	
	$id = $this->_calcPostID($log);//ID

	$log = implode('', $log);//文字列化
	
	array_unshift($data, $id, time());//先頭にIDとタイムスタンプ

	$fp = fopen($this->_getLogfileName(), 'w');

	$this->myFputcsv($fp, $data);
	fwrite($fp, trim($log));
	
	fclose($fp);
	
	return;
}

配列のログはimplodeで文字列に変更。array_unshiftにはタイムスタンプの前にIDを渡す。
後の流れは前バージョンと同じ。

このまま実行すると読み込みの時にエラーが出てしまうので、readLogメソッドのarray_unshiftにidを追加しておく。

lib/mode.php (class Model – function readLog)

array_unshift($label, 'id', 'date');

これで書き込みすればIDがつくようになりました~

テンプレートの編集

画面はGETで操作するんで、ログのIDと、編集・削除画面に飛ばす為のリンクをテンプレートに追加する。

template/index.tpl

<div id="main">
<?php if( !empty($logdata) ) : ?>
	<?php foreach($logdata as $log):   ?>
	<div class="log">
	<p>[<?php $log->the_('id'); ?>] <strong class="name"><?php $log->the_('name'); ?></strong> <span class="date"><?php $log->the_('date','Y/m/d H:i'); ?></span></p>
	<p class="comment"><?php $log->the_('comment'); ?></p>
	<p class="mode">
		<a href="?view=edit&amp;id=<?php $log->the_('id'); ?>">Edit</a> | 
		<a href="?view=delete&amp;id=<?php $log->the_('id'); ?>">Delete</a>
	</p>
	</div>
	<?php endforeach; ?>
<?php else: ?>
<p>書き込みはありません<p>
<?php endif; ?>
</div>

ついでにログが無いときのメッセージも入れておいた。

編集と削除のリンクはviewでeditとdelete、idで記事のIDを渡します。

oop-bbs-step6

テンプレートファイル作成

templateディレクトリにviewキーで渡す値と同じファイル名のテンプレートを作る。
edit.tplが編集画面、delete.tplが削除画面です。
中は後でやるのでここは適当に

<h2>Edit</h2>
<?php var_dump($logdata); ?>

などと入れておく。

Controllerの編集

編集or削除のリンクをクリックすると、$_GET[view]にedit|deleteが、
$_GET[‘id’]に記事のIDが入った配列をControllerが受け取る。
これを$_POSTの時と同じように条件分岐で処理させると、

if(isset($_GET['view']) && $_GET['view'] != ''){
    $viewInstance->setTemplateName($modelInstance->clean($_GET['view']));//テンプレートをセット
}

$_GET[view]の値をsetTemplateNameに渡すだけで画面が切り替わります。
掲示板のログはModelのreadLog()でファイルから読み込んだ後に、ViewのsetLog()から渡してます。
1件だけ欲しいとなると、これらのメソッドの間にIDで記事を探すメソッドを加えればいいということになります。

何も考えずにベタな書き方をすればこうなるんだろうけども…

$modelInstance->readLog();//ログを読み込む
if((isset($_GET['view']) && $_GET['view'] == 'edit') || (isset($_GET['view']) && $_GET['view'] == 'delete')){
	$viewInstance->setLog($this->model->記事を探す($_GET['id']));//Viewにログを渡す
	$viewInstance->setTemplateName($modelInstance->clean($_GET['view']));//テンプレートをセット
}else{
	$viewInstance->setLog($this->model->getLogData());//Viewにログを渡す
	$viewInstance->setTemplateName('index');//テンプレートをセット
}

editとdeleteの他にページが増えたらif文がどんどん長くなっていく罠…。
使い勝手とか使いまわしの事を考えるとeditとかdeleteとかの値をいちいちチェックしていない方が良いし、
PHPには可変変数で関数の実行やインスタンスの作成が出来るという特徴があるんで、それを使って書きます。

$modelInstance->readLog();//ログを読み込む
        
$viewName = 'index';

if(isset($_GET['view']) && $_GET['view'] != ''){
	$viewName = $modelInstance->clean($_GET['view']);
}

$viewInstance->setTemplateName($viewName);//テンプレートをセット

$viewName = $viewName.'View';

if(method_exists($this, $viewName)){
	$this->$viewName();//ビュー実行
}else{
	die('システムエラーが発生しました');
}

$_GET[‘view’]に入ってる値をテンプレート名として利用しつつ、Viewという単語を足してメソッドを自動的に実行するようにしてみた。
これならif文の条件が延々増えるってことはないし、見た感じ処理の内容が分かりやすいです。
ただこの方法だと実行先のメソッドで$viewInstance$modelInstanceのローカル変数が使えなくなってしまうので、
各メソッド内でファイルを読み込んでインスタンスを作るか、Controllerのコンストラクタでファイルを読み込んでプロパティにする必要があります。
引数として渡すっていうのはクールじゃないから却下 😐
今回はファイル数が多いからコンストラクタでプロパティ化を採ります。

インスタンスプロパティ

ファイルのインクルードとインスタンス作成部分をコンストラクタにごっそりお引越し。
置換で$viewInstance$this->viewに、$modelInstance$this->modelにします。

lib/controller.php (class Controller)

public function __construct()
{
    include_once('model.php');
    include_once('view.php');
    include_once('bbs_log.php');
    include_once('config.php');
        
    $iniFilePath = dirname($_SERVER['SCRIPT_FILENAME']).'/lib/'.self::INIT_FILE;
        
    $this->config = new Config($iniFilePath);//設定
    $this->model = new Model($this->config);//モデル
    $this->view = new View($this->config);//ビュー
}

executeの方はログファイルのセットをした後、ログを読み込んでテンプレートのセット。
それからビューメソッド実行という順にします。
ついでにPOSTの方もActionという単語を付けてメソッドを実行するようにしておきました。

lib/controller.php (class Controller)

public function execute()
{
    $this->model->setLogfileName($this->config->get('logfile_name'));//ログファイルをセット
	
	if(isset($_POST['mode']) && $_POST['mode'] != ''){
		
		//トークンの確認
		$token = $this->model->clean($_POST['token']);
		
		if($token == @$_COOKIE["_token"]){
			$this->view->setSystemMessage('error', '不正な送信です');//エラーをViewに渡す
			
		}else{
			setcookie("_token", $token);
			$action = $this->model->clean($_POST['mode']).'Action';
			$this->$action();
		}
	}
	
	$this->model->readLog();//ログを読み込む
	
	$viewName = 'index';
	
	if(isset($_GET['view']) && $_GET['view'] != ''){
				
		$viewName = $this->model->clean($_GET['view']);
		
	}
	
	$this->view->setTemplateName($viewName);//テンプレートをセット
		
	$viewName = $viewName.'View';
	
	if(method_exists($this, $viewName)){
		$this->view->setToken($this->createToken());//Viewにトークンを渡す
		$this->$viewName();//ビュー実行
	}else{
		die('システムエラーが発生しました');
	}
	
}

各メソッドは次のようになります。

lib/controller.php (class Controller)

// 書き込み
function writeAction()
{
	$this->model->setPostdata($_POST);//送信内容のセット

	if($this->model->validation()===true){
		$this->model->write();//ログに書き込み
		$this->view->setSystemMessage('success', '保存しました');//メッセージをセット
		$this->model->readLog();//再読み込み
	}else{
		$this->view->setSystemMessage('error', $this->model->getError());//エラーをViewに渡す
	}
}
//削除処理
function deleteAction()
{
}

//編集処理
function editAction()
{
}

//インデックス
private function indexView()
{
	$this->view->setLog($this->model->getLogData());//Viewにログを渡す
	$this->view->display();//表示
}

//編集画面
private function editView()
{
	$log = $this->model->findData($this->model->clean($_GET['id']));
	
	$this->view->setLog($log);//Viewにログを渡す
	$this->view->display();//表示
}

//削除画面
private function deleteView()
{
	$log = $this->model->findData($this->model->clean($_GET['id']));
	
	$this->view->setSystemMessage('','以下の記事を削除しますか?');
	$this->view->setLog($log);//Viewにログを渡す
	$this->view->display();//表示
}

これで表示の分岐は出来たのでIDを元に記事を探すメソッド(findData)を作るとします。
追加するのはModelクラスです。

lib/model.php (class Model)

public function findData($id)
{
	$logs = $this->getLogData();
	
	if(empty($id)) return false;
	
	foreach($logs as $log){
		if($log->id == $id) return $log;
	}
	
}

foreachで探すだけっていう。
これで編集と削除画面がエラーなく表示されるようになりました。

編集画面と削除画面用テンプレートの編集

ロジックが出来たからやっつけver_dumpだったテンプレートを真面目に作る。…といっても9割方index.tplそのまま。

edit.tplもdelete.tplも、$logdataの中身はIDを指定して渡されるログのオブジェクトが1つだけ入ってます。
ログの表示は出力用メソッドの変数を$logから$logdataにするだけ。

edit.tplはフォームにログをセットしますが、テキストエリアにセットする値に改行が含まれているとタグが見えてしまうので、
タグを改行文字コードに置換する処理を入れます。

oopbbs-step6-edit

template/edit.tpl

<h2>Edit</h2>
<?php echo $message; ?>
<?php if(!empty($logdata)) : ?>
<form action="" method="post">
<input type="hidden" name="token" value="<?php print($token);?>" />
<input type="hidden" name="id" value="<?php $logdata->the_('id'); ?>"/>
<table>
<tr><th scope="row"><label for="name">Name</label></th><td><input type="text" id="name" name="name" value="<?php $logdata->the_('name'); ?>" /></td></tr>
<tr><th scope="row"><label for="comment">Comment</label></th>
<td><textarea id="comment" name="comment" rows="4" cols="50"><?php print $logdata->the_('comment','br2nl') ?></textarea></td>
</tr>
</table>
<p>投稿時に設定したキーを入力してボタンを押してください。</p>
<div class="button">
	<label for="key">Key</label>:<input type="password" id="key" name="key" />
	<button name="mode" value="delete">Delete</button>
</div>
</form>
<?php endif; ?>
<p class="center"><a href="index.php">トップに戻る</a></p>

lib/bbs_log.php (class Log)

    public function getThe_($name, $option=NULL)
    {
        $data = $this->{$name};
        
        if(!empty($option)){
            if($name=='date'){
                return date($option, $data);
            }elseif($option == 'br2nl'){//改行タグを改行コードに変換
            	return str_replace('<br />', '', $data);
            }
        }else{
            return $this->{$name};
        }
    }

ヘルパーとか静的クラスとかを作る手もあるのだけど、テンプレートでの使いやすさ優先でLogクラスに。

削除画面は確認で表示するだけ。

oopbbs-step6-del

template/delete.php

<h2>Delete</h2>
<?php echo $message; ?>
<div id="main">
<?php if(!empty($logdata)) : ?>
	<form action="" method="post">
	<input type="hidden" name="token" value="<?php print($token);?>" />
	<input type="hidden" name="id" value="<?php $logdata->the_('id'); ?>"/>
   
	<div class="log">
	<p>[<?php $logdata->the_('id'); ?>] <strong class="name"><?php $logdata->the_('name'); ?></strong> <span class="date"><?php $logdata->the_('date','Y/m/d H:i'); ?></span></p>
	<p class="comment"><?php $logdata->the_('comment'); ?></p>
	</div>
	
	<p>投稿時に設定したキーを入力してボタンを押してください。</p>
	<div class="button">
		<label for="key">Key</label>:<input type="password" id="key" name="key" />
		<button name="mode" value="delete">Delete</button>
	</div>
	</form>
<?php endif; ?>
	<p class="center"><a href="index.php">トップに戻る</a></p>
</div>

画面は出来たけどボタンを押しても何も起きないので、次は編集&削除のロジックを入れますよ。

ディレクトリ構成

oopbbs-step6-dir

Step6 Download

動作確認等はローカルで行ってください。ライセンスはCreative Commons BY-NCです。

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

oop-bbs-step6.zip – 1421 回のダウンロード – 15.17 KB

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください