2. スタートページの実装
<< 前の記事 [【①ゲームの概要】](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c)
[1. 間違い探しゲームの概要](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-間違い探しゲームの概要) [1-1. ゲームの全体像](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-1-ゲームの全体像) [1-2. 開発環境及びファイルの全体像](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-開発環境及びファイルの全体像) [1-2-1. 開発・動作確認済み環境](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-1-開発動作確認済み環境) [1-2-2. 作成するファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-作成するファイル) [1-2-2-1. メインファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-1-メインファイル) [1-2-2-2. サブファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-2-サブファイル) [1-3. ソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-ソースコード) [1-3-1. メインファイルのソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-1-メインファイルのソースコード) [1-3-2. サブファイルのソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-2-サブファイルのソースコード) [1-4. 参考にさせて頂いた記事・サイトの一覧](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-4-参考にさせて頂いた記事サイトの一覧)次の記事 >> [【③問題表示ページを作る】](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2)
[3. 問題表示ページの実装](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-問題表示ページの実装) [3-1. 無効なアクセスの拒否](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-1-無効なアクセスの拒否) [3-2. リセット回数の計測](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-2-リセット回数の計測) [3-3. 問題用文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-問題用文字配列の設定) [3-3-1. 文字ペア配列の選択](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-1-文字ペア配列の選択) [3-3-2. 文字ペア配列及び文字ペアのシャッフル](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-2-文字ペア配列及び文字ペアのシャッフル) [3-3-3. 正解文字と不正解文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-正解文字と不正解文字配列の設定) [3-3-3-1. 正解文字の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-1-正解文字の設定) [3-3-3-2. 不正解文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-2-不正解文字配列の設定) [3-3-4. 問題用文字配列の生成](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-4-問題用文字配列の生成) [3-4. 開始時刻の記録](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-4-開始時刻の記録) [3-5. 選択肢の描写](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-5-選択肢の描写)まず、最初にプレイヤーが訪れることになるページを生成する「start.php」を実装します。
<?php
// セッションの開始
session_start();
// 送信されたデータの検証
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {
// 変数への代入
$name = $_POST['name'];
$difficulty = $_POST['difficulty'];
$permission = $_POST['permission'];
// 名前のバリデーション
if (empty(trim($name))) {
$error_msg = '名前に空白は無効です';
} elseif (mb_strlen($name, 'UTF-8') > 10) {
$error_msg = '名前は10字以内にしてください';
} elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
$error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
}
// セッション変数への格納と問題表示ページへの遷移
if (empty($error_msg)) {
$_SESSION['name'] = $name;
$_SESSION['difficulty'] = $difficulty;
$_SESSION['permission'] = $permission;
header('Location:find_the_mistake.php');
exit();
}
}
// セッションの放棄
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>間違い探し</title>
</head>
<body>
<h1>間違い探し</h1>
<p>20×20個のボタンが表示されるので、指定されたボタンを見つけ出して押しましょう!</p>
<?php if (isset($error_msg)): ?>
<hr>
<ul>
<li><?= $error_msg; ?></li>
</ul>
<hr>
<?php endif; ?>
<form method="POST">
<label>名前
<small> (10字以内)</small><br>
<input type="text" name="name" required value="<?php if (isset($name)) echo $name;?>">
</label>
<p>
<span>難易度</span><br>
<label>
<input type="radio" name="difficulty" value="易しい(絵文字)" required <?php if (empty($difficulty) || isset($difficulty) && $difficulty === '易しい(絵文字)') echo 'checked';?>>
易しい(絵文字)
</label>
<label>
<input type="radio" name="difficulty" value="難しい(漢字)" <?php if (isset($difficulty) && $difficulty === '難しい(漢字)') echo 'checked';?>>
難しい(漢字)
</label>
</p>
<p>
<span>ランキングへの登録</span><br>
<label>
<input type="radio" name="permission" value="許可しない" required <?php if (empty($permission) || isset($permission) && $permission === '許可しない') echo 'checked';?>>
許可しない
</label>
<label>
<input type="radio" name="permission" value="許可する" <?php if (isset($permission) && $permission === '許可する') echo 'checked';?>>
許可する
</label>
</p>
<input type="submit" value="問題に挑戦!(時間計測が開始されます)">
</form>
<p>
<button type="button" onclick="location.href='ranking.php'">ランキングページへ</button>
</p>
</body>
</html>
2-1. セッション
ページ間を移動しても名前や難易度などの値を保持するために、セッションを利用します。
$_SESSION
は定義済みのスーパーグローバル関数で、ページ間で遷移しても値を保持させることができます。
これは 'スーパーグローバル' あるいは自動グローバル変数と呼ばれるものです。 スクリプト全体を通してすべてのスコープで使用することができます。
引用元:PHPマニュアル「PHP: $_SESSION - Manual」
// セッションの開始
session_start();
2-1-1. セッションの開始
セッションを利用するにはsession_start()
を記述します。
注意点として、こちらの関数は「ブラウザに何かを出力する前に」呼び出す必要があります。
クッキーに基づくセッションを使用している場合、ブラウザに何か出力を行う前に session_start() をコールする必要があります。
引用元:PHPマニュアル「PHP: session_start - Manual」
この為、安全を見て<?php ?>
タグ内の最上部に記述しています。
また、理由については下記質問が参考になります。
Teratail「なぜsession_startより前に何も出力があってはいけない?」
2-1-2. セッションの放棄
<?php ?>
タグ内の後半でセッションを放棄しています。
// セッション変数の初期化
$_SESSION = [];
setcookie(session_name(), '', time() - 1, '/');
session_destroy();
後述のバリデーションに引っ掛からなかった場合には
header()
で別ページに遷移させ、exit()
で後続処理を停止させていますが、
このページはデフォルトでセッションを放棄させるようにしています。
これによって
- バリデーションに引っ掛かった
- 初回のアクセスだった
場合にはセッションが残らないようになっています。
各コードの意味は以下です。
-
$_SESSION = []
- セッション変数を全て初期化します。
-
setcookie(session_name(), '', time() - 1, '/');
- セッション開始時にセッションIDがクッキーに保存されるので、これも削除します。
-
session_destroy()
- セッションに登録されたデータを全て破棄します。
PHPマニュアル「PHP: session_destroy - Manual」
2つ目のsetcookie()
関数ですが、
- 第1引数 : クッキーの名前
- 第2引数 : クッキーの値
- 第3引数 : クッキーの有効期限
- 第4引数 : サーバー上でクッキーを有効としたいパス
となっています。
第1引数にsession_name()
関数によって取得した現在のセッション名(デフォルトではPHPSESSID
)、
第3引数にtime()
関数による現在のUnixタイムスタンプから- 1
した過去のタイムスタンプ(=有効期限切れ)
第4引数に/
を指定することでサーバードメイン配下の全てのパス
を指定し,cookieを実質的に削除しています。
PHPマニュアル「PHP: setcookie - Manual」
PHPマニュアル「PHP: session_name - Manual」
PHPマニュアル「PHP: time - Manual」
これらの処理はバリデーションを通過した場合には、
先述のexit()
で処理が止められるので実行されません。
2-2. バリデーション
// 送信されたデータの検証
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {
// 変数への代入
$name = $_POST['name'];
$difficulty = $_POST['difficulty'];
$permission = $_POST['permission'];
// 名前のバリデーション
if (empty(trim($name))) {
$error_msg = '名前に空白は無効です';
} elseif (mb_strlen($name, 'UTF-8') > 10) {
$error_msg = '名前は10字以内にしてください';
} elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
$error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
}
// 中略
}
2-2-1. 無効な送信の拒否
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {
// 中略
}
まず、$_POST
の存在確認がされてから、値の検証されるようにしています。
これは、開発者ツールなどでフロントのフォームが改竄された場合の送信を無効とするためです。
2-2-2. 名前のバリデーション
// 名前のバリデーション
if (empty(trim($name))) {
$error_msg = '名前に空白は無効です';
} elseif (mb_strlen($name, 'UTF-8') > 10) {
$error_msg = '名前は10字以内にしてください';
} elseif ($name !== preg_replace('/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $name)) {
$error_msg = '名前の前後に空白文字や制御文字を含めないで下さい';
}
入力された名前に対し、
- 半角スペースのみ無効
- 10文字以内
- 前後に空白文字/制御文字を含めない
のバリデーションをかけています。
2-2-2-1. 半角スペースのみ無効
trim()
関数はスペースを取り除く関数で、その結果が空だった場合にエラーメッセージを格納しています。
文字列の先頭および末尾にあるホワイトスペースを取り除く
引用元:PHPマニュアル「PHP: trim - Manual」
ただし、こちらの関数は全角スペースには対応していないので更にバリデーションをかけます。
2-2-2-2. 文字数
mb_strlen()
によって文字数をカウントし、
カウントが10を超えている場合にエラーメッセージを設定しています。
日本語にはマルチバイト文字なので、通常のstrlen()
ではなく
マルチバイト対応のmb_strlen()
を用いています1。
2-2-2-3. 空白文字/制御文字無効
名前の前後に空白文字や制御文字が含まれている場合を検知しています。
具体的には、「対象の文字列を空文字に変換する前と後の文字列が等しくない場合」を検知しています。
全角スペースのみもこちらで弾けます。
文字数判定の後にこの判定を入れているのは、
preg_replace()
が正規表現を使った重い処理であり、
文字数判定を後にしてしまうと何万文字と言う文字を送りつけられた場合に、
サーバーがダウンしてしまう可能性があるためです(ReDoS攻撃対策)。
これらについては下記参考記事もご参照下さい。
Qiita「【PHP】マルチバイト(全角スペース等)対応のtrim処理 - Qiita」 by @fallout さん
Qiita「正規表現の落とし穴(ReDoS - Regular Expressions DoS) - Qiita」 by @prograti さん
2-2-3. エラーメッセージの表示
<?php if (isset($error_msg)): ?>
<hr>
<ul>
<li><?= $error_msg; ?></li>
</ul>
<hr>
<?php endif; ?>
エラーメッセージがある場合のみ、
<hr>
タグで囲った中で<ul>
タグ<li>
タグを用いて
$error_msg
に格納されたエラーメッセージを出力しています。
2-2-4. 入力値の保持 - <input>
タグ
<form method="POST">
<label>名前
<small> (10字以内)</small><br>
<input type="text" name="name" required value="<?php if (isset($name)) echo $name;?>">
</label>
<p>
<span>難易度</span><br>
<label>
<input type="radio" name="difficulty" value="易しい(絵文字)" required <?php if (empty($difficulty) || isset($difficulty) && $difficulty === '易しい(絵文字)') echo 'checked';?>>
易しい(絵文字)
</label>
<label>
<input type="radio" name="difficulty" value="難しい(漢字)" <?php if (isset($difficulty) && $difficulty === '難しい(漢字)') echo 'checked';?>>
難しい(漢字)
</label>
</p>
<p>
<span>ランキングへの登録</span><br>
<label>
<input type="radio" name="permission" value="許可しない" required <?php if (empty($permission) || isset($permission) && $permission === '許可しない') echo 'checked';?>>
許可しない
</label>
<label>
<input type="radio" name="permission" value="許可する" <?php if (isset($permission) && $permission === '許可する') echo 'checked';?>>
許可する
</label>
</p>
<input type="submit" value="問題に挑戦!(時間計測が開始されます)">
</form>
データがPOST
された場合、
$name
$difficulty
$permission
に入力値が格納されているので、
条件分岐で変数が定義されていた場合は
-
text
タイプの<input>
タグでは**value
属性に変数の値** -
radio
タイプの<input>
タグでは**checked
属性**
をそれぞれecho
することで、
バリデーションで弾かれてページが再読み込みされた場合でも、
入力値を保持させることができます。
また、
$difficulty
$permission
が空だった場合(=初回訪問時)は、
- 難易度「易しい(絵文字)」
- ランキングへの登録「許可しない」
がデフォルトで選択されるようになっています。
2-3. 問題表示ページへの遷移
// セッション変数への格納と問題表示ページへの遷移
if (empty($error_msg)) {
$_SESSION['name'] = $name;
$_SESSION['difficulty'] = $difficulty;
$_SESSION['permission'] = $permission;
header('Location:find_the_mistake.php');
exit();
}
先程のバリデーションをパスした場合は、
$error_msg
にエラーメッセージが格納されないので、
この変数が空の場合は「バリデーションをパスした」ものと判断しています。
以降のページで名前と難易度とランキング登録への認否は使用するので、
$_SESSION
にキーを設定して代入しています。
その後、header()
によって次のファイルへと遷移させ、後続の処理をexit()
により停止させています2。
これでスタートページが完成しました!
次は問題表示ページを実装します。