PHPログイン機能サンプル


某QAサイトでよく見るログイン機能のコードはとあるQiitaの記事を参考にしているものが多いのですが、`if` のネストも深くて美しいコードじゃないし、SQLに変数を直接埋め込んでいたりと、決して参考にしてはいけないサンプルだったので、こちらを書いた。

詳しく仕組みを理解したいのであれば、「[PHPでログイン機能を実装するチュートリアル #1](https://qiita.com/ShibuyaKosuke/items/f114ffccf441edb2b745)」を読んで欲しいが、コピペで済ませるなら、せめてこのコードを使って欲しい。

サンプルデータは、ユーザー名：user、パスワード：password となっている。

```php
<?php

/**
 * database.php
 * @since 2018/09/18
 */
function h($string)
{
	return htmlspecialchars($string, ENT_QUOTES, 'utf-8');
}

function connect()
{
	$dsn = 'mysql:host=localhost;dbname=sample;charset=utf8mb4;';
	$username = 'root';
	$password = 'password';
	$options = [
		PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
		PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
	];
	$pdo = new PDO($dsn, $username, $password, $options);
	return $pdo;
}
```

```php
<?php
/**
 * index.php
 * @since 2018/09/18
 */
ini_set('display_errors', true);
error_reporting(E_ALL);

session_start();

require 'database.php';

// エラーを格納する変数
$err = [];

// 「ログイン」ボタンが押されて、POST通信のとき
if (filter_input(INPUT_SERVER, 'REQUEST_METHOD') === 'POST') {
	$user_name = filter_input(INPUT_POST, 'user_name');
	$password = filter_input(INPUT_POST, 'password');

	if ($user_name === '') {
		$err['user_name'] = 'ユーザー名は入力必須です。';
	}
	if ($password === '') {
		$err['password'] = 'パスワードは入力必須です。';
	}

	// エラーがないとき
	if (count($err) === 0) {

		// DB接続
		$pdo = connect();

		// ステートメント
		$stmt = $pdo->prepare('SELECT * FROM User WHERE user_name = ?');

		// パラメータ設定
		$params = [];
		$params[] = $user_name;

		// SQL実行
		$stmt->execute($params);

		// レコードセットを取得
		$rows = $stmt->fetchAll();

		// パスワード検証
		foreach ($rows as $row) {
			$password_hash = $row['password'];

			// パスワード一致
			if (password_verify($password, $password_hash)) {
				session_regenerate_id(true);
				$_SESSION['login_user'] = $row;
				header('Location:main.php');
				return;
			}
		}
		$err['login'] = 'ログインに失敗しました。';
	}
}
?>
<!DOCTYPE HTML>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<title>ログイン</title>
		<style type="text/css">
			.error {
				color: red;
			}
		</style>
	</head>
	<body>
		<div id="wrapper">
			<form action="" method="post">
				<?php if (isset($err['login'])) : ?>
					<p class="error"><?php echo h($err['login']); ?></p>
				<?php endif; ?>
				<p>
					<label for="user_name">ユーザー名</label>
					<input id="user_id" name="user_name" type="text" />
					<?php if (isset($err['user_name'])) : ?>
						<p class="error"><?php echo h($err['user_name']); ?></p>
					<?php endif; ?>
				</p>
				<p>
					<label for="">パスワード</label>
					<input id="password" name="password" type="password" />
					<?php if (isset($err['password'])) : ?>
						<p class="error"><?php echo h($err['password']); ?></p>
					<?php endif; ?>
				</p>
				<p>
					<button type="submit">ログイン</button>
				</p>
			</form>
		</div>
	</body>
</html>
```

```php
<?php
/**
 * main.php
 *
 * @since 2018/09/18
 */
session_start();

require 'database.php';
$login_user = $_SESSION['login_user'];
?>
<!DOCTYPE HTML>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
		<?php foreach ($login_user as $key => $val) : ?>
			<p><?php echo h($key); ?> : <?php echo h($val); ?></p>
		<?php endforeach; ?>
	</body>
</html>
```

```php
<?php
/**
 * adduser.php
 *
 * @since 2018/09/21
 */
ini_set('display_errors', true);
error_reporting(E_ALL);

session_start();

require 'database.php';

$err = [];

// 「ログイン」ボタンが押されて、POST通信のとき
if (filter_input(INPUT_SERVER, 'REQUEST_METHOD') === 'POST') {
	$user_name = filter_input(INPUT_POST, 'user_name');
	$password = filter_input(INPUT_POST, 'password');
	$password_conf = filter_input(INPUT_POST, 'password_conf');

	if ($user_name === '') {
		$err['user_name'] = 'ユーザー名は入力必須です。';
	}
	if ($password === '') {
		$err['password'] = 'パスワードは入力必須です。';
	}
	if ($password !== $password_conf) {
		$err['password_conf'] = 'パスワードが一致しません。';
	}

	// エラーがないとき
	if (count($err) === 0) {

		// DB接続
		$pdo = connect();

		// ステートメント
		$stmt = $pdo->prepare('INSERT INTO `User` (`id`, `user_name`, `password`) VALUES (null, ?, ?)');

		// パラメータ設定
		$params = [];
		$params[] = $user_name;
		$params[] = password_hash($password, PASSWORD_DEFAULT);

		// SQL実行
		$success = $stmt->execute($params);
	}
}
?>
<!DOCTYPE HTML>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<title></title>
        <style type="text/css">
            .error {
                color: red;
            }
        </style>
	</head>
	<body>
		<?php if (count($err) > 0) : ?>
			<?php foreach ($err as $e): ?>
				<p class="error"><?php echo h($e); ?></p>
			<?php endforeach; ?>
		<?php endif; ?>

		<?php if (isset($success) && $success) : ?>
			<p>登録に成功しました。</p>
			<p><a href="index.php">こちらからログインしてください。</a></p>
		<?php else: ?>
			<form action="" method="post">
				<p>
					<label for="user_name">ユーザー名</label>
					<input id="user_id" name="user_name" type="text" />
				</p>
				<p>
					<label for="">パスワード</label>
					<input id="password" name="password" type="password" />
				</p>
				<p>
					<label for="">確認用パスワード</label>
					<input id="password_conf" name="password_conf" type="password" />
				</p>
				<p>
					<button type="submit">ログイン</button>
				</p>
				<p>
					<a href="adduser.php">新規ユーザー登録</a>
				</p>
			</form>
		<?php endif; ?>
	</body>
</html>
```


```sql
CREATE TABLE `User` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'AI',
  `user_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '氏名',
  `password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'パスワード',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_name` (`user_name`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

INSERT INTO `User` (`id`, `user_name`, `password`)
VALUES
	(1,'user','$2y$10$ecRmAWY4n/jLa0tTzIaG7.SMhb1TfdROy3nXeG5aVZorUX1n6/WHO');
```