HTML・PHP・MySQLだけで作る間違い探しゲーム【⑤結果表示ページを作る】

# 5. 結果表示ページ
<details>
<summary><< 前の記事 [【④DB接続設定ファイルを作る】](https://qiita.com/_Taturon_/items/f8e464821892555e80aa)</summary>
&emsp;[4. DB接続設定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-db接続設定)
&emsp;&emsp;[4-1. PDOオブジェクトの属性](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-pdoオブジェクトの属性)
&emsp;&emsp;&emsp;[4-1-1. フェッチ形式の指定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-1-フェッチ形式の指定)
&emsp;&emsp;&emsp;[4-1-2. エラーモードの設定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-2-エラーモードの設定)
&emsp;&emsp;&emsp;&emsp;[4-1-2-1. エラーモードの違いによるエラー文の違い](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-2-1-エラーモードの違いによるエラー文の違い)
&emsp;&emsp;&emsp;[4-1-3. エミュレーションの設定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-3-エミュレーションの設定)
&emsp;&emsp;&emsp;&emsp;[4-1-3-1. プリペアドステートメント](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-3-1-プリペアドステートメント)
&emsp;&emsp;&emsp;&emsp;[4-1-3-2. エミュレーション](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-3-2-エミュレーション)
&emsp;&emsp;[4-2. 例外発生時の処理](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-2-例外発生時の処理)
&emsp;&emsp;&emsp;[4-2-1. HTTPヘッダの送信](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-2-1-httpヘッダの送信)
&emsp;&emsp;&emsp;[4-2-2. 処理の中断とエラーメッセージの表示](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-2-2-処理の中断とエラーメッセージの表示)
&emsp;&emsp;[4-3. DBの切断について](#4-3-dbの切断について)
</details>
<details>
<summary>次の記事 >> [【⑥ランキングページを作る】](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b)</summary>
&emsp;[6. ランキング表示ページ](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-ランキング表示ページ)
&emsp;&emsp;[6-1. 表示切り替え機能](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-1-表示切り替え機能)
&emsp;&emsp;[6-2. 入力値の保持 - <select>タグ](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-2-入力値の保持---selectタグ)
&emsp;&emsp;[6-3. ランキングの表示](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-3-ランキングの表示)
&emsp;&emsp;&emsp;[6-3-1. テーブルの表示](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-3-1-テーブルの表示)
&emsp;&emsp;&emsp;[6-3-2. テーブルデザイン](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-3-2-テーブルデザイン)
</details>

ここではプレイヤーの回答の**正解／不正解**、**選択した難易度**、**回答時間**、**リセット回数**を表示するページを実装します。
![結果ページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598695/b622d5b8-b88c-f350-6451-3e33e4cae47d.png)

```php:result.php
<?php

// セッションの開始
session_start();

// 無効なアクセスの拒否
if (empty($_SESSION) || empty($_POST)) {
	header('Location:start.php');
	exit();
}

// 回答時間を算出
$end_time = microtime(true);
$start_time = $_SESSION['start_time'];
$time = sprintf('%05.2f', $end_time - $start_time) . '秒';

// 回答を変数に格納
$answer = $_POST['answer'];

// セッション変数を変数に格納
$name = $_SESSION['name'];
$difficulty = $_SESSION['difficulty'];
$permission = $_SESSION['permission'];
$correct = $_SESSION['correct'];
$count = sprintf('%02d', $_SESSION['count']) . '回';

// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();

// 回答時間が100秒以上の場合は値を上書き
if ($time > 100) {
	$time = '100秒以上';
}

// リセット回数が100回以上の場合は値を上書き
if ($count > 100) {
	$count = '100回以上';
}

// 正解・不正解によるメッセージの分岐
if (html_entity_decode($correct) === $answer) {
	$result = '正解です！';
} else {
	$result = '不正解です。。。';
}

// 正解かつ許可されていた場合のみDBに登録
if ($result === '正解です！' && $permission === '許可する') {

	// db接続
	require_once('db_connect.php');

	// 新規登録処理
	$sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
	$stmt = $dbh->prepare($sql);
	$stmt->execute([$name, $difficulty, $time, $count]);
}

?>
<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<title>間違い探し</title>
	</head>
	<body>
		<h1><small>結果は...<?= $result ?></small></h1>
		<h2><small>難易度: <?= $difficulty; ?></small></h2>
		<h2><small>回答時間: <?= $time; ?></small></h2>
		<h2><small>リセット回数: <?= $count; ?></small></h2>
		<button type="button" onclick="location.href='start.php'">スタートページへ</button>
		<button type="button" onclick="location.href='ranking.php'">ランキングページへ</button></a>
	</body>
</html>
```
セッションの開始や放棄はこれまでのファイルと同様です。

## 5-1. 無効なアクセスの拒否
```php:result.php
// 無効なアクセスの拒否
if (empty($_SESSION) || empty($_POST)) {
	header('Location:start.php');
	exit();
}
```
「**find_the_mistake.php**」ではセッション変数のみ存在確認していましたが、
ここでは押されたボタンの**`value`属性の値**が`POST`されているはずなので、
その値がない場合もスタートページへリダイレクトさせています。

## 5-2. 回答時間の算出
```php:result.php
// 回答時間を算出
$end_time = microtime(true);
$start_time = $_SESSION['start_time'];
$time = sprintf('%05.2f', round($end_time - $start_time, 2)) . '秒';
```
「**find_the_mistake.php**」で[開始時間を記録した](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-4-開始時刻の記録)のと同様に、
`microtime()`を用いて**終了時間**を`$end_time`へ格納しています。

次いで、**開始時間**をセッション変数から`$start_time`へ格納し、
`sprintf()`の第2引数として「終了時間 - 開始時間」の引き算[^9]の結果を渡しています。

[^9]: `microtime()`のタイムスタンプはUnixエポックからの経過マイクロ秒数となっています。このため「新しい方（ここでは`$end_time`）」から「古い方（`$start_time`）」を引いた値が**経過時間のマイクロ秒数**となります。

`sprintf()`は第2引数の文字列を、
第1引数で**指定したフォーマットの文字列に変換**する関数で、
`%05.2f`は「**小数第2位まで0埋めした5桁の浮動小数点数**」を意味します。

これらの各文字は**指定子**といい、次のような書式で記述します。
`[%][符号指定子][パディング指定子][アライメント指定子][表示幅指定子][精度指定子][型指定子]`

今回は符合指定子とアライメント指定子以外を下記のように指定しています。

- `%`・・・フォーマットとして認識させるためのパーセント文字[^10]
- `0`・・・**パディング指定子** 指定した桁数に満たない部分を埋める記号（今回は0埋め）
- `5`・・・**表示幅指定子** 表示する最低桁数
- `.2`・・・**精度指定子** 小数点以下の最低表示桁数
- `f`・・・**型指定子** 引数を浮動小数点数として表示する

[^10]: これがないと各文字が指定子として認識されなくなります。また、この文字自体は指定子ではありません。

詳細に関しては下記もご確認下さい。

PHPマニュアル「[PHP: sprintf - Manual](https://www.php.net/manual/ja/function.sprintf.php)」
Let'sプログラミング ～初心者の方を対象としたプログラミングの総合学習サイト～
「[指定の形式にフォーマット(sprintf) - 文字列関数 - PHP関数](https://www.javadrive.jp/phpfunc/string/index7.html)」

変換後、最後尾には文字「秒」を連結させ、変数`$time`へ格納しています。

これで回答時間が算出できました。

## 5-3. 各変数への格納
```php:result.php
// 回答を変数に格納
$answer = $_POST['answer'];

// セッション変数を変数に格納
$name = $_SESSION['name'];
$difficulty = $_SESSION['difficulty'];
$permission = $_SESSION['permission'];
$correct = $_SESSION['correct'];
$count = sprintf('%02d', $_SESSION['count']) . '回';
```
`POST`された値はプレイヤーの回答として`$answer`に格納しています。

またこのページからの**望ましくないページ遷移を防ぐ**ため[^11]、
スタートページと同様にセッションを放棄させます。

[^11]: 例えば「このページからもう一度問題表示ページへ戻る」などです。個人的には「問題表示ページから結果ページへは一方通行としたい」と考えたためこのような実装となりました。

問題表示ページのアクセス制限は**`$_SESSION`が定義されているかどうか**で判定しているので、結果ページへやってきた時点でセッションを放棄させることによりよって問題表示ページヘ戻ることは出来なくなります。

これにより、PHPのスクリプト終了時点でセッション変数は初期化されているので、`<html>タグ内で出力させるためには**別の変数**に格納しておく必要があります。

リセット回数は上記「回答時間の算出」で用いた`sprintf()`を用いて、「**0埋めした2桁の小数値**」のフォーマットに変換してから`$count`に格納しています。

## 5-4. 保存する回答時間とカウント数の上限設定
```php:result.php
// 回答時間が100秒以上の場合は値を上書き
if ($time > 100) {
	$time = '100秒以上';
}

// リセット回数が100回以上の場合は値を上書き
if ($count > 100) {
	$count = '100回以上';
}
```
ここまでのコードでは回答時間とリセット回数に上限は存在しません。

このままでも構わないのですが、

- ランキングページの**レイアウト崩れ**
- **不必要に大きな値**が表示されること

を事前に防ぐために、今回は制限を追加します。

具体的には、DBに保存される回答時間・カウント数がそれぞれ

- **100秒以上**
- **100回以上**

の場合に**定型文に置き換わる**ようにしています。

## 5-5. 正解／不正解による表示メッセージの分岐
```php:result.php
// 正解・不正解によるメッセージの分岐
if (html_entity_decode($correct) === $answer) {
	$result = '正解です！';
} else {
	$result = '不正解です。。。';
}
```
最も初歩的な条件分岐です。

注意点として、選択された文字が絵文字だった場合
`$answer`に格納された値（=`POST`された値）は**ブラウザへの出力**を経ている為、
HTMLエンティティとして**ではなく**絵文字そのものとなっています。

一方、`$correct`に格納された値（=セッション変数として保持されていた値）は、
ブラウザへの出力はないままなので**HTMLエンティティのまま**格納されています。

この為、正解かどうか(=文字列が一致しているかどうか)を判定する為には、

- セッション変数として保持されていた値を**デコードする**
- `POST`された値を**HTMLエンティティ化する**

のどちらかを実行する必要があります。今回は前者を採用しています。

PHPマニュアル「[PHP: html_entity_decode - Manual](https://www.php.net/manual/ja/function.html-entity-decode)」

後者の場合は下記関数が使えます。

PHPマニュアル「[PHP: htmlentities - Manual](https://www.php.net/manual/ja/function.htmlentities.php)」

## 5-6. ランキングへの登録
プレイヤーの回答が**正解**かつランキングへの登録が**許可されている**場合のみ、DBへ保存します。

```php:result.php
// 正解かつ許可されていた場合のみDBに登録
if ($result === '正解です！' && $permission === '許可する') {

	// db接続
	require_once('db_connect.php');

	// 新規登録処理
	$sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
	$stmt = $dbh->prepare($sql);
	$stmt->execute([$name, $difficulty, $time, $count]);
}
```

### 5-6-1. ランキングテーブルの構成
まず先に、データを保存するランキングテーブルを構築する必要があります。詳しい方法については「MySQL テーブル作成」などで検索すればたくさんヒットすると思いますので、ここでは割愛します。

```sql:create_rankings_table.sql
CREATE TABLE `rankings` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `difficulty` varchar(10) NOT NULL,
  `time` varchar(10) NOT NULL,
  `reset` varchar(15) NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
```
一般的なテーブル構造となっています。各カラムの概要は以下です。

- `id`・・・主キー／符号なし／オートインクリメント属性が指定されたidカラム
- `name`・・・入力された名前を保存する可変長文字列／20字までのカラム
- `difficulty`・・・選択された難易度を保存する可変長文字列／10字までのカラム
- `time`・・・回答時間を保存する可変長文字列／10字までのカラム
- `reset`・・・リセット回数を保存する可変長文字列／15字までのカラム

いわゆる「**寿司ビール問題**」を回避する為に、**文字セット**は**`utf8mb4`**を、**照合順序**は**`utf8mb4_bin`**をデフォルトとしています。

Qiita「[寿司ビール問題①　初心者→中級者へのSTEP20/25 - Qiita](https://qiita.com/kamohicokamo/items/3cc05f63a90148525caf)」 by @kamohicokamo さん

また脚注1.でも少し触れましたが、一部の絵文字の文字数が**正常にカウントされません**（2倍にカウントされる)ので、`name`カラムの文字数上限はバリデーションで制限した10文字の**2倍**の20文字としてます。

テーブル名やカラム名の命名規則に関しては下記記事が参考になります。

Qiita「[データベースオブジェクトの命名規約 - Qiita](https://qiita.com/genzouw/items/35022fa96c120e67c637)」 by @genzouw さん

### 5-6-2. ランキングへの登録
```php:result.php
	// db接続
	require_once('db_connect.php');

	// 新規登録処理
	$sql = 'INSERT INTO rankings (name, difficulty, time, reset) VALUES (?, ?, ?, ?)';
	$stmt = $dbh->prepare($sql);
	$stmt->execute([$name, $difficulty, $time, $count]);
```
`require_once()`でDB接続設定用ファイル「**db_connect.php**」を呼び出してDB接続した後、`INSERT`文を用いて`rankings`テーブルにデータを挿入しています。

SQL文では**疑問符プレースホルダー**を用いたSQL文を記述し、`prepare()`メソッドによってDBサーバーに**プリペアドステートメントとして**SQL文を渡しています（今回のDB接続設定では**静的プレースホルダー**を選択している為）。

PHPマニュアル「[PHP: PDO - Manual](https://www.php.net/manual/ja/class.pdo.php)」
PHPマニュアル「[PHP: PDO::prepare - Manual](https://www.php.net/manual/ja/pdo.prepare.php)」

`prepare()`メソッドは`PDOStatement`オブジェクトを返すのでそれを`$stmt`に格納し、次いで`PDOStatement`クラスの`execute()`メソッドによって、

- `execute()`の引数に渡された値とプレースホルダーのバインド
- SQL文の実行

を行っています。

PHPマニュアル「[PHP: PDOStatement - Manual](https://www.php.net/manual/ja/class.pdostatement.php)」
PHPマニュアル「[PHP: PDOStatement::execute - Manual](https://www.php.net/manual/ja/pdostatement.execute.php)」

`bindValue()`メソッドを使用せずに`execute()`の引数に値を配列として渡す場合の注意点として、**`NULL`以外はすべて`PDO::PARAM_STR`扱い**になるというのがあります。

今回のテーブルは`id`カラム以外はすべて可変長文字列カラムですので問題ありませんが、テーブルのカラムが**文字列以外の型の場合**は`execute()`の引数は**空**にし、`prepare()`と`execute()`の間で`bindValue()`メソッドを使用した方がいいです。

Qiita「[PHPでデータベースに接続するときのまとめ - PDO::prepare → PDOStatement::execute の2ステップでクエリを実行する - Qiita](https://qiita.com/mpyw/items/b00b72c5c95aac573b71#pdoprepare--pdostatementexecute-%E3%81%AE2%E3%82%B9%E3%83%86%E3%83%83%E3%83%97%E3%81%A7%E3%82%AF%E3%82%A8%E3%83%AA%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B)」 by @mpyw さん

結果を表示させ、ランキングに登録させることができました。
最後に、ランキングページを実装します。

<details>
<summary><< 前の記事 [【④DB接続設定ファイルを作る】](https://qiita.com/_Taturon_/items/f8e464821892555e80aa)</summary>
&emsp;[4. DB接続設定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-db接続設定)
&emsp;&emsp;[4-1. PDOオブジェクトの属性](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-pdoオブジェクトの属性)
&emsp;&emsp;&emsp;[4-1-1. フェッチ形式の指定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-1-フェッチ形式の指定)
&emsp;&emsp;&emsp;[4-1-2. エラーモードの設定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-2-エラーモードの設定)
&emsp;&emsp;&emsp;&emsp;[4-1-2-1. エラーモードの違いによるエラー文の違い](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-2-1-エラーモードの違いによるエラー文の違い)
&emsp;&emsp;&emsp;[4-1-3. エミュレーションの設定](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-3-エミュレーションの設定)
&emsp;&emsp;&emsp;&emsp;[4-1-3-1. プリペアドステートメント](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-3-1-プリペアドステートメント)
&emsp;&emsp;&emsp;&emsp;[4-1-3-2. エミュレーション](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-1-3-2-エミュレーション)
&emsp;&emsp;[4-2. 例外発生時の処理](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-2-例外発生時の処理)
&emsp;&emsp;&emsp;[4-2-1. HTTPヘッダの送信](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-2-1-httpヘッダの送信)
&emsp;&emsp;&emsp;[4-2-2. 処理の中断とエラーメッセージの表示](https://qiita.com/_Taturon_/items/f8e464821892555e80aa#4-2-2-処理の中断とエラーメッセージの表示)
&emsp;&emsp;[4-3. DBの切断について](#4-3-dbの切断について)
</details>
<details>
<summary>次の記事 >> [【⑥ランキングページを作る】](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b)</summary>
&emsp;[6. ランキング表示ページ](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-ランキング表示ページ)
&emsp;&emsp;[6-1. 表示切り替え機能](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-1-表示切り替え機能)
&emsp;&emsp;[6-2. 入力値の保持 - <select>タグ](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-2-入力値の保持---selectタグ)
&emsp;&emsp;[6-3. ランキングの表示](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-3-ランキングの表示)
&emsp;&emsp;&emsp;[6-3-1. テーブルの表示](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-3-1-テーブルの表示)
&emsp;&emsp;&emsp;[6-3-2. テーブルデザイン](https://qiita.com/_Taturon_/items/5e6da2f18b341fcf744b#6-3-2-テーブルデザイン)
</details>