HTML・PHP・MySQLだけで作る間違い探しゲーム【②スタートページを作る】

# 2. スタートページの実装
<details>
<summary><< 前の記事 [【①ゲームの概要】](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c)</summary>
&emsp;[1. 間違い探しゲームの概要](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-間違い探しゲームの概要)
&emsp;&emsp;[1-1. ゲームの全体像](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-1-ゲームの全体像)
&emsp;&emsp;[1-2. 開発環境及びファイルの全体像](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-開発環境及びファイルの全体像)
&emsp;&emsp;&emsp;[1-2-1. 開発・動作確認済み環境](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-1-開発動作確認済み環境)
&emsp;&emsp;&emsp;[1-2-2. 作成するファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-作成するファイル)
&emsp;&emsp;&emsp;&emsp;[1-2-2-1. メインファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-1-メインファイル)
&emsp;&emsp;&emsp;&emsp;[1-2-2-2. サブファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-2-サブファイル)
&emsp;&emsp;[1-3. ソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-ソースコード)
&emsp;&emsp;&emsp;[1-3-1. メインファイルのソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-1-メインファイルのソースコード)
&emsp;&emsp;&emsp;[1-3-2. サブファイルのソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-2-サブファイルのソースコード)
&emsp;&emsp;[1-4. 参考にさせて頂いた記事・サイトの一覧](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-4-参考にさせて頂いた記事サイトの一覧)
</details>
<details>
<summary>次の記事 >> [【③問題表示ページを作る】](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2)</summary>
&emsp;[3. 問題表示ページの実装](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-問題表示ページの実装)
&emsp;&emsp;[3-1. 無効なアクセスの拒否](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-1-無効なアクセスの拒否)
&emsp;&emsp;[3-2. リセット回数の計測](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-2-リセット回数の計測)
&emsp;&emsp;[3-3. 問題用文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-問題用文字配列の設定)
&emsp;&emsp;&emsp;[3-3-1. 文字ペア配列の選択](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-1-文字ペア配列の選択)
&emsp;&emsp;&emsp;[3-3-2. 文字ペア配列及び文字ペアのシャッフル](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-2-文字ペア配列及び文字ペアのシャッフル)
&emsp;&emsp;&emsp;[3-3-3. 正解文字と不正解文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-正解文字と不正解文字配列の設定)
&emsp;&emsp;&emsp;&emsp;[3-3-3-1. 正解文字の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-1-正解文字の設定)
&emsp;&emsp;&emsp;&emsp;[3-3-3-2. 不正解文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-2-不正解文字配列の設定)
&emsp;&emsp;&emsp;[3-3-4. 問題用文字配列の生成](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-4-問題用文字配列の生成)
&emsp;&emsp;[3-4. 開始時刻の記録](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-4-開始時刻の記録)
&emsp;&emsp;[3-5. 選択肢の描写](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-5-選択肢の描写)
</details>


まず、最初にプレイヤーが訪れることになるページを生成する「**start.php**」を実装します。

![スタート画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598695/5df28a94-cbb8-119f-62bd-b82e27bd7006.png)

