PHPプログラミング

最終更新日:
公開日:

ワークショップ

ひと言掲示板を作る(23)

リロードによる多重投稿を防止する

「ひと言掲示板」に、再読み込みしたら同じ投稿が重複送信されてしまうのを予防する機能を実装します。

この記事のポイント

  • 自動リダイレクトでフォームの再送信を防ぐ
  • 成功メッセージをセッションに保存してリダイレクト後に表示する

目次

リロードするとPOSTデータが再送信される

前回まで、掲示板の管理画面を作成してきました。
今回は一般向けの掲示板に戻って、リロード(再読み込み)による投稿の多重送信を防ぐ機能を実装していきます。

多重投稿の防止機能がない場合は、次のようにブラウザを再読み込みすると同じ内容で繰り返し投稿されてしまいます。
まずは通常の1回目の投稿です。

ここで、ブラウザを再読み込みします。
このとき、フォーム再送信のメッセージが表示されます。
以下のサンプルはChromeの場合ですが、ブラウザによってメッセージは異なります。

再読み込みすると、全く同じ内容で投稿が実行されていることが確認できます。

この再読み込みによって繰り返し投稿されてしまう事象を解決していきます。

「ひと言掲示板を作る」の概要については「ひと言掲示板を作る」をご覧ください。
デモはこちら

前回までに作成したコードはこちら:Github

再読み込みで同じ投稿がされる原因

ブラウザで再読み込みすると、どうして同じ内容の投稿が繰り返されてしまうのか。
その原因から解説します。

form要素の送信ボタンなどを押してページを移動する場合、フォームに入力された値もパラメータとしてセットで送信されます。
これは通信方式がGETPOSTのいずれであっても同じです。
ここで、パラメータで受け取ったデータに問題がなければ、正常に掲示板への書き込みが行われます。

そこで、移動先のページで再読み込みを行うとします。
すると、先ほどのパラメータもセットで再読み込みが行われます。
システムとしては、また新しい投稿があったと判断して正常に処理を行ってしまうため、掲示板にも同じ内容で書き込みが投稿される、という流れになります。
つまり、システムから見たら1回目も2回目も、10回目も全て同じ「投稿されたデータ」となります。

そこで、このような再読み込みによるデータの多重送信を防ぐための方法として、大きく2つの方法があります。

  • トークンやセッションを使ってページ遷移の正当性確認する
  • 自動リダイレクトを行い、再読み込みをパラメータのない状態にする

トークンやセッションを使った確認方法はLaravelなどのフレームワークでも取り入れられている方法です。
しかし、今回のようにフレームワークを使っていない場合での実装は複雑になってしまうため、2つ目の自動リダイレクトを使った方法で実装していきます。

自動リダイレクトを実装

投稿が正常に完了したときのみ、自動リダイレクトが行われるように設定していきます。
このリダイレクトにはheader関数を使います。
index.php」を開き、以下の赤字のコードを追記してください。

index.php

<?php

---- 省略 ----

if( !empty($_POST['btn_submit']) ) {

	// 空白除去
	$view_name = preg_replace( '/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $_POST['view_name']);
	$message = preg_replace( '/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $_POST['message']);

	// 表示名の入力チェック
	if( empty($view_name) ) {
		$error_message[] = '表示名を入力してください。';
	} else {

		// セッションに表示名を保存
		$_SESSION['view_name'] = $view_name;
	}

	// メッセージの入力チェック
	if( empty($message) ) {
		$error_message[] = 'ひと言メッセージを入力してください。';
	}

	if( empty($error_message) ) {
		
		// 書き込み日時を取得
		$current_date = date("Y-m-d H:i:s");

		// トランザクション開始
		$pdo->beginTransaction();

		try {

			// SQL作成
			$stmt = $pdo->prepare("INSERT INTO message (view_name, message, post_date) VALUES ( :view_name, :message, :current_date)");

			// 値をセット
			$stmt->bindParam( ':view_name', $view_name, PDO::PARAM_STR);
			$stmt->bindParam( ':message', $message, PDO::PARAM_STR);
			$stmt->bindParam( ':current_date', $current_date, PDO::PARAM_STR);

			// SQLクエリの実行
			$res = $stmt->execute();

			// コミット
			$res = $pdo->commit();

		} catch(Exception $e) {

			// エラーが発生した時はロールバック
			$pdo->rollBack();
		}

		if( $res ) {
			$_SESSION['success_message'] = 'メッセージを書き込みました。';
		} else {
			$error_message[] = '書き込みに失敗しました。';
		}

		// プリペアドステートメントを削除
		$stmt = null;

		header('Location: ./');
		exit;
	}
}

---- 省略 ----

投稿が成功したときのメッセージも、このタイミングでセッションの$_SESSION['success_message']に保存するように変更します。
今まではリダイレクトせずにそのまま表示していたため、シンプルに変数から表示すれば大丈夫でした。
しかしリダイレクトすると変数の中身もリセットされてしまうため、セッションやGETパラメータ(URLにデータを含める)などでリダイレクト前のページから後のページへ値を渡す必要があります。
今回は表示名の保存でもセッションを利用しているため、投稿が成功したときのメッセージも同様にセッションを利用します。

続くheader関数ですが、「Location:」の後ろにリンクを指定します。
今回は自分自身を呼び出すため、「./」としています。
header関数を実行した後にリダイレクト前のページでのPHP実行を終了させるため、次の行に「exit;」も追記しておきます。

最後に、先ほどセッションに保存した投稿成功メッセージを表示できるように修正しましょう。

index.php

---- 省略 ----

<body>
<h1>ひと言掲示板</h1>
<?php if( empty($_POST['btn_submit']) && !empty($_SESSION['success_message']) ): ?>
	<p class="success_message"><?php echo htmlspecialchars( $_SESSION['success_message'], ENT_QUOTES, 'UTF-8'); ?></p>
	<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>

---- 省略 ----

if文の条件式では、2つのことをチェックしています。

  • POSTパラメータの「書き込む」ボタンが押されていないか
  • 表示する成功メッセージのセッションがあるか

empty関数を使ってチェックを行い、いずれの条件も満たしていたらif文の中でセッションに入っているメッセージを出力します。
セッションのメッセージを出力するときに、htmlspecialchars関数でサニタイズしておきましょう。

セッションにメッセージが残っていると掲示板を開くたびに投稿成功メッセージが表示されてしまいます。
そこで、メッセージを出力した次の行でunset関数を使い、1度表示したメッセージをセッションから削除します。

今回のコード改修で、今まで成功メッセージを格納していた変数「$success_message」は不要になるため、「// 変数の初期化」で初期化しているコードは削除しても大丈夫です。

以上で多重投稿の防止機能が実装できました。
試しに投稿して、再読み込みをしてみましょう。

まずは普通に書き込んでみます。

書き込みが成功すると、上部に成功メッセージが表示され、投稿したメッセージも表示されます。

続いて、ブラウザの再読み込みをしてみます。

先ほど上部に表示されていた成功メッセージは非表示になり、多重投稿がされていないことも確認できました。

今回はここまでになります。
次回はひと言メッセージの投稿に文字数制限を設定します。

今回作成したコード:Github