こんにちは、しょごです。
今春アニメは鋼鉄城のカバネリが最高でしたね、生駒のコスプレしたいです。菖蒲様が可愛すぎます、おまんじゅうあげたいです、菖蒲様の血すすりたいです、ハァ、マジ尊い。。
さて、この連載では簡単な掲示板を構築しながら、バックエンド技術をイチから学んでいきます。
【プログラミング構築】データベースにアクセスして投稿データを抽出しよう
前回は登録したデータを抽出するまで操作しました。今回はデータの更新・削除をしてみようと思います。
今回実装する機能について
基本的には前回のデータ登録のような処理を行います。
データベースにアクセスし、該当データに新たな情報を更新しましょう。SQL文は、更新にupdate、削除にdeleteを利用します。
しかし、現状で更新・削除ロジックを入れてしまうと誰でも他人の投稿を他人が更新削除を行えてしまうので、当初の仕様通り「管理モード」をつけます。
投稿画面の下の方に、管理モードにログインするためのフォームを設けたいと思います。昔の掲示板によくあったやつですね。(セキュリティを考えるともっと違う形の方がいいのですが、今回は割愛するということで。。)
いずれにしても、特定条件のみ更新と削除ができるという機能を設けます。
管理モード準備
それでは、投稿画面の下の方に管理画面へのログインフォームを設けましょう。eventIdに投稿するときとは別のフラグを管理画面へ渡すことで処理を分岐させます。
▼view/post.php
<?php
// 登録データ取得
$post_datas = $action->getDbPostData();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>掲示板やんなー</title>
</head>
<body>
<h1>掲示板やんなー</h1>
<!-- 入力エリア -->
<div class="input_area">
<form action="./index.php" method="post" id="contact_form">
<dl class="name">
<dt>名前</dt>
<dd><input type="text" name="name" value=""></dd>
</dl>
<dl class="email">
<dt>メールアドレス</dt>
<dd><input type="text" name="email" value=""></dd>
</dl>
<dl class="body">
<dt>本文</dt>
<dd><textarea name="body"></textarea></dd>
</dl>
<input type="hidden" name="eventId" value="save">
<input type="submit" value="送信">
</form>
</div>
<!-- //入力エリア -->
<hr>
<!-- 投稿表示エリア -->
<?php if (!empty($post_datas)) {?>
<div class="list">
<?php foreach ($post_datas as $post) { ?>
<div class="item">
<div class="name"><?php if (!empty($post["email"])) {?><a href="mailto:<?php echo $post["email"];?>"><?php } ?><?php echo $post["name"];?><?php if (!empty($post["email"])) {?></a><?php } ?></div>
<div class="body"><?php echo nl2br($post["body"]);?></div>
<div class="date"><?php echo $post["created_at"];?></div>
</div>
<?php } ?>
</div>
<?php } ?>
<!-- // 投稿表示エリア -->
<!-- // ここから追記----------------------->
<hr>
<p>管理モード</p>
<!-- エラーエリア -->
<?php if (!empty($errm)) {?>
<div class="error">
<?php foreach($errm as $key => $value) {
echo $value;
}?>
</div>
<?php }?>
<form action="./index.php" method="post">
<input type="hidden" name="eventId" value="login">
<input type="password" name="password">
<input type="submit" value="送信">
</form>
</body>
</html>
投稿とは別の入力フォームを用意しています。
ログイン用パスワードを付与して、モードを切り替えました。投稿フォームのformタグと別に分けることで、送信するPOSTデータを簡易的に分けられます。
また、パスワード一致に失敗した場合は、エラー文言を返すようにしています。(昔の掲示板によくあったよね、こういうの。。)
続いて、パスワードが正しいかどうかをチェックするロジックを用意します。
パスワードはデータベースなどで別途ユーザ管理をしてもいいと思いますが、今回は簡易的に定数で特定文字列を保持しておき、そちらにPOSTデータで送られてきた文字列が一致するかをチェックする形とします。
まずはパスワードとなる文字列を設定し、下記ファイルに一行追記します。
▼config/properties.php
/**
* DB接続情報
*/
// 接続データベース情報(本番)
define('DATABASE_NAME','bbs_db');
define('DATABASE_USER','xxxxx');
define('DATABASE_PASSWORD','xxxxxx');
define('DATABASE_HOST','localhost');
define('PDO_DSN','mysql:dbname=' . DATABASE_NAME .';host=' . DATABASE_HOST);
// ここを追記---------------
// 管理モードパスワード
define('LOGIN_PASSWORD','anegasaki_nene');
さらに、パスワードをチェックするロジックを用意します。
POSTデータを受け取り、先ほど定数化したパスワードとなる文字列と一致しているかをチェックし、間違っていた場合はエラー文言を返すようにします。
▼class/business/getFormAction.php
<?php
class getFormAction {
public $pdo;
/**
* コネクション確保
*/
function __construct() {
try {
$this->pdo = new PDO( PDO_DSN, DATABASE_USER, DATABASE_PASSWORD, array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));
} catch (PDOException $e) {
echo 'error' . $e->getMessage();
die();
}
}
/**
* 記事データをDBに保存
*/
function saveDbPostData($data){
// データの保存
$smt = $this->pdo->prepare('insert into post (name,email,body,created_at,updated_at) values(:name,:email,:body,now(),now())');
$smt->bindParam(':name',$data['name'], PDO::PARAM_STR);
$smt->bindParam(':email',$data['email'], PDO::PARAM_STR);
$smt->bindParam(':body',$data['body'], PDO::PARAM_STR);
$smt->execute();
}
/**
* 記事データをDBから読み込み
*/
function getDbPostData(){
// 保存前に再度コードの利用フラグチェック
$smt = $this->pdo->prepare('select * from post order by created_at DESC limit 100');
$smt->execute();
// 実行結果を配列に返す。
$result = $smt->fetchAll(PDO::FETCH_ASSOC);
return $result;
}
// ここを追記--------------------------
/**
* 入力されたパスワードをチェックする。
*/
function checkAdminMode($password){
$erm = array();
// 入力データが存在するか
if (!empty($password)) {
// 入力データとパスワードが一致するか
if ($password != LOGIN_PASSWORD) {
$errm["login"] = "パスワードが違います。";
}
}
return $erm;
}
}
続いて、eventIdによって表示させる画面をスイッチさせます。
▼index.php
<?php
require_once("./config/properties.php");
require_once('./class/business/getFormAction.php');
$action = new getFormAction();
$eventId = null;
// イベントID取得
if (isset($_POST['eventId'])) {
$eventId = $_POST['eventId'];
}
switch ($eventId) {
// DBsave
case 'save':
$action->saveDbPostData($_POST);
require("./view/post.php");
break;
// ここから 追記------------------------------
// admin
case 'login':
// パスワードが一致するかチェック
$errm = $action->checkAdminMode($_POST["password"]);
if (empty($errm)) {
require("./view/admin_list.php");
} else {
require("./view/post.php");
}
break;
// 初回アクセス時、投稿画面表示
default:
require("./view/post.php");
break;
}
?>
加えて、ログインした後の画面を用意します。
ここでは登録データを並べ、編集と削除を行えるページを作ります。名前・投稿内容と投稿日付と編集削除を行うリンクをそれぞれ付与したものです。
変更・削除を行いたい記事のリンクをクリックすることで、それぞれ実行できるようにします。
▼view/admin_list.php
<?php
// 登録データ取得
$post_datas = $action->getDbPostData();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>管理モードやんなー</title>
</head>
<body>
<h1>管理モードやんなーー</h1>
<!-- 投稿表示エリア -->
<?php if (!empty($post_datas)) {?>
<table width="100%" border="1">
<tr>
<th>名前</th>
<th>内容</th>
<th>日付</th>
<th>編集</th>
<th>削除</th>
</tr>
<?php foreach ($post_datas as $post) { ?>
<tr>
<td><?php if (!empty($post["email"])) {?><a href="mailto:<?php echo $post["email"];?>"><?php } ?><?php echo $post["name"];?><?php if (!empty($post["email"])) {?></a><?php } ?></td>
<td><?php echo mb_substr($post["body"], 0, 15);?>..</td>
<td><?php echo $post["created_at"];?></td>
<td align="center" valign="middle">
<form action="./index.php" method="post">
<input type="hidden" name="eventId" value="edit">
<input type="hidden" name="id" value="<?php echo $post["id"];?>">
<input type="submit" value="変更">
</form>
</td>
<td align="center" valign="middle">
<form action="./index.php" method="post">
<input type="hidden" name="eventId" value="delete">
<input type="hidden" name="id" value="<?php echo $post["id"];?>">
<input type="submit" value="削除">
</form>
</td>
</tr>
<?php } ?>
</table>
<?php } ?>
<!-- // 投稿表示エリア -->
<hr>
<p><a href="./">掲示板に戻る</a></p>
</body>
</html>
ここまで用意すれば管理モード画面を表示することができ、ページ下部で正しいパスワードを入力すると変更・削除を行えるようになります。
特定データを更新する
つづいて、記事の変更ボタンをクリックすると変更画面へ遷移するロジックを設けます。
更新フラグに加えて変更したい記事の固有IDを渡し、データベースから該当IDの記事データを取得。登録したときに用いた入力フォームと同様のエリアを表示させ、それぞれにデータ付与した状態を構築します。ここで変更したいデータを入力し、POST送信することでデータを更新します。
まずは、特定IDのデータを取得するロジックと、更新処理のロジックを用意します。
特定IDのデータを取得するロジックは、すでにある投稿データを取得するロジックを改修しましょう。投稿データを取得する関数に引数でIDを付与すると、そのIDのデータのみ取得するようにします。
▼class/business/getFormAction.php
<?php
class getFormAction {
public $pdo;
/**
* コネクション確保
*/
function __construct() {
try {
$this->pdo = new PDO( PDO_DSN, DATABASE_USER, DATABASE_PASSWORD, array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));
} catch (PDOException $e) {
echo 'error' . $e->getMessage();
die();
}
}
/**
* 記事データをDBに保存
*/
function saveDbPostData($data){
// データの保存
$smt = $this->pdo->prepare('insert into post (name,email,body,created_at,updated_at) values(:name,:email,:body,now(),now())');
$smt->bindParam(':name',$data['name'], PDO::PARAM_STR);
$smt->bindParam(':email',$data['email'], PDO::PARAM_STR);
$smt->bindParam(':body',$data['body'], PDO::PARAM_STR);
$smt->execute();
}
// ここを下記に変更する-------------------------
/**
* 記事データをDBから読み込み
*/
function getDbPostData($id = null){
if (!empty($id)) {
// 指定投稿IDのデータを取得
$smt = $this->pdo->prepare('select * from post where id = :id');
$smt->bindParam(':id',$id, PDO::PARAM_INT);
} else {
// 投稿データを取得
$smt = $this->pdo->prepare('select * from post order by created_at DESC limit 100');
}
$smt->execute();
// 実行結果を配列に返す。
$result = $smt->fetchAll(PDO::FETCH_ASSOC);
return $result;
}
/**
* 入力されたパスワードをチェックする。
*/
function checkAdminMode($password){
$errm = array();
// 入力データが存在するか
if (!empty($password)) {
// 入力データとパスワードが一致するか
if ($password != LOGIN_PASSWORD) {
$errm["login"] = "パスワードが違います。";
}
}
return $errm;
}
// ここを追記する----------------------------------
/**
* 投稿を更新する
*/
function updateDbPostData($data){
// データの保存
$smt = $this->pdo->prepare('update post set name = :name, email = :email, body = :body, updated_at = now() where id = :id');
$smt->bindParam(':name',$data['name'], PDO::PARAM_STR);
$smt->bindParam(':email',$data['email'], PDO::PARAM_STR);
$smt->bindParam(':body',$data['body'], PDO::PARAM_STR);
$smt->bindParam(':id',$data['id'], PDO::PARAM_INT);
$smt->execute();
}
}
?>
変更ボタン押下後に遷移する編集画面を作ります。
基本的にはpost.phpの投稿部分を流用すればいいのですが、新規投稿時と違い、すでに登録しているデータを各入力フォームへ付与します。そうすることで既存のデータを変更することができます。
また、変更する投稿の固有IDも隠れて付与しています。こちらを用いてデータ更新ロジックへPOSTします。
▼view/admin_detail.php
<?php
// 登録データ取得
$post_data = $action->getDbPostData($id);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>管理モードやんなー</title>
</head>
<body>
<h1>管理モードやんなーー</h1>
<!-- 入力エリア -->
<div class="input_area">
<form action="./index.php" method="post" id="contact_form">
<dl class="name">
<dt>名前</dt>
<dd><input type="text" name="name" value="<?php echo $post_data[0]["name"];?>"></dd>
</dl>
<dl class="email">
<dt>メールアドレス</dt>
<dd><input type="text" name="email" value="<?php echo $post_data[0]["email"];?>"></dd>
</dl>
<dl class="body">
<dt>本文</dt>
<dd><textarea name="body"><?php echo $post_data[0]["body"];?></textarea></dd>
</dl>
<input type="hidden" name="id" value="<?php echo $id;?>">
<input type="hidden" name="eventId" value="edit_save">
<input type="submit" value="送信">
</form>
</div>
<!-- //入力エリア -->
<hr>
</body>
</html>
加えてeventIdによって処理振り分けを追記します。
▼index.php
<?php
require_once("./config/properties.php");
require_once('./class/business/getFormAction.php');
$action = new getFormAction();
$eventId = null;
// イベントID取得
if (isset($_POST['eventId'])) {
$eventId = $_POST['eventId'];
}
switch ($eventId) {
// DBsave
case 'save':
$action->saveDbPostData($_POST);
require("./view/post.php");
break;
// login
case 'login':
// パスワードが一致するかチェック
$errm = $action->checkAdminMode($_POST["password"]);
if (empty($errm)) {
require("./view/admin_list.php");
} else {
require("./view/post.php");
}
break;
// admin list
case 'admin':
require("./view/admin_list.php");
break;
// ここを追記-------------------------------------------
// データ更新画面
case 'edit':
$id = $_POST['id'];
require("./view/admin_detail.php");
break;
// データ更新
case 'edit_save':
$action->updateDbPostData($_POST);
require("./view/admin_list.php");
break;
// 初回アクセス時、投稿画面表示
default:
require("./view/post.php");
break;
}
?>
これで更新ロジックが完了です。
データを削除する
次にデータ削除ロジックを用意します。
すでに削除ボタンは用意しているので、削除ロジックとフラグで処理振り分けを実装すれば完了です。基本的に変更と同じく、削除したい投稿の固有IDを用いてdelte文を実行するロジックを付与すればOKです。
データ削除方式の種類
データ削除には「物理削除」と「論理削除」があります。それぞれの方式でアルゴリズムと対処方法が変わってきますので、まずはそれぞれの仕様を理解しましょう。
データ自体を削除する「物理削除」
物理削除は、データベースのテーブルから該当のデータレコード自体を削除することです。
delete文を用いる方法で、こちらを行うことでテーブルから完全にデータを削除することができます。単純に削除されるので、テーブル容量は削除した分だけ容量が軽くなります。ただし、deleteを実行完了後に復旧することはできません。
データ自体は残しつつ消えたとして扱う「論理削除」
論理削除は、該当のデータレコード自体は削除せず、データベースのテーブルに削除フラグを付与することであたかも削除したように扱う方式です。
こちらはupdate文を用いて削除フラグを付与する形になるので、データは残り続けます。
また論理削除を実装する場合、データ一覧を取得する際に「削除フラグがないものを取得する」という条件を加えておくことが必要になります。テーブル容量はどんどん蓄積されますが、フラグを解除すれば容易にデータを復旧することができます。
また、削除フラグはフラグカラムを設けて真偽を付与したり、削除した日付を登録したりすることでフラグとしてもいいと思います。
重要データなどを扱うシステムを構築する際は、物理削除よりも論理削除を利用したシステムが構築されるケースが多いです。
今回は一番シンプルに物理削除を行いましょう。
▼class/business/getformAction.php
<?php
class getFormAction {
public $pdo;
/**
* コネクション確保
*/
function __construct() {
try {
$this->pdo = new PDO( PDO_DSN, DATABASE_USER, DATABASE_PASSWORD, array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));
} catch (PDOException $e) {
echo 'error' . $e->getMessage();
die();
}
}
/**
* 記事データをDBに保存
*/
function saveDbPostData($data){
// データの保存
$smt = $this->pdo->prepare('insert into post (name,email,body,created_at,updated_at) values(:name,:email,:body,now(),now())');
$smt->bindParam(':name',$data['name'], PDO::PARAM_STR);
$smt->bindParam(':email',$data['email'], PDO::PARAM_STR);
$smt->bindParam(':body',$data['body'], PDO::PARAM_STR);
$smt->execute();
}
/**
* 記事データをDBから読み込み
*/
function getDbPostData($id = null){
if (!empty($id)) {
// 指定投稿IDのデータを取得
$smt = $this->pdo->prepare('select * from post where id = :id');
$smt->bindParam(':id',$id, PDO::PARAM_INT);
} else {
// 投稿データを取得
$smt = $this->pdo->prepare('select * from post order by created_at DESC limit 100');
}
$smt->execute();
// 実行結果を配列に返す。
$result = $smt->fetchAll(PDO::FETCH_ASSOC);
return $result;
}
/**
* 入力されたパスワードをチェックする。
*/
function checkAdminMode($password){
$errm = array();
// 入力データが存在するか
if (!empty($password)) {
// 入力データとパスワードが一致するか
if ($password != LOGIN_PASSWORD) {
$errm["login"] = "パスワードが違います。";
}
}
return $errm;
}
/**
* 投稿を更新する
*/
function updateDbPostData($data){
// データの保存
$smt = $this->pdo->prepare('update post set name = :name, email = :email, body = :body, updated_at = now() where id = :id');
$smt->bindParam(':name',$data['name'], PDO::PARAM_STR);
$smt->bindParam(':email',$data['email'], PDO::PARAM_STR);
$smt->bindParam(':body',$data['body'], PDO::PARAM_STR);
$smt->bindParam(':id',$data['id'], PDO::PARAM_INT);
$smt->execute();
}
// ここを追加----------------------------------------
/**
* 投稿を削除する
*/
function deleteDbPostData($id){
// データの削除
$smt = $this->pdo->prepare('delete from post where id = :id');
$smt->bindParam(':id',$id, PDO::PARAM_INT);
$smt->execute();
}
}
?>
次に処理振り分けを追加します。
▼index.php
<?php
require_once("./config/properties.php");
require_once('./class/business/getFormAction.php');
$action = new getFormAction();
$eventId = null;
// イベントID取得
if (isset($_POST['eventId'])) {
$eventId = $_POST['eventId'];
}
switch ($eventId) {
// DBsave
case 'save':
$action->saveDbPostData($_POST);
require("./view/post.php");
break;
// login
case 'login':
// パスワードが一致するかチェック
$errm = $action->checkAdminMode($_POST["password"]);
if (empty($errm)) {
require("./view/admin_list.php");
} else {
require("./view/post.php");
}
break;
// admin list
case 'admin':
require("./view/admin_list.php");
break;
// データ更新画面
case 'edit':
$id = $_POST['id'];
require("./view/admin_detail.php");
break;
// データ更新
case 'edit_save':
$action->updateDbPostData($_POST);
require("./view/admin_list.php");
break;
// ここを追記-----------------------------------------
// データ削除
case 'delete':
$id = $_POST['id'];
$action->deleteDbPostData($id);
require("./view/admin_list.php");
break;
// 初回アクセス時、投稿画面表示
default:
require("./view/post.php");
break;
}
?>
以上で物理削除実装が完了です。
なお、論理削除にも対応できるように、今回の利用しているテーブルに削除日付(deleted_at)を設けてあります。
論理削除を行う場合は、deleteではなくupdateでdelated_atに削除を実行した日付を付与し、記事データを取得するロジックでupdated_atがnullのデータを取得するように改修しておけば対応が可能です。
▼class/business/getFormAction.php
/**
* 記事データをDBから読み込み(論理削除対応)
*/
function getDbPostData($id = null){
if (!empty($id)) {
// 指定投稿IDのデータを取得
$smt = $this->pdo->prepare('select * from post where id = :id and deleted_at != null');
$smt->bindParam(':id',$id, PDO::PARAM_INT);
} else {
// 投稿データを取得
$smt = $this->pdo->prepare('select * from post where deleted_at != null order by created_at DESC limit 100');
}
$smt->execute();
// 実行結果を配列に返す。
$result = $smt->fetchAll(PDO::FETCH_ASSOC);
return $result;
}
/**
* 投稿を削除する(論理削除の場合)
*/
function deleteDbPostData($id){
// データの保存
$smt = $this->pdo->prepare('update post set name = :name, email = :email, body = :body, updated_at = now(), deleted_at = now() where id = :id');
$smt->bindParam(':name',$data['name'], PDO::PARAM_STR);
$smt->bindParam(':email',$data['email'], PDO::PARAM_STR);
$smt->bindParam(':body',$data['body'], PDO::PARAM_STR);
$smt->bindParam(':id',$data['id'], PDO::PARAM_INT);
$smt->execute();
}
これで掲示板が完成です。
まとめ
4回にわたって、CRUDを学ぶ掲示板構築をご紹介しました。ここまででデータベースの登録・更新・削除を行うプログラムを構築方法を学ぶことができたと思います。
ただし、今回の構築ではセキュリティの観点を割愛した構築を行いました。
現状のままではクロスサイトスクリプティングなどのWebシステムを構築する上で切っても切れない関係である「脆弱性を突いた攻撃」に対して、はっきりいってノーガードです。実際にサービスを構築する際は、そのような攻撃に対するセキュリティ対策を考慮する必要があります。
そのため次回はセキュリティに関してのお話をいたします。
防御を知るにはまずはどんな攻撃方法があるのかを知る必要がありますので、その点をご説明していきます。
それでは。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。