こんにちは! ケケンタです。
この記事ではPHP初心者の方に向けて
掲示板アプリの作り方
をご紹介します。
board.php(掲示板トップ)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* 投稿者ID(20桁)を生成
*/
if (isset($_SESSION['cont_id'])) {
$cont_id = $_SESSION['cont_id'];
} else {
$_SESSION['cont_id'] =
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90));
$cont_id = $_SESSION['cont_id'];
}
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 投稿ボタンが押下されたときの処理
*/
if (isset($_POST['post_btn'])) {
// 更新操作用の処理
unset($_SESSION['id']);
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
/**
* 必要項目がすべて入力されてたら投稿処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
INSERT INTO
board_info (title, comment, contributor_id)
VALUES
(:TITLE, :COMMENT, :CONTRIBUTOR_ID)
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
$stmt->bindValue(':CONTRIBUTOR_ID', $cont_id, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
// 投稿に成功したらセッション変数を破棄
unset($_SESSION['title']);
unset($_SESSION['comment']);
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 投稿一覧取得処理
*/
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
$sql = ('
SELECT *
FROM board_info
ORDER BY id DESC
');
$stmt = $pdo->prepare($sql);
// SQL実行
$stmt->execute();
// 投稿情報を辞書形式ですべて取得
$post_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>掲示板アプリ</h1>
<!-- 投稿フォーム -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<label>
<p>タイトル(※最大30文字)</p>
<input type="text" name="post_title" value="<?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?>">
<!-- エラーメッセージ -->
<?php if (isset($err_msg_title)) {
echo "<p class='err'>{$err_msg_title}</p>";
} ?>
</label>
</div>
<div>
<label>
<p>投稿内容(※最大1000文字)</p>
<textarea name="post_comment" cols="50" rows="10"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></textarea>
<!-- エラーメッセージ -->
<?php if (isset($err_msg_comment)) {
echo "<p class='err'>{$err_msg_comment}</p>";
} ?>
</label>
</div>
</div>
<button class="btn--mg-c" type="submit" name="post_btn" value="post_btn">投稿</button>
</form>
</section>
<hr>
<!-- 投稿一覧 -->
<section class="post-list">
<?php if (count($post_list) === 0) : ?>
<!-- 投稿が無いときはメッセージを表示する -->
<p class="no-post-msg">現在、投稿はありません。</p>
<?php else : ?>
<ul>
<!-- 投稿情報の出力 -->
<?php foreach ($post_list as $post_item) : ?>
<li>
<form action="" method="post">
<!-- 投稿ID -->
<span>ID:<?php echo $post_item['id']; ?> </span>
<!-- 投稿タイトル -->
<span><?php echo $post_item['title']; ?></span>
<!-- 投稿者ID -->
<span>/投稿者:<?php echo $post_item['contributor_id']; ?></span>
<!-- 投稿内容 -->
<p class="p-pre"><?php echo $post_item['comment']; ?></p>
<!-- 投稿日時 -->
<span class="post-datetime">投稿日時:<?php echo $post_item['created_at']; ?></span>
<!-- 過去に更新されていたら更新日時も表示 -->
<?php if ($post_item['created_at'] < $post_item['updated_at']) : ?>
<span class="post-datetime post-datetime__updated">更新日時:<?php echo $post_item['updated_at']; ?></span>
<?php endif; ?>
</form>
<!-- 自分の投稿内容かつセッションが有効な間は編集・削除が可能 -->
<?php if ($post_item['contributor_id'] === $cont_id) : ?>
<div class="btn-flex">
<form action="update-edit.php" method="post">
<button type="submit" name="update_btn">編集</button>
<input type="hidden" name="post_id" value="<?php echo $post_item['id']; ?>">
</form>
<form action="delete-confirm.php" method="post">
<button type="submit" name="delete_btn">削除</button>
<input type="hidden" name="post_id" value="<?php echo $post_item['id']; ?>">
</form>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['id']) && ($_SESSION['id'] == $post_item['id'])): ?>
<p class='updated-post'>更新しました</p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
</body>
</html>
update-edit.php(編集画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 編集ボタンで遷移してきたときの処理
*/
if (isset($_POST['update_btn'])) {
/**
* 編集対象の投稿情報を取得
*/
if (isset($_POST['post_id']) && $_POST['post_id'] != '') {
// セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容取得処理
*/
$sql = ('
SELECT id, title, comment
FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 更新ボタンが押下されたときの処理
*/
if (isset($_POST['update_submit_btn'])) {
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
/**
* 必要項目がすべて入力されてたら更新処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容更新処理
*/
$sql = ('
UPDATE board_info
SET title = :TITLE, comment = :COMMENT
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
// 更新に成功したらセッション変数を破棄
// unset($_SESSION['id']); // ※投稿IDは敢えて破棄せず、掲示板ページでID判定をするために情報を保持する★
unset($_SESSION['title']);
unset($_SESSION['comment']);
// 掲示板ページへ戻る
header('Location: board.php');
exit();
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* キャンセルボタンが押下されたら
* セッション情報を破棄して
* 掲示板一覧画面へ戻る
*/
if (isset($_POST['cancel_btn'])) {
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
header('Location: board.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>投稿編集画面</h1>
<!-- 投稿編集フォーム -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<label>
<p>タイトル</p>
<input type="text" name="post_title" value="<?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?>">
<!-- エラーメッセージ -->
<?php if (isset($err_msg_title)) {
echo "<p class='err'>{$err_msg_title}</p>";
} ?>
</label>
</div>
<div>
<label>
<p>投稿内容</p>
<textarea name="post_comment" cols="50" rows="10"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></textarea>
<!-- エラーメッセージ -->
<?php if (isset($err_msg_comment)) echo "<p class='err'>{$err_msg_comment}</p>"; ?>
</label>
</div>
</div>
<div class="btn-flex">
<button type="submit" name="update_submit_btn" value="update_submit_btn">更新</button>
<button type="submit" name="cancel_btn" value="cancel_btn">キャンセル</button>
</div>
</form>
</section>
</body>
</html>
delete-confirm.php(削除確認画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 削除ボタンで遷移してきたときの処理
*/
if (isset($_POST['delete_btn'])) {
/**
* 編集対象の投稿情報を取得
*/
if (isset($_POST['post_id']) && $_POST['post_id'] != '') {
// セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
SELECT id, title, comment
FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 削除ボタンが押下されたときの処理
*/
if (isset($_POST['delete_submit_btn'])) {
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容削除処理
*/
$sql = ('
DELETE FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 削除に成功したらセッション変数を破棄
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
// 削除成功画面へ遷移
header('Location: delete-success.php');
exit();
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
/**
* キャンセルボタンが押下されたら
* セッション情報を破棄して
* 掲示板一覧画面へ戻る
*/
if (isset($_POST['cancel_btn'])) {
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
header('Location: board.php');
return;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>削除確認</h1>
<p class="delete-confirm-msg">以下の投稿を削除します。</p>
<!-- 削除確認画面 -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<p>タイトル</p>
<p><?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?></p>
</div>
<div>
<p>投稿内容</p>
<p class="p-pre"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></p>
</div>
</div>
<div class="btn-flex">
<button type="submit" name="delete_submit_btn" value="delete_submit_btn">削除</button>
<button type="submit" name="cancel_btn" value="cancel_btn">キャンセル</button>
</div>
</form>
</section>
</body>
</html>
delete-success.php(削除成功画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* 掲示場TOPへ自動で遷移する処理★
*/
header('refresh: 3; url=board.php');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>削除が完了しました。</h1>
<p class="delete-success-msg">3秒後に自動で掲示板TOPへ戻ります。</p>
</body>
</html>
掲示板アプリを作成するためにはPHPの基本文法はもちろん、データベース操作(いわゆるCRUD)やHTTPリクエストなどの知識が不可欠です。
PHPの基礎学習後の総まとめ課題として、ぜひこの記事を通して掲示板アプリの作り方を学んでいって下さい。
- PHPでの掲示板アプリの作り方
- 掲示板アプリを作成するために必要な知識
また、今回ご紹介する掲示板ではデータベース操作はもちろん、セッションも組み合わせて実装しています。
そのため、本記事は
PHPでアプリを作りたいけど、そもそもどの知識をどう使えば良いのか分からない……。
という風に、学習した基礎知識を実践で組み合わせて活用するイメージが湧かないという方にもおすすめの記事となっています。
なお、上の記事のセキュリティ対策は以下の書籍を参考にしています。
(通称:徳丸本と呼ばれる「Webアプリ開発者必読」とまで言われている書籍です)
体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践
徳丸 浩/著 SBクリエイティブ/出版│Amazon
掲示板アプリのイメージ
あれこれと説明を始める前に、第一に掲示板アプリのイメージを掴んでいただくことが大切と思いますので、まずは3つの基本機能のイメージをお伝えします。
- 投稿
- 編集
- 削除
まず、今回作成する掲示板アプリのトップページがこちらです。
続いて各機能のイメージです。
投稿
- 「タイトル」と「投稿内容」に任意のテキストを入力
- 「投稿ボタン」を押下
- 投稿一覧に投稿が追加される
編集
- 編集したい投稿の「編集ボタン」を押下
- 投稿編集画面へ遷移
- 編集内容を入力
- 「更新ボタン」を押下
- 掲示板トップへ遷移
- 投稿に編集内容が反映、また「更新しました」が表示される
削除
- 削除したい投稿の「削除ボタン」を押下
- 削除確認画面へ遷移
- 「削除ボタン」を押下
- 削除完了画面へ遷移
- 3秒後に自動で掲示板トップへ遷移
掲示板アプリを作成するために必要な知識・スキル
ここまで掲示板アプリのイメージをお見せしました。
では、この掲示板アプリを実際に作成するためには、具体的にどういった知識・スキルが必要になるのでしょうか?
本章では、この掲示板アプリを作成する際に必要となる知識についてご紹介します。
- PHPの基本文法
- HTTPリクエスト(POST送信)
- データベース操作(PDO)
- セッション
- SQL
PHPの基礎を学ばれてきた方であれば、上記に並んでいる知識・スキル名は恐らく見たことのあるものがほとんどかと思います。
しかし、この中でPDOについては初めて聞くという方が比較的多いことが予想されるため簡単に説明します。
PDOとは、PHPからデータベースへ簡単にアクセスできるようにするための拡張モジュールです。
通常、データベース関係の処理はMySQLならMySQL用に、PostgreSQLならPostgreSQL用に記述する必要があります。しかし、PDOを利用すると同じ処理の書き方で異なるデータベースソフトウェアと連携することが可能となります。
これにより、PHPでデータベース操作するのが楽になります。
今回の掲示板アプリ作成には上記5つの知識・スキルへの理解が必須となります。
逆に言うと、これらがきちんと理解できていれば掲示板アプリは作成できるということでもあります。
参考記事
なお、以下の書籍「PHP本格入門(上)」ではSQLを除く掲示板アプリを作成するために必要な知識を網羅的に学習することが可能です。
当記事とこちらの書籍を組み合わせて学習することで、掲示板アプリの作成方法をよりしっかりと理解できるようになります。
ちなみに僕自身、PHPはこの書籍で学習し、知識を組み合わせて使用することで初めてのWebアプリを作成しました。
また、SQLを学びたい方には評判も良いこちらの書籍がおすすめです。
【仕様①】作成する掲示板アプリ
ここからは掲示板アプリの各仕様の詳細をご説明していきます。
なお、細かな仕様はこのあとすぐにご説明しますが、今回作成する掲示板アプリは機能をより簡素化する意味で
ログイン機能を持っていない匿名掲示板
となっています。
ただし、匿名と言いつつも、投稿者のことは、セッションを利用して一定時間、一意に識別する仕様となっています。
※セッションの解説を加えたかったため、敢えてこのような仕様としました。
ログイン機能を持つ掲示板は今後別の記事でご紹介する予定です!
基本仕様
以下が掲示板アプリの基本的な仕様です。
基本機能
データベース操作の基本であるCRUDをすべて使用できるように、投稿機能だけでなく敢えて編集・削除機能も持たせています。
機能 | 説明 | CRUD対応 |
---|---|---|
投稿 | 「タイトル」と「投稿内容」の投稿が可能 | Create |
投稿一覧表示 | 投稿されている投稿一覧を表示 | Read |
編集 | 投稿後、最短30分間のみ投稿の編集が可能 | Update |
削除 | 投稿後、最短30分間のみ投稿の削除が可能 | Delete |
投稿一覧に表示する情報
情報名 | 備考 |
---|---|
投稿ID | 投稿ごとの自動連番 |
投稿タイトル | 最大30文字まで(以降は切り捨て) |
投稿者ID | 投稿者を一意に識別するためのID。セッションで管理 |
投稿内容 | 最大1000文字まで(以降は切り捨て) |
投稿日時 | |
投稿更新日時 | 「編集」が行われた投稿にのみ表示 |
補足機能
掲示板アプリの基本機能の他に、以下の補足機能を実装しています。
- タイトルと投稿内容の入力は必須とする(※未入力の場合はエラーメッセージを表示)
- 更新した投稿の右下に「更新しました」と表示(※追加で投稿するなど何らかの操作が行われたら非表示になる)
投稿者の識別方法・関連機能
先ほど、今回ご紹介する掲示板アプリはログイン機能を持たない匿名掲示板とお伝えをしました。
しかし、ログイン機能を持たない代わりに、セッションを利用して投稿者自体は一定時間だけ一意に識別する仕様としています。
具体的なコードは後述しますが、本機能の仕様は以下の通りになります。
- 投稿者には初アクセス時に固有IDを割り振る
- 固有IDはセッションで保持
- 掲示板へアクセスまたは何らからの操作を行ってから30分間は同じIDで投稿が可能
- 投稿したときのIDが有効な間、投稿者は自身の投稿に対してのみ「編集」、「削除」が行える
分かりやすく言うと、ユーザの初アクセス時に最短30分間のみ有効なチケットを配るイメージです。
ユーザが初アクセスまたは何らかの操作を行うことで、同じIDのチケットが再度配られます。
一方、ユーザがチケットを受け取ってから30分以上何もしないと配られたチケットは無効となり、再度投稿を行う(または掲示板へ再度アクセスする)ことで別IDが記載されたチケットが配られます。
注意点として、ここで言っているユーザとはパソコン利用者本人のことではなくブラウザのことです。
イメージが湧きづらい方は、掲示板を実装後、試しにchromeの通常モードとシークレットモード(ctrl + shift + Nで起動)でそれぞれ掲示板へ投稿してみて下さい。
30分以内に操作をしても別々のIDで投稿が実行されるはずです。
【仕様②】掲示板アプリのデータベース構造・ファイル構成
以上が掲示板アプリの仕様です。
ここからは、仕様を踏まえた上で、具体的なデータベース構造とファイル構成を解説していきます。
データベース構造
今回、データベースはMySQLを使用しています。
データベース構造
データベース名 | 作成テーブル |
---|---|
board | board_info |
board_infoテーブル構造
カラム名 | 備考 |
---|---|
id | 投稿ID=主キー 自動連番 |
title | 投稿タイトル 字数制限:30文字 |
comment | 投稿内容 字数制限:1000文字 |
contributor_id | 投稿者ID 字数制限:20文字 |
created_at | 投稿日時 |
updated_at | 更新日時 |
作成後のテーブル構造画面(phpMyAdminの画面)
- id……AUTO_INCREMENTを設定することで、投稿データが追加される度に自動連番が割り振られる
- updated_at……ON UPDATE CURRENT_TIMESTAMP()を設定することで、投稿内容が編集されたとき自動で更新日時が反映される
- 各字数制限……必要な字数に制限することで、予期しない操作などによる不具合を防ぐ目的で設定
ファイル構成
作成した各ファイルはこちらです。
掲示板アプリのコード解説
以上が作成する掲示板アプリのデータベース構造とファイル構成でした。
本章からは、機能別に実際のコードを提示しつつ、各機能・処理ごとのポイントをお伝えしていきます。
解説の流れですが、機能別に処理を分割して解説していきます。
board.php(掲示板トップ)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* 投稿者ID(20桁)を生成
*/
if (isset($_SESSION['cont_id'])) {
$cont_id = $_SESSION['cont_id'];
} else {
$_SESSION['cont_id'] =
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90));
$cont_id = $_SESSION['cont_id'];
}
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 投稿ボタンが押下されたときの処理
*/
if (isset($_POST['post_btn'])) {
// 更新操作用の処理
unset($_SESSION['id']);
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
/**
* 必要項目がすべて入力されてたら投稿処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
INSERT INTO
board_info (title, comment, contributor_id)
VALUES
(:TITLE, :COMMENT, :CONTRIBUTOR_ID)
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
$stmt->bindValue(':CONTRIBUTOR_ID', $cont_id, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
// 投稿に成功したらセッション変数を破棄
unset($_SESSION['title']);
unset($_SESSION['comment']);
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 投稿一覧取得処理
*/
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
$sql = ('
SELECT *
FROM board_info
ORDER BY id DESC
');
$stmt = $pdo->prepare($sql);
// SQL実行
$stmt->execute();
// 投稿情報を辞書形式ですべて取得
$post_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>掲示板アプリ</h1>
<!-- 投稿フォーム -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<label>
<p>タイトル(※最大30文字)</p>
<input type="text" name="post_title" value="<?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?>">
<!-- エラーメッセージ -->
<?php if (isset($err_msg_title)) {
echo "<p class='err'>{$err_msg_title}</p>";
} ?>
</label>
</div>
<div>
<label>
<p>投稿内容(※最大1000文字)</p>
<textarea name="post_comment" cols="50" rows="10"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></textarea>
<!-- エラーメッセージ -->
<?php if (isset($err_msg_comment)) {
echo "<p class='err'>{$err_msg_comment}</p>";
} ?>
</label>
</div>
</div>
<button class="btn--mg-c" type="submit" name="post_btn" value="post_btn">投稿</button>
</form>
</section>
<hr>
<!-- 投稿一覧 -->
<section class="post-list">
<?php if (count($post_list) === 0) : ?>
<!-- 投稿が無いときはメッセージを表示する -->
<p class="no-post-msg">現在、投稿はありません。</p>
<?php else : ?>
<ul>
<!-- 投稿情報の出力 -->
<?php foreach ($post_list as $post_item) : ?>
<li>
<form action="" method="post">
<!-- 投稿ID -->
<span>ID:<?php echo $post_item['id']; ?> </span>
<!-- 投稿タイトル -->
<span><?php echo $post_item['title']; ?></span>
<!-- 投稿者ID -->
<span>/投稿者:<?php echo $post_item['contributor_id']; ?></span>
<!-- 投稿内容 -->
<p class="p-pre"><?php echo $post_item['comment']; ?></p>
<!-- 投稿日時 -->
<span class="post-datetime">投稿日時:<?php echo $post_item['created_at']; ?></span>
<!-- 過去に更新されていたら更新日時も表示 -->
<?php if ($post_item['created_at'] < $post_item['updated_at']) : ?>
<span class="post-datetime post-datetime__updated">更新日時:<?php echo $post_item['updated_at']; ?></span>
<?php endif; ?>
</form>
<!-- 自分の投稿内容かつセッションが有効な間は編集・削除が可能 -->
<?php if ($post_item['contributor_id'] === $cont_id) : ?>
<div class="btn-flex">
<form action="update-edit.php" method="post">
<button type="submit" name="update_btn">編集</button>
<input type="hidden" name="post_id" value="<?php echo $post_item['id']; ?>">
</form>
<form action="delete-confirm.php" method="post">
<button type="submit" name="delete_btn">削除</button>
<input type="hidden" name="post_id" value="<?php echo $post_item['id']; ?>">
</form>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['id']) && ($_SESSION['id'] == $post_item['id'])): ?>
<p class='updated-post'>更新しました</p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
</body>
</html>
update-edit.php(編集画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 編集ボタンで遷移してきたときの処理
*/
if (isset($_POST['update_btn'])) {
/**
* 編集対象の投稿情報を取得
*/
if (isset($_POST['post_id']) && $_POST['post_id'] != '') {
// セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容取得処理
*/
$sql = ('
SELECT id, title, comment
FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 更新ボタンが押下されたときの処理
*/
if (isset($_POST['update_submit_btn'])) {
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
/**
* 必要項目がすべて入力されてたら更新処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容更新処理
*/
$sql = ('
UPDATE board_info
SET title = :TITLE, comment = :COMMENT
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
// 更新に成功したらセッション変数を破棄
// unset($_SESSION['id']); // ※投稿IDは敢えて破棄せず、掲示板ページでID判定をするために情報を保持する★
unset($_SESSION['title']);
unset($_SESSION['comment']);
// 掲示板ページへ戻る
header('Location: board.php');
exit();
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* キャンセルボタンが押下されたら
* セッション情報を破棄して
* 掲示板一覧画面へ戻る
*/
if (isset($_POST['cancel_btn'])) {
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
header('Location: board.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>投稿編集画面</h1>
<!-- 投稿編集フォーム -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<label>
<p>タイトル</p>
<input type="text" name="post_title" value="<?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?>">
<!-- エラーメッセージ -->
<?php if (isset($err_msg_title)) {
echo "<p class='err'>{$err_msg_title}</p>";
} ?>
</label>
</div>
<div>
<label>
<p>投稿内容</p>
<textarea name="post_comment" cols="50" rows="10"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></textarea>
<!-- エラーメッセージ -->
<?php if (isset($err_msg_comment)) echo "<p class='err'>{$err_msg_comment}</p>"; ?>
</label>
</div>
</div>
<div class="btn-flex">
<button type="submit" name="update_submit_btn" value="update_submit_btn">更新</button>
<button type="submit" name="cancel_btn" value="cancel_btn">キャンセル</button>
</div>
</form>
</section>
</body>
</html>
delete-confirm.php(削除確認画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 削除ボタンで遷移してきたときの処理
*/
if (isset($_POST['delete_btn'])) {
/**
* 編集対象の投稿情報を取得
*/
if (isset($_POST['post_id']) && $_POST['post_id'] != '') {
// セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
SELECT id, title, comment
FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 削除ボタンが押下されたときの処理
*/
if (isset($_POST['delete_submit_btn'])) {
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容削除処理
*/
$sql = ('
DELETE FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 削除に成功したらセッション変数を破棄
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
// 削除成功画面へ遷移
header('Location: delete-success.php');
exit();
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
/**
* キャンセルボタンが押下されたら
* セッション情報を破棄して
* 掲示板一覧画面へ戻る
*/
if (isset($_POST['cancel_btn'])) {
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
header('Location: board.php');
return;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>削除確認</h1>
<p class="delete-confirm-msg">以下の投稿を削除します。</p>
<!-- 削除確認画面 -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<p>タイトル</p>
<p><?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?></p>
</div>
<div>
<p>投稿内容</p>
<p class="p-pre"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></p>
</div>
</div>
<div class="btn-flex">
<button type="submit" name="delete_submit_btn" value="delete_submit_btn">削除</button>
<button type="submit" name="cancel_btn" value="cancel_btn">キャンセル</button>
</div>
</form>
</section>
</body>
</html>
delete-success.php(削除成功画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* 掲示場TOPへ自動で遷移する処理★
*/
header('refresh: 3; url=board.php');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>削除が完了しました。</h1>
<p class="delete-success-msg">3秒後に自動で掲示板TOPへ戻ります。</p>
</body>
</html>
何度かお伝えしていますが、本記事で紹介しているコードはセキュリティリスクを考慮していません。本番環境で利用するには、ご紹介するコードにセキュリティ対策を施すことが絶対不可欠です。
セキュリティ対策を施したコードをご覧になりたい場合は以下の記事をご覧ください。
投稿機能
改めて、投稿機能のイメージ図です。
上記の処理を実装しているのがboard.phpです。
board.php(掲示板トップ)~投稿機能部分~
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* 投稿者ID(20桁)を生成
*/
if (isset($_SESSION['cont_id'])) {
$cont_id = $_SESSION['cont_id'];
} else {
$_SESSION['cont_id'] =
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90));
$cont_id = $_SESSION['cont_id'];
}
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 投稿ボタンが押下されたときの処理
*/
if (isset($_POST['post_btn'])) {
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
/**
* 必要項目がすべて入力されてたら投稿処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローするみ表示する設定にする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
INSERT INTO
board_info (title, comment, contributor_id)
VALUES
(:TITLE, :COMMENT, :CONTRIBUTOR_ID)
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
$stmt->bindValue(':CONTRIBUTOR_ID', $cont_id, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
// 投稿に成功したらセッション変数を破棄
unset($_SESSION['title']);
unset($_SESSION['comment']);
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 投稿一覧取得処理
*/
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
$sql = ('
SELECT *
FROM board_info
ORDER BY id DESC
');
$stmt = $pdo->prepare($sql);
// SQL実行
$stmt->execute();
// 投稿情報を辞書形式ですべて取得
$post_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>掲示板アプリ</h1>
<!-- 投稿フォーム -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<label>
<p>タイトル(※最大30文字)</p>
<input type="text" name="post_title" value="<?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?>">
<!-- エラーメッセージ -->
<?php if (isset($err_msg_title)) {
echo "<p class='err'>{$err_msg_title}</p>";
} ?>
</label>
</div>
<div>
<label>
<p>投稿内容(※最大1000文字)</p>
<textarea name="post_comment" cols="50" rows="10"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></textarea>
<!-- エラーメッセージ -->
<?php if (isset($err_msg_comment)) echo "<p class='err'>{$err_msg_comment}</p>"; ?>
</label>
</div>
</div>
<button class="btn--mg-c" type="submit" name="post_btn" value="post_btn">投稿</button>
</form>
</section>
<hr>
<!-- 投稿一覧 -->
<section class="post-list">
<?php if (count($post_list) === 0) : ?>
<!-- 投稿が無いときはメッセージを表示する -->
<p class="no-post-msg">現在、投稿はありません。</p>
<?php else : ?>
<ul>
<!-- 投稿情報の出力 -->
<?php foreach ($post_list as $post_item) : ?>
<li>
<form action="" method="post">
<!-- 投稿ID -->
<span>ID:<?php echo $post_item['id']; ?> </span>
<!-- 投稿タイトル -->
<span><?php echo $post_item['title']; ?></span>
<!-- 投稿者ID -->
<span>/投稿者:<?php echo $post_item['contributor_id']; ?></span>
<!-- 投稿内容 -->
<p class="p-pre"><?php echo $post_item['comment']; ?></p>
<!-- 投稿日時 -->
<span class="post-datetime">投稿日時:<?php echo $post_item['created_at']; ?></span>
</form>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
</body>
</html>
board.php内で行なっている処理を整理すると以下のようになります。
- セッションの設定・開始
- 投稿者IDの生成・セッション変数への格納
- 投稿処理
- エラーメッセージの格納(未入力項目があった場合のみ)
- 投稿一覧取得処理
上から順番に解説をしていきます。
セッションの設定・開始
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
この部分でセッションの有効期限を1800秒=30分にし、session.gc_divisorに1を設定することでセッションが確実に破棄されるようにしています。
デフォルトだとセッションは100分の1の確率で破棄される設定になっています。
本掲示板アプリで実装している「最短30分間のみ有効な投稿者ID」の30分間の部分は、この設定で実現しています。もし30分以外の時間にしたいときは、ここの秒数を変更すればOKです。
ちなみに上記の秒数と確率はphp.iniを直接編集して設定することも可能です。
投稿者IDの生成・セッション変数への格納
/**
* 投稿者ID(20桁)を生成
*/
if (isset($_SESSION['cont_id'])) {
$cont_id = $_SESSION['cont_id'];
} else {
$_SESSION['cont_id'] =
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) . chr(mt_rand(65, 90)) .
chr(mt_rand(65, 90)) . chr(mt_rand(65, 90));
$cont_id = $_SESSION['cont_id'];
}
この処理ではランダムな20桁の文字列を生成し、それを投稿者IDとして割り振っています。
また、投稿者IDを割り振るかどうかについては、以下の条件を元に判定しています。
過去に投稿者IDが割り振られている=セッション変数($_SESSION[‘cont_id’])に投稿者IDが格納されているかどうか
このあと解説する編集・削除機能では、この投稿者IDを元に機能の有効・無効化を行っています。
ログイン機能を持たない本掲示板アプリにおいては、この処理がユーザの操作権限制御を担っているというわけです。
投稿処理
続いてメインとなる投稿処理です。
データベース接続に必要な情報の定義
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
投稿処理
/**
* 投稿ボタンが押下されたときの処理
*/
if (isset($_POST['post_btn'])) {
~ エラーメッセージ関係の処理は中略 ~
/**
* 必要項目がすべて入力されてたら投稿処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
INSERT INTO
board_info (title, comment, contributor_id)
VALUES
(:TITLE, :COMMENT, :CONTRIBUTOR_ID)
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
$stmt->bindValue(':CONTRIBUTOR_ID', $cont_id, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
~ エラーメッセージ関係の処理は中略 ~
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
ここでの処理の流れは以下の通りです。
- 投稿ボタン(name=post_btnのボタン)が押されたら……
- 「タイトル」と「投稿内容」が入力されているか判定
- 「タイトル」と「投稿内容」が両方とも入力されていたらデータベース接続を実行
- データ登録するためのSQL文を発行(INSERT文を使用)
- プレースホルダーへ値をセット
- SQL文を実行(データの登録)
- DBとの接続を切る
この中で最も重要なのが、PDOを利用して投稿内容をデータベースへ登録している以下の処理です。
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
INSERT INTO
board_info (title, comment, contributor_id)
VALUES
(:TITLE, :COMMENT, :CONTRIBUTOR_ID)
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
$stmt->bindValue(':CONTRIBUTOR_ID', $cont_id, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
PDOを利用する場合、文法的な構造は基本的にこのような形・流れになります。
上記でコメントが記述されている部分が処理1つ1つの区切りと思って頂くとイメージしやすいかと思います。
エラーメッセージの格納(未入力項目があった場合のみ)
/**
* 投稿ボタンが押下されたときの処理
*/
if (isset($_POST['post_btn'])) {
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
~ 投稿処理は中略 ~
}
ここで行っている処理は以下の通りです。
- タイトルもしくは投稿内容のいずれかのみが入力されていた場合は、その入力内容をセッション変数($_SESSION[‘title’]、$_SESSION[‘comment’])に保持する
- タイトルもしくは投稿内容のいずれか、または両方が未入力ならエラーメッセージを変数へ格納する
しかし、この処理が無いと「タイトルまたは本文の一方だけを入力して投稿ボタンを押してしまった場合、入力されていた内容が消え、同じ内容を再度入力する手間が増える」という状態になってしまいユーザビリティが低下してしまいます。
イメージが湧きづらい方は、実際にこちらの処理をコメントアウトしていただくと、「こういうことか」とすぐにご理解いただけるかと思います。
また、後者のエラーメッセージについては、ここで変数に格納された場合、以下の「投稿フォーム」に関するHTML部分で出力する実装になっています。
<!-- 投稿フォーム -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<label>
<p>タイトル(※最大30文字)</p>
<input type="text" name="post_title" value="<?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?>">
<!-- エラーメッセージ -->
<?php if (isset($err_msg_title)) {
echo "<p class='err'>{$err_msg_title}</p>";
} ?>
</label>
</div>
<div>
<label>
<p>投稿内容(※最大1000文字)</p>
<textarea name="post_comment" cols="50" rows="10"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></textarea>
<!-- エラーメッセージ -->
<?php if (isset($err_msg_comment)) {
echo "<p class='err'>{$err_msg_comment}</p>";
} ?>
</label>
</div>
</div>
<button class="btn--mg-c" type="submit" name="post_btn" value="post_btn">投稿</button>
</form>
</section>
投稿一覧取得処理
投稿一覧取得処理については、①投稿データの取得と②取得した投稿データの表示に切り分けてご説明します。
①投稿データの取得
/**
* 投稿一覧取得処理
*/
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
$sql = ('
SELECT *
FROM board_info
ORDER BY id DESC
');
$stmt = $pdo->prepare($sql);
// SQL実行
$stmt->execute();
// 投稿情報を辞書形式ですべて取得
$post_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
?>
①投稿データの取得での流れは、SQL文でSELECTを使用している点を除けば基本的に投稿処理と同じです。
注目していただきたいのが、ハイライトされている2か所の記述です。
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
上記の一文では、取得するデータ形式を「DBテーブルのカラム名をキーとした連想配列」に指定しています。これは次節で解説する表示部分に直接関係してくる設定記述です。
// 投稿情報を辞書形式ですべて取得
$post_list = $stmt->fetchAll(PDO::FETCH_ASSOC);
また、後者のハイライト箇所によって、データベース上に登録されている投稿情報すべてを辞書形式で$post_listに格納しています。
上記2つの処理により、投稿情報を出力する際、データベースに設定してあるキー名を指定することで簡単に目的の情報を指定できます。
例えば、タイトル情報を出力したい場合は$post_list[‘title’]と記述すればOKということです。(以下、テーブル構造の再掲)
②取得した投稿データの表示
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>掲示板アプリ</h1>
<!-- 投稿フォーム -->
~ 中略 ~
<!-- 投稿一覧 -->
<section class="post-list">
<?php if (count($post_list) === 0) : ?>
<!-- 投稿が無いときはメッセージを表示する -->
<p class="no-post-msg">現在、投稿はありません。</p>
<?php else : ?>
<ul>
<!-- 投稿情報の出力 -->
<?php foreach ($post_list as $post_item) : ?>
<li>
<form action="" method="post">
<!-- 投稿ID -->
<span>ID:<?php echo $post_item['id']; ?> </span>
<!-- 投稿タイトル -->
<span><?php echo $post_item['title']; ?></span>
<!-- 投稿者ID -->
<span>/投稿者:<?php echo $post_item['contributor_id']; ?></span>
<!-- 投稿内容 -->
<p class="p-pre"><?php echo $post_item['comment']; ?></p>
<!-- 投稿日時 -->
<span class="post-datetime">投稿日時:<?php echo $post_item['created_at']; ?></span>
</form>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
</body>
</html>
上記のハイライトされている箇所が投稿一覧を出力するための処理です。
ご覧いただいている通り、先ほど取得した投稿データ($post_list)をforeachで回し、HTMLタグ内にテキストとして埋め込んでいます。
先ほどの①投稿データの取得処理の中で、
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
という記述をご紹介しました。
これにより、$post_listに格納されているデータは連想配列になっているのでした。
そのため、HTML内ではテーブルのカラム名をキーにすることで目的の値を出力しています。
なお、プログラム内のコメントをご覧いただくとお分かりになるかと思いますが、投稿が無い=取得データ($post_list)が0個のときは「現在、投稿はありません」と表示する実装になっています。
ここまでが投稿機能の解説でした!
続いて編集機能についてご説明していきます。
編集機能
繰り返しになりますが、前提として本掲示板アプリでは現在の投稿者IDと同じIDを持つ投稿のみ編集可能です。
投稿機能の章では割愛しましたが、実はコード説明の冒頭で提示した全体コードのboard.php内のフォームには、以下のハイライト部分の記述がありました。
<!-- 投稿一覧 -->
<section class="post-list">
~ 中略 ~
<ul>
<!-- 投稿情報の出力 -->
<?php foreach ($post_list as $post_item) : ?>
~ 投稿一覧表示処理 中略 ~
<!-- 自分の投稿内容かつセッションが有効な間は編集・削除が可能 -->
<?php if ($post_item['contributor_id'] === $cont_id) : ?>
<div class="btn-flex">
<form action="update-edit.php" method="post">
<button type="submit" name="update_btn">編集</button>
<input type="hidden" name="post_id" value="<?php echo $post_item['id']; ?>">
</form>
<form action="delete-confirm.php" method="post">
<button type="submit" name="delete_btn">削除</button>
<input type="hidden" name="post_id" value="<?php echo $post_item['id']; ?>">
</form>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['id']) && ($_SESSION['id'] == $post_item['id'])): ?>
<p class='updated-post'>更新しました</p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
編集(または削除)機能が使用できるかどうかは、実はこの部分で制御をしています。
$post_item[‘contributor_id’] === $cont_idの部分で「投稿の投稿者ID」と「現在の投稿者ID」が同じかどうかを判定しています。もし異なれば、編集・削除ボタンは非表示、つまり編集・削除機能は使用不可ということです。
本章では編集機能をご紹介しますが、本機能を使用できる背景にはこうした処理があることをまずはご説明させていただきました。
それでは、以下より編集機能の解説を進めていきます。
本掲示板アプリにおける投稿編集は投稿編集画面で行います。
「投稿編集画面」ページへアクセスするには、投稿に表示される「編集ボタン」を押下する必要があります。
そして、「編集ボタン」が押下されると直接「投稿編集画面」へ遷移し、その先はupdate-edit.phpファイルの処理が実行されるという流れになります。
編集機能の流れ
今回はボタンを押下した時点でupdate-edit.php(投稿編集画面)へ直接アクセスする実装になっています。しかし、その他の方法として、board.php内で投稿者IDの照合を行い、本当に一致していたときだけupdate-edit.phpへ遷移するといったことも可能です。(セキュリティ面などを考えるとこの方法の方が望ましいですが、今回は簡略化のために省略しています。)
update-edit.php(編集画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 編集ボタンで遷移してきたときの処理
*/
if (isset($_POST['update_btn'])) {
/**
* 編集対象の投稿情報を取得
*/
if (isset($_POST['post_id']) && $_POST['post_id'] != '') {
// ★セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容取得処理
*/
$sql = ('
SELECT id, title, comment
FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// ★投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 更新ボタンが押下されたときの処理
*/
if (isset($_POST['update_submit_btn'])) {
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
/**
* 必要項目がすべて入力されてたら更新処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容更新処理
*/
$sql = ('
UPDATE board_info
SET title = :TITLE, comment = :COMMENT
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
// 更新に成功したらセッション変数を破棄
// unset($_SESSION['id']); // ★※投稿IDは敢えて破棄せず、掲示板ページでID判定をするために情報を保持する
unset($_SESSION['title']);
unset($_SESSION['comment']);
// ★掲示板ページへ戻る
header('Location: board.php');
exit();
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**★
* キャンセルボタンが押下されたら
* セッション情報を破棄して
* 掲示板一覧画面へ戻る
*/
if (isset($_POST['cancel_btn'])) {
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
header('Location: board.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>投稿編集画面</h1>
<!-- 投稿編集フォーム -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<label>
<p>タイトル</p>
<input type="text" name="post_title" value="<?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?>">
<!-- エラーメッセージ -->
<?php if (isset($err_msg_title)) {
echo "<p class='err'>{$err_msg_title}</p>";
} ?>
</label>
</div>
<div>
<label>
<p>投稿内容</p>
<textarea name="post_comment" cols="50" rows="10"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></textarea>
<!-- エラーメッセージ -->
<?php if (isset($err_msg_comment)) echo "<p class='err'>{$err_msg_comment}</p>"; ?>
</label>
</div>
</div>
<div class="btn-flex">
<button type="submit" name="update_submit_btn" value="update_submit_btn">更新</button>
<button type="submit" name="cancel_btn" value="cancel_btn">キャンセル</button>
</div>
</form>
</section>
</body>
</html>
uppdate-edit.php内で行われている処理は大きく3つに分けられます。
- 投稿編集画面へ遷移してきたときの処理
- 「更新ボタン」が押下されたときの処理
- 「キャンセルボタン」が押下されたときの処理
基本的には投稿機能の解説が理解できていれば、上記のコードもある程度は流れを追えるようになっているはずです。
しかし、いくつか投稿編集機能ならではの処理(★が付いている処理)があるため、そちらについて詳しくご説明します。
投稿編集画面へ遷移してきたときの処理
/**
* 編集ボタンで遷移してきたときの処理
*/
if (isset($_POST['update_btn'])) {
/**
* 編集対象の投稿情報を取得
*/
if (isset($_POST['post_id']) && $_POST['post_id'] != '') {
// ★セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // データをカラム名をキーとする連想配列で取得する
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容取得処理
*/
$sql = ('
SELECT id, title, comment
FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// ★投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
上記のハイライト部分について順番に解説します。
まず、以下のコードではセッション変数に編集対象の投稿IDを保存しています。
// ★セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
ここで、投稿編集画面内の処理で投稿IDを利用する目的は2つあります。
- 投稿IDを元にデータベースから現在の投稿内容を取得するため
- 投稿IDをキーとして投稿内容を上書き(UPDATE)するため
実は、セッションを利用せずとも、POSTで受け取った情報をそのまま使用して、データベースから投稿内容を取得することは可能です。もちろんPOSTで受け取った情報をキーとしてそのまま使用することも可能です。
それなのにどうしてわざわざセッション変数に保持するのかというと
です。
今回の投稿編集機能にも、未入力ならエラーメッセージを返す処理を持たせています。
もしもユーザが投稿内容をまっさらにして「更新ボタン」を押下したとすると、その時点でPOST送信されてきた投稿ID情報は消滅してしまい、投稿内容を上書きする際に必要な情報(投稿ID)が不足してしまうのです。
そのため、投稿編集画面へ遷移してきた時点でセッション変数に投稿IDを保持して、ユーザの操作内容によって必要な情報が消滅してしまうのを防ぐようにしています。
アプリ開発ではデータの生存期間を理解することが大切
続いて以下の投稿情報の取得処理についてです。
// ★投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
こちらも前述の処理と目的は同じで、データベースから取得した投稿内容データの生存期間を伸ばすためのものです。
想定しているシチュエーションは、「投稿編集画面でページ更新が行われたとき」です。
ページ更新が行われてしまうとPOST送信された情報は消滅してしまい、編集フォームから元々の投稿内容が消えてしまうのです。そこで、セッション変数に保持することでページ更新が行われたとしても確実に元々の投稿内容を出力できるようにしています。
「更新ボタン」が押下されたときの処理
/**
* 更新ボタンが押下されたときの処理
*/
if (isset($_POST['update_submit_btn'])) {
/**
* セッション変数に情報を保存して
* タイトルまたは投稿内容の片方だけが
* 入力されていた場合、
* 入力フォームに内容を保持する
*/
if (isset($_POST['post_title']) && $_POST['post_title'] != '') {
$_SESSION['title'] = $_POST['post_title'];
} else {
unset($_SESSION['title']);
}
if (isset($_POST['post_comment']) && $_POST['post_comment'] != '') {
$_SESSION['comment'] = $_POST['post_comment'];
} else {
unset($_SESSION['comment']);
}
/**
* エラーメッセージ格納
*/
if ($_POST['post_title'] == '') $err_msg_title = '※タイトルを入力して下さい';
if ($_POST['post_comment'] == '') $err_msg_comment = '※投稿内容を入力して下さい';
/**
* 必要項目がすべて入力されてたら更新処理を実行
*/
if (
isset($_POST['post_title']) && $_POST['post_title'] != '' &&
isset($_POST['post_comment']) && $_POST['post_comment'] != ''
) {
$title = $_POST['post_title'];
$comment = $_POST['post_comment'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容更新処理
*/
$sql = ('
UPDATE board_info
SET title = :TITLE, comment = :COMMENT
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
$stmt->bindValue(':TITLE', $title, PDO::PARAM_STR);
$stmt->bindValue(':COMMENT', $comment, PDO::PARAM_STR);
// SQL実行
$stmt->execute();
// 更新に成功したらセッション変数を破棄
// unset($_SESSION['id']); // ★※投稿IDは敢えて破棄せず、掲示板ページでID判定をするために情報を保持する
unset($_SESSION['title']);
unset($_SESSION['comment']);
// ★掲示板ページへ戻る
header('Location: board.php');
exit();
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
「更新ボタンが押下されたときの処理」では2か所についてご説明します。
// 更新に成功したらセッション変数を破棄
// unset($_SESSION['id']); // ★※投稿IDは敢えて破棄せず、掲示板ページでID判定をするために情報を保持する
unset($_SESSION['title']);
unset($_SESSION['comment']);
このコードでは更新が成功した時点でセッション変数の情報をまっさらにしています。
しかし、ハイライト箇所、つまり投稿IDについては、敢えてセッション変数から削除せずに情報を残しています。
その理由は
編集が完了し、掲示板トップへ戻った際に「更新しました」のテキストを出力するため
です。
それにより、どの投稿が編集されたかを分かりやすくしています。
ちなみにこの仕組みの理屈は、「投稿がセッション変数に保持している投稿IDと同じ投稿IDを持っていたら『更新しました』を表示する」となります。
該当する処理はboard.phpの以下の部分です。
<!-- 投稿一覧 -->
<section class="post-list">
<?php if (count($post_list) === 0) : ?>
~ 中略 ~
<?php else : ?>
<ul>
<!-- 投稿情報の出力 -->
<?php foreach ($post_list as $post_item) : ?>
<li>
~ 中略 ~
<?php if (isset($_SESSION['id']) && ($_SESSION['id'] == $post_item['id'])) echo "<p class='updated-post'>更新しました</p>" ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
続いて掲示板ページへ戻る処理の解説です。
// ★掲示板ページへ戻る
header('Location: board.php');
exit();
header(‘Location: 遷移先のパス’)と記述することで、ページの遷移先を指定可能です。
上記のコードでは遷移先としてboard.php(掲示板トップ)を指定しています。
この処理はアプリ開発で使用頻度が高めなのでしっかり理解しておきましょう。
「キャンセルボタン」が押下されたときの処理
/**★
* キャンセルボタンが押下されたら
* セッション情報を破棄して
* 掲示板一覧画面へ戻る
*/
if (isset($_POST['cancel_btn'])) {
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
header('Location: board.php');
exit();
}
こちらの処理はいたってシンプルで、掲示板トップへ戻る前にセッション情報を削除しているだけです。
ちなみにboard.php(トップページ)でも、編集処理で使用しているものと同じセッション変数を扱っています。
そのため、掲示板トップへ遷移する前にきちんとセッション情報を削除しておかないと、掲示板トップへ遷移したとき入力フォームに編集対象の投稿内容情報が表示されてしまうというおかしな状況になってしまいます。
編集機能における仕組みのポイントはデータの生存期間です。
はじめの内は「データがいつまで生きているのか」をイメージするのは慣れないかもしれませんが、実践を繰り返すことで確実に身に付く感覚です。
以上が編集機能の解説でした。
削除機能
最後、3つ目の機能である削除機能について解説をしていきます。
delete-confirm.php(削除確認画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* DB接続情報
*/
const DB_HOST = 'mysql:dbname=board;host=127.0.0.1;charset=utf8';
const DB_USER = 'kekenta';
const DB_PASSWORD = 'kekenta_pass';
/**
* 削除ボタンで遷移してきたときの処理
*/
if (isset($_POST['delete_btn'])) {
/**
* 編集対象の投稿情報を取得
*/
if (isset($_POST['post_id']) && $_POST['post_id'] != '') {
// セッションに投稿IDを保持
$_SESSION['id'] = $_POST['post_id'];
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容登録処理
*/
$sql = ('
SELECT id, title, comment
FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 投稿情報の取得
$post_info = $stmt->fetch();
$_SESSION['title'] = $post_info['title'];
$_SESSION['comment'] = $post_info['comment'];
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
}
/**
* 削除ボタンが押下されたときの処理
*/
if (isset($_POST['delete_submit_btn'])) {
try {
/**
* DB接続処理
*/
$pdo = new PDO(DB_HOST, DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 例外が発生した際にスローする
]);
/**
* 投稿内容削除処理
*/
$sql = ('
DELETE FROM board_info
WHERE id = :ID
');
$stmt = $pdo->prepare($sql);
// プレースホルダーに値をセット
$stmt->bindValue(':ID', $_SESSION['id'], PDO::PARAM_INT);
// SQL実行
$stmt->execute();
// 削除に成功したらセッション変数を破棄
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
// 削除成功画面へ遷移
header('Location: delete-success.php');
exit();
} catch (PDOException $e) {
echo '接続失敗' . $e->getMessage();
exit();
}
// DBとの接続を切る
$pdo = null;
$stmt = null;
}
/**
* キャンセルボタンが押下されたら
* セッション情報を破棄して
* 掲示板一覧画面へ戻る
*/
if (isset($_POST['cancel_btn'])) {
unset($_SESSION['id']);
unset($_SESSION['title']);
unset($_SESSION['comment']);
header('Location: board.php');
return;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>削除確認</h1>
<p class="delete-confirm-msg">以下の投稿を削除します。</p>
<!-- 削除確認画面 -->
<section class="post-form">
<form action="#" method="post">
<div class="post-form__flex">
<div>
<p>タイトル</p>
<p><?php if (isset($_SESSION['title'])) echo $_SESSION['title']; ?></p>
</div>
<div>
<p>投稿内容</p>
<p class="p-pre"><?php if (isset($_SESSION['comment'])) echo $_SESSION['comment']; ?></p>
</div>
</div>
<div class="btn-flex">
<button type="submit" name="delete_submit_btn" value="delete_submit_btn">削除</button>
<button type="submit" name="cancel_btn" value="cancel_btn">キャンセル</button>
</div>
</form>
</section>
</body>
</html>
delete-success.php(削除成功画面)
<?php
/**
* セッション開始
* セッションの保存期間を1800秒に指定 ※任意の秒数へ変更可能
* かつ、確実に破棄する
*/
ini_set('session.gc_maxlifetime', 1800);
ini_set('session.gc_divisor', 1);
session_start();
/**
* ★掲示場TOPへ自動で遷移する処理
*/
header('refresh: 3; url=board.php');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>掲示板アプリ</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>削除が完了しました。</h1>
<p class="delete-success-msg">3秒後に自動で掲示板TOPへ戻ります。</p>
</body>
</html>
削除機能の流れ
- 削除したい投稿の「削除ボタン」を押下する
- 削除確認画面へ遷移する
- 「削除ボタン」を押下する
- 削除を実行し、削除完了画面へ遷移する
- 3秒後に自動で掲示板トップへ遷移する
実は、削除機能で解説することはそれほど多くありません。
delete-confirm.php(削除確認画面)のコードこそ長いですが、処理の流れや構造は以下の通りで、update-edit.php(投稿編集画面)のときとほとんど同じです。
- 削除確認画面へ遷移してきたときの処理
- 「削除ボタン」が押下されたときの処理
- 「キャンセルボタン」が押下されたときの処理
異なるのはザックリ以下の3点のみです。
- 「更新ボタン」が「削除ボタン」になっている
- 編集画面では入力フォーム(input、textareaタグ)だった部分が、単なるテキスト(pタグ)になっている
- 「削除ボタン」押下後に「削除成功画面へ」遷移する
しかも、最初の2つについてはPHPの処理ではなくHTMLでの表示に関係することのため、特筆する部分もありません。
したがって、本章での解説は「削除ボタン」押下後のdelete-success.php(削除成功画面)、しかもその内の1ヵ所のみとなります。
/**
* ★掲示場TOPへ自動で遷移する処理
*/
header('refresh: 3; url=board.php');
こちらは、削除機能の流れの最後にあたる「3秒後に自動で掲示板トップへ遷移する」を実現している処理です。
編集機能でも登場したheader()関数を利用しており、このように記述をすると
削除完了画面へ遷移してから3秒後にboard.phpへリダイレクトする
という処理が実行されます。
編集機能のときは、どの投稿を編集したのかが分かるように「更新しました」を表示する仕様としました。
一方、削除のときは、ユーザにしっかりと「削除した」ことを伝えるために削除完了画面へ遷移させ、その後は操作不要で自動的に掲示板トップへ遷移する実装にする実装としています。
※もちろん「トップへ戻る」というリンクを設置する形でも問題ありません。
最初は難しく感じるかもしれないけど、実践を繰り返せば必ずできるようになる
以上、PHPで掲示板アプリを作成する方法について解説をしてきました。
簡単な振り返りとして、当記事でご紹介した掲示板アプリのポイントは以下の通りです。
- 基本機能は3つ(投稿・編集・削除)
- ログイン機能は持たないが、セッションを活用し投稿者IDでユーザを識別
- 編集・削除後にユーザへ完了内容を適切に伝達
- 空白投稿は拒否(エラーメッセージの出力)
正直、初めてWebアプリを作成される方にとっては非常に難しい内容だったかと思います。
こんなことまで考えて作れないよ……
と思われた方もいらっしゃるのではないかと想像しています。
しかし、実際に自分の手でアプリ開発をしてみると分かりますが、少なくとも慣れない内はいわゆる「一発書き」は難しいです。
実装するべきことを整理し、それらを少しずつプログラミングしていく途中途中で
「ここはこうした方が良いな」
ということに気が付き、改良を重ねていくという流れになることが多いです。
その道のりを超えると、当記事でご紹介したようなコードが出来上がります。
僕も経験がありますが、もしかしたら後からコードを振り返ったとき
(自分、よくこんな風にプログラミングできたな……)
と感じることもあるかもしれません。
プログラミングは意外とそんなもので、やっているときとそれを客観視したときとで、体感するレベル感にギャップが生まれがちです。
それを埋めていくためには、やはり実践しかありません。
プログラミングを自力でできるようになる=習得するためには多くの時間と労力がかかりますが、勉強を継続することで必ず身に付けることが可能です。
この記事がプログラミング初心者の方にとって役に立ったなら何よりです。
セキュリティ対策実装verについては以下の記事をご覧下さい!
最後までご覧いただきありがとうございました。
また別の記事でお会いできたら光栄です。
ご自身でセキュリティ対策についてきちんと勉強したいという方には以下の書籍がおすすめです。
(通称:徳丸本と呼ばれる「Webアプリ開発者必読」とまで言われている書籍です)
体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践
徳丸 浩/著 SBクリエイティブ/出版│Amazon
PHPの独学に限界を感じている方へ