5. 結果表示ページ
<< 前の記事 【④DB接続設定ファイルを作る】
4. DB接続設定
4-1. PDOオブジェクトの属性
4-1-1. フェッチ形式の指定
4-1-2. エラーモードの設定
4-1-2-1. エラーモードの違いによるエラー文の違い
4-1-3. エミュレーションの設定
4-1-3-1. プリペアドステートメント
4-1-3-2. エミュレーション
4-2. 例外発生時の処理
4-2-1. HTTPヘッダの送信
4-2-2. 処理の中断とエラーメッセージの表示
4-3. DBの切断について
ここではプレイヤーの回答の正解/不正解、選択した難易度、回答時間、リセット回数を表示するページを実装します。
<?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. 無効なアクセスの拒否
// 無効なアクセスの拒否
if (empty($_SESSION) || empty($_POST)) {
header('Location:start.php');
exit();
}
「find_the_mistake.php」ではセッション変数のみ存在確認していましたが、
ここでは押されたボタンのvalue
属性の値がPOST
されているはずなので、
その値がない場合もスタートページへリダイレクトさせています。
5-2. 回答時間の算出
// 回答時間を算出
$end_time = microtime(true);
$start_time = $_SESSION['start_time'];
$time = sprintf('%05.2f', round($end_time - $start_time, 2)) . '秒';
「find_the_mistake.php」で開始時間を記録したのと同様に、
microtime()
を用いて終了時間を$end_time
へ格納しています。
次いで、開始時間をセッション変数から$start_time
へ格納し、
sprintf()
の第2引数として「終了時間 - 開始時間」の引き算1の結果を渡しています。
sprintf()
は第2引数の文字列を、
第1引数で指定したフォーマットの文字列に変換する関数で、
%05.2f
は「小数第2位まで0埋めした5桁の浮動小数点数」を意味します。
これらの各文字は指定子といい、次のような書式で記述します。
[%][符号指定子][パディング指定子][アライメント指定子][表示幅指定子][精度指定子][型指定子]
今回は符合指定子とアライメント指定子以外を下記のように指定しています。
-
%
・・・フォーマットとして認識させるためのパーセント文字2 -
0
・・・パディング指定子 指定した桁数に満たない部分を埋める記号(今回は0埋め) -
5
・・・表示幅指定子 表示する最低桁数 -
.2
・・・精度指定子 小数点以下の最低表示桁数 -
f
・・・型指定子 引数を浮動小数点数として表示する
詳細に関しては下記もご確認下さい。
PHPマニュアル「PHP: sprintf - Manual」
Let'sプログラミング ~初心者の方を対象としたプログラミングの総合学習サイト~
「指定の形式にフォーマット(sprintf) - 文字列関数 - PHP関数」
変換後、最後尾には文字「秒」を連結させ、変数$time
へ格納しています。
これで回答時間が算出できました。
5-3. 各変数への格納
// 回答を変数に格納
$answer = $_POST['answer'];
// セッション変数を変数に格納
$name = $_SESSION['name'];
$difficulty = $_SESSION['difficulty'];
$permission = $_SESSION['permission'];
$correct = $_SESSION['correct'];
$count = sprintf('%02d', $_SESSION['count']) . '回';
POST
された値はプレイヤーの回答として$answer
に格納しています。
またこのページからの望ましくないページ遷移を防ぐため3、
スタートページと同様にセッションを放棄させます。
問題表示ページのアクセス制限は$_SESSION
が定義されているかどうかで判定しているので、結果ページへやってきた時点でセッションを放棄させることによりよって問題表示ページヘ戻ることは出来なくなります。
これにより、PHPのスクリプト終了時点でセッション変数は初期化されているので、`タグ内で出力させるためには別の変数に格納しておく必要があります。
リセット回数は上記「回答時間の算出」で用いたsprintf()
を用いて、「0埋めした2桁の小数値」のフォーマットに変換してから$count
に格納しています。
5-4. 保存する回答時間とカウント数の上限設定
// 回答時間が100秒以上の場合は値を上書き
if ($time > 100) {
$time = '100秒以上';
}
// リセット回数が100回以上の場合は値を上書き
if ($count > 100) {
$count = '100回以上';
}
ここまでのコードでは回答時間とリセット回数に上限は存在しません。
このままでも構わないのですが、
- ランキングページのレイアウト崩れ
- 不必要に大きな値が表示されること
を事前に防ぐために、今回は制限を追加します。
具体的には、DBに保存される回答時間・カウント数がそれぞれ
- 100秒以上
- 100回以上
の場合に定型文に置き換わるようにしています。
5-5. 正解/不正解による表示メッセージの分岐
// 正解・不正解によるメッセージの分岐
if (html_entity_decode($correct) === $answer) {
$result = '正解です!';
} else {
$result = '不正解です。。。';
}
最も初歩的な条件分岐です。
注意点として、選択された文字が絵文字だった場合
$answer
に格納された値(=POST
された値)はブラウザへの出力を経ている為、
HTMLエンティティとしてではなく絵文字そのものとなっています。
一方、$correct
に格納された値(=セッション変数として保持されていた値)は、
ブラウザへの出力はないままなのでHTMLエンティティのまま格納されています。
この為、正解かどうか(=文字列が一致しているかどうか)を判定する為には、
- セッション変数として保持されていた値をデコードする
-
POST
された値をHTMLエンティティ化する
のどちらかを実行する必要があります。今回は前者を採用しています。
PHPマニュアル「PHP: html_entity_decode - Manual」
後者の場合は下記関数が使えます。
PHPマニュアル「PHP: htmlentities - Manual」
5-6. ランキングへの登録
プレイヤーの回答が正解かつランキングへの登録が許可されている場合のみ、DBへ保存します。
// 正解かつ許可されていた場合のみ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 テーブル作成」などで検索すればたくさんヒットすると思いますので、ここでは割愛します。
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」 by @kamohicokamo さん
また脚注1.でも少し触れましたが、一部の絵文字の文字数が正常にカウントされません(2倍にカウントされる)ので、name
カラムの文字数上限はバリデーションで制限した10文字の2倍の20文字としてます。
テーブル名やカラム名の命名規則に関しては下記記事が参考になります。
Qiita「データベースオブジェクトの命名規約 - Qiita」 by @genzouw さん
5-6-2. ランキングへの登録
// 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」
PHPマニュアル「PHP: PDO::prepare - Manual」
prepare()
メソッドはPDOStatement
オブジェクトを返すのでそれを$stmt
に格納し、次いでPDOStatement
クラスのexecute()
メソッドによって、
-
execute()
の引数に渡された値とプレースホルダーのバインド - SQL文の実行
を行っています。
PHPマニュアル「PHP: PDOStatement - Manual」
PHPマニュアル「PHP: PDOStatement::execute - Manual」
bindValue()
メソッドを使用せずにexecute()
の引数に値を配列として渡す場合の注意点として、NULL
以外はすべてPDO::PARAM_STR
扱いになるというのがあります。
今回のテーブルはid
カラム以外はすべて可変長文字列カラムですので問題ありませんが、テーブルのカラムが文字列以外の型の場合はexecute()
の引数は空にし、prepare()
とexecute()
の間でbindValue()
メソッドを使用した方がいいです。
Qiita「PHPでデータベースに接続するときのまとめ - PDO::prepare → PDOStatement::execute の2ステップでクエリを実行する - Qiita」 by @mpyw さん
結果を表示させ、ランキングに登録させることができました。
最後に、ランキングページを実装します。
<< 前の記事 【④DB接続設定ファイルを作る】
4. DB接続設定
4-1. PDOオブジェクトの属性
4-1-1. フェッチ形式の指定
4-1-2. エラーモードの設定
4-1-2-1. エラーモードの違いによるエラー文の違い
4-1-3. エミュレーションの設定
4-1-3-1. プリペアドステートメント
4-1-3-2. エミュレーション
4-2. 例外発生時の処理
4-2-1. HTTPヘッダの送信
4-2-2. 処理の中断とエラーメッセージの表示
4-3. DBの切断について