```php: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](https://www.php.net/manual/ja/reserved.variables.session.php)」

```php:start.php
// セッションの開始
session_start();
```
### 2-1-1. セッションの開始

セッションを利用するには`session_start()`を記述します。
注意点として、こちらの関数は「**ブラウザに何かを出力する前に**」呼び出す必要があります。

>クッキーに基づくセッションを使用している場合、ブラウザに何か出力を行う前に session_start() をコールする必要があります。
>引用元：PHPマニュアル「[PHP: session_start - Manual](https://www.php.net/manual/ja/function.session-start.php)」

この為、安全を見て`<?php ?>`タグ内の**最上部**に記述しています。
また、理由については下記質問が参考になります。

Teratail「[なぜsession_startより前に何も出力があってはいけない？](https://teratail.com/questions/94655)」

### 2-1-2. セッションの放棄
`<?php ?>`タグ内の後半でセッションを放棄しています。

```php:start.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](https://www.php.net/manual/ja/function.session-destroy.php)」

2つ目の`setcookie()`関数ですが、

- 第1引数 : クッキーの**名前**
- 第2引数 : クッキーの**値**
- 第3引数 : クッキーの**有効期限**
- 第4引数 : サーバー上で**クッキーを有効としたいパス**

となっています。

第1引数に`session_name()`関数によって取得した現在のセッション名（デフォルトでは`PHPSESSID`）、
第3引数に`time()`関数による現在のUnixタイムスタンプから`- 1`した過去のタイムスタンプ（＝有効期限切れ）
第4引数に`/`を指定することでサーバードメイン配下の全てのパス
を指定し,cookieを実質的に削除しています。

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

これらの処理は**バリデーション**を通過した場合には、
先述の`exit()`で処理が止められるので実行されません。

## 2-2. バリデーション

```php:start.php
// 送信されたデータの検証
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. 無効な送信の拒否

```php:start.php
if (isset($_POST['name'], $_POST['difficulty'], $_POST['permission'])) {

	// 中略

}
```

まず、`$_POST`の**存在確認**がされてから、値の検証されるようにしています。
これは、開発者ツールなどでフロントのフォームが改竄された場合の送信を無効とするためです。

### 2-2-2. 名前のバリデーション

```php:start.php
	// 名前のバリデーション
	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](https://www.php.net/manual/ja/function.trim.php)」

ただし、こちらの関数は**全角スペースには対応していない**ので更にバリデーションをかけます。

#### 2-2-2-2. 文字数
`mb_strlen()`によって文字数をカウントし、
カウントが10を超えている場合にエラーメッセージを設定しています。

日本語にはマルチバイト文字なので、通常の`strlen()`ではなく
**マルチバイト対応**の`mb_strlen()`を用いています[^1]。
[^1]: 実は少々不具合が残っており、「☺️」や「❤️」の場合、`mb_stlen()`は正しくカウントしてくれません（2倍にカウントされてしまう）。「😂」や「💔」は正しくカウントしてくれるのに謎です...

#### 2-2-2-3. 空白文字／制御文字無効
名前の前後に**空白文字や制御文字**が含まれている場合を検知しています。
具体的には、「対象の文字列を空文字に変換する前と後の文字列が等しくない場合」を検知しています。
**全角スペースのみ**もこちらで弾けます。

文字数判定の後にこの判定を入れているのは、
`preg_replace()`が**正規表現を使った重い処理**であり、
文字数判定を後にしてしまうと何万文字と言う文字を送りつけられた場合に、
サーバーがダウンしてしまう可能性があるためです（**ReDoS攻撃対策**）。

これらについては下記参考記事もご参照下さい。

Qiita「[【PHP】マルチバイト(全角スペース等)対応のtrim処理 - Qiita](https://qiita.com/fallout/items/a13cebb07015d421fde3)」 by @fallout さん
Qiita「[正規表現の落とし穴（ReDoS - Regular Expressions DoS） - Qiita](https://qiita.com/prograti/items/9b54cf82a08302a5d2c7)」 by @prograti さん

### 2-2-3. エラーメッセージの表示

```php:start.php
		<?php if (isset($error_msg)): ?>
		<hr>
		<ul>
			<li><?= $error_msg; ?></li>
		</ul>
		<hr>
		<?php endif; ?>
```
エラーメッセージがある場合のみ、
`<hr>`タグで囲った中で`<ul>`タグ`<li>`タグを用いて
`$error_msg`に格納されたエラーメッセージを出力しています。

![スタート画面_エラーメッセージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598695/8a63129d-2be5-1a24-db0e-61b8d342520e.png)

### 2-2-4. 入力値の保持 - `<input>`タグ

```php:start.php
		<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. 問題表示ページへの遷移

```php:start.php
	// セッション変数への格納と問題表示ページへの遷移
	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]。

[^2]: 後続の処理にセッションの放棄があるので、処理を止めないとせっかくセッションに保存した情報が初期化されてしまいます。

これでスタートページが完成しました！
次は問題表示ページを実装します。

<details>
<summary><< 前の記事 [【①ゲームの概要】](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c)</summary>
&emsp;[1. 間違い探しゲームの概要](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-間違い探しゲームの概要)
&emsp;&emsp;[1-1. ゲームの全体像](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-1-ゲームの全体像)
&emsp;&emsp;[1-2. 開発環境及びファイルの全体像](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-開発環境及びファイルの全体像)
&emsp;&emsp;&emsp;[1-2-1. 開発・動作確認済み環境](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-1-開発動作確認済み環境)
&emsp;&emsp;&emsp;[1-2-2. 作成するファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-作成するファイル)
&emsp;&emsp;&emsp;&emsp;[1-2-2-1. メインファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-1-メインファイル)
&emsp;&emsp;&emsp;&emsp;[1-2-2-2. サブファイル](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-2-2-2-サブファイル)
&emsp;&emsp;[1-3. ソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-ソースコード)
&emsp;&emsp;&emsp;[1-3-1. メインファイルのソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-1-メインファイルのソースコード)
&emsp;&emsp;&emsp;[1-3-2. サブファイルのソースコード](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-3-2-サブファイルのソースコード)
&emsp;&emsp;[1-4. 参考にさせて頂いた記事・サイトの一覧](https://qiita.com/_Taturon_/items/5a8fb0f179f7ede6a60c#1-4-参考にさせて頂いた記事サイトの一覧)
</details>
<details>
<summary>次の記事 >> [【③問題表示ページを作る】](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2)</summary>
&emsp;[3. 問題表示ページの実装](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-問題表示ページの実装)
&emsp;&emsp;[3-1. 無効なアクセスの拒否](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-1-無効なアクセスの拒否)
&emsp;&emsp;[3-2. リセット回数の計測](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-2-リセット回数の計測)
&emsp;&emsp;[3-3. 問題用文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-問題用文字配列の設定)
&emsp;&emsp;&emsp;[3-3-1. 文字ペア配列の選択](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-1-文字ペア配列の選択)
&emsp;&emsp;&emsp;[3-3-2. 文字ペア配列及び文字ペアのシャッフル](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-2-文字ペア配列及び文字ペアのシャッフル)
&emsp;&emsp;&emsp;[3-3-3. 正解文字と不正解文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-正解文字と不正解文字配列の設定)
&emsp;&emsp;&emsp;&emsp;[3-3-3-1. 正解文字の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-1-正解文字の設定)
&emsp;&emsp;&emsp;&emsp;[3-3-3-2. 不正解文字配列の設定](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-3-2-不正解文字配列の設定)
&emsp;&emsp;&emsp;[3-3-4. 問題用文字配列の生成](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-3-4-問題用文字配列の生成)
&emsp;&emsp;[3-4. 開始時刻の記録](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-4-開始時刻の記録)
&emsp;&emsp;[3-5. 選択肢の描写](https://qiita.com/_Taturon_/items/cb1cee8a3035f476ccf2#3-5-選択肢の描写)
</details>