ブラウザのリロードによるフォームデータの二重登録を抑止する

フォームで送られたPOSTデータをデータベースに登録するページを作っていて遭遇した、ちょっとした問題。

> php経由でデータベースに登録後、完了ページを表示したところで、意図した表示にならないためソースを見ようとした
> 　↓
> 「フォーム再送信の確認」と出て再送信するまでソースが見られない
> 　↓
> 再読み込みする
> 　↓
> 先ほどのPOSTデータがデータベースに二重登録されてしまった

という次第。
実際、ユーザー側でリロードしてしまうこともあるわけで、これを抑止する方法を模索した。
ざっと検索するとJavaScriptでやる方法はあるようだが、スクリプトが面倒だし(自分が得意じゃないというのもある)、何より

> ユーザーの行動をシステム側で制限すべきではない

とか書いてあったりするので、物理的にリロード禁止するのはよろしくないようだ。
となれば、リロードしてもデータが登録されないようにすればよいわけで、[こんなページ](https://www.system-ido.com/risouken/index.php?page=tech&num=6)を参考にしてみたという覚書。

実験用として「フォームで送られた文字列をデータファイル```count.dat```に追加して登録する」というスクリプトを考える。
```test1.php
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>リロード禁止のテスト:入力フォーム</title>
</head>
<body>
	<form action="test2.php" method="post">
		<input type="text" name="str"><button type="submit">送信</button>
	</form>
</body>
</html>
```
```test2.php
<?php
if ($str = filter_input(INPUT_POST, 'str') ?? '') {
	$fh = fopen('count.dat', 'a+');
	fwrite($fh, "$str\n");
	fclose($fh);
}

$html = '';
foreach (file('count.dat', FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) {
	$html .= "$line<br>\n";
}
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>リロード禁止のテスト:処理・表示部</title>
</head>
<body>
count.datの中身：<br>
<?=$html?>
<a href="test1.php">もどる</a>
</body>
</html>
```
[^1]

```test1.php```のデータを```test2.php```を送って表示させた状態でリロードすると(POSTデータが残っているので)二重登録されることを確認。
方針としては、

1. 適当な文字列を用意し、フォームページでhidden属性のフィールドに格納する
2. 同じ文字列をsessionに入れる
3. 処理ページへ飛んだときにPOSTの文字列とsessionの文字列を比較し、同じならsessionのデータを空にする
4. POSTデータを使って処理
5. POSTの文字列とsessionの文字列が一致しない場合は処理を行わない

こうすることで、表示ページでリロードしても今度はsessionが空になっているため処理が行われない、という仕掛け。
```test1new.php
<?php
session_start(); //セッション開始
$no_reload = $_SESSION['no_reload'] = bin2hex(random_bytes(8)); //適当な文字列を入れる
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>リロード禁止のテスト:入力フォーム</title>
</head>
<body>
	<form action="test2new.php" method="post">
		<input type="hidden" name="no_reload" value="<?= $no_reload; ?>"> <!--hidden属性に文字列を格納 -->
		<input type="text" name="str"><button type="submit">送信</button>
	</form>
</body>
</html>
```
```test2new.php
<?php
session_start(); //セッション開始

if ($_SESSION['no_reload'] == filter_input(INPUT_POST, 'no_reload')) { //セッションの文字列とPOSTデータを比較し一致したら処理開始
	$_SESSION['no_reload'] = ''; //セッションの文字列を消去
	if ($str = filter_input(INPUT_POST, 'str') ?? '') { //ここからは同様の登録処理
		$fh = fopen('count.dat', 'a+');
		fwrite($fh, "$str\n");
		fclose($fh);
	}
}

$html = '';
foreach (file('count.dat', FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) {
	$html .= "$line<br>\n";
}

?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>リロード禁止のテスト:処理・表示部</title>
</head>
<body>
count.datの中身：<br>
<?=$html?>
<a href="test1new.php">もどる</a>
</body>
</html>
```

この状態でリロードするとsessionが空になっているためPOSTデータの```$_POST['str']```と一致せず、登録処理がスキップされる。
実際リロードすると「フォーム再送信の確認」は出るものの、データの二重登録は行われなくなった。
ところで「フォーム再送信の確認」そのものを出さないようにする方法はあるのだろうか？　と思って調べたが、キャッシュを消したりなんだかんだしてもうまくいかない。
結局「処理するページと結果表示ページを分ける」[^2]のが一番いいようで、それなら上みたいなセッションを使った処理はそもそもいらないということが判明。お疲れさまでした…。

[^1]:実験用なので出力時のエスケープ処理は省略。よいこのみなさんはこのスクリプトをそのまま使ってはダメよ。
[^2]:処理が終わったらそのままページを表示させるのでなく```header('Location: result.php');```などで飛ばすということ。