目次
概要
目的
要件定義
開発環境
データベース設計
デバック準備
トップページ作成
ログイン後のページ作成
アカウント登録ページ作成
今後の課題
概要
PHP初学者がログインページを作成して、PHPについての知識をアウトプットする。
目的
PHP初心者の私がPHPを勉強するにあたって、自分で行った勉強の内容を備忘録として記事を作成していきます。
初学者向けのPHP勉強でToDoリストを作成するものがありますが、以前似たようなものを作成したので、今回はログインページを作成します。
今回の記事では、動くものを作る
というのが目的なため、セキュリティやデータベースの設計などはあまり考えずに実装します。
今回作成したコードをもとに次回以降の記事でコードを改修しつつ、PHPについての知識をインプット/アウトプットしていきたいと思います。
また、知識のアウトプットのとして記事を作成するため、ところどころ不確実なところがあるかと思いますが、お手柔らかにお願いいたします。
ここに載せているコードについて、当社のサービスとは一切関係ありません。
要件定義
- トップページ
- ログインのための「メールアドレス」と「パスワード」入力フォームがあること
- 「メールアドレス」と「パスワード」を入力して、ログインができるボタンがあること
- ログインに成功すると、ログイン成功ページに遷移すること
- ログインに失敗すると、遷移せず失敗した旨のメッセージが表示されること
- 新規登録のためのページに遷移するボタンがあること
- ログイン成功ページ
- ログインしている「メールアドレス」を表示すること(「パスワード」は表示しない)
- 「メールアドレス」と「パスワード」を変更できるフォームがあること
- ログアウトするボタンがあること
- 新規登録ページ
- 「メールアドレス」と「パスワード」を入力して登録できるフォームがあること
- トップページに遷移するボタンがあること
開発環境
- 使用PC : macBookAir M2 2023 15インチ
- macOS : Sonoma 14.6.1
- 実行環境 : MAMP 6.9.0 ARMCPU
- PHPバージョン : 7.4.33
- mysqlバージョン : 5.7.39
今回はPHP勉強ということで、簡単に導入できるMAMPを使って開発していきます。
(この記事を投稿している現時点(2024/10/15)では、私の環境だとMAMPの最新バージョンでApacheをうまく動かすことができなかったので、バージョンを下げたものを使用しています。)
ダウンロードしてインストール後、MAMPのフォルダ内のhtdocsフォルダ内にphp_loginというフォルダを作成し、その中にファイルを作成していきます。
動作確認を行う際には、MAMPの右上のStartボタンを押下したあと、http://localhost/php_login/
にアクセスします。
データベース設計
今回は、MAMPのMySQLを使用します。
http://localhost/phpMyAdmin/
からデータベースを表示します。
テーブルには、次のカラムを追加します。
- id(一意)
- email(一意)
- password
- create_date(自動入力)
- update_date(自動入力)
idは、アカウント登録した際に自動的に入力されるようにしています。
また、メールアドレスの重複を防ぐため、一意になるように設定しています。
さらに、create_dateはアカウントを登録した時間、update_dateはアカウントを更新した時間が入るように設定します。
CREATE DATABASE login;
CREATE TABLE login_info
(
id int(11) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
create_date DATETIME NOT NULL DEFAULT CURRENT_DATETIME,
update_date DATETIME NOT NULL DEFAULT CURRENT_DATETIME ON UPDATE CURRENT_DATETIME,
PRIMARY KEY(id)
);
まず、「login」というデータベースを作成します。
次に、「login_info」というテーブルとそのカラムを作成します。
テーブルで追加するカラムは上記の通りです。
デバッグ準備
万が一、構文の不備などでエラーが発生した場合、エラー文がデフォルトのままだと表示されません。
すなわち、完全に手探りな状態で開発することになってしまいます。(エラーログの場所が分からず、1日ほどログなしで書いていました...)
それだと、開発スピードが落ちてしまうため、エラーログを表示できるようにします。
エラーログをブラウザ上で表示させるには、php.ini
の中身を変更します。
php.ini
の場所は、
mac : /Applications/MAMP/bin/php/php[PHPのバージョン]/conf/php.ini
windows : C:\MAMP\bin\php\php[PHPのバージョン]\conf\php.ini
にあるようです。
PHPのバージョンは、MAMPのバージョンによって書いてある場所が異なるようですが、私の環境ではMAMPを開いてすぐの画面にありました。
このファイルをエディタで開いて、下記の設定を探します。
display_errors = Off
この設定を
display_errors = On
に書き換えます。
これで下記の画像のようにエラー文をブラウザ上で表示できるようになります。
備考 : エラーログファイルからエラーを見る
上記は、先輩エンジニアからのアドバイスをいただいて実践しました。
一方で、開発の際に私自身が別の方法でエラーログを見ていたので、その方法を残します。
今回は、phpinfo.phpという名前でファイルを作り、下記のコードで保存、MAMPのサーバーを起動した状態で、http://localhost/php_login/phpinfo.php/
にアクセスします。
<?php phpinfo(); ?>
error_logの欄を探すと、エラーログが表示されるファイルパスの場所が出てきます。
これを開いておけば、エラーが発生した際に自動的にエラーが表示されていきます。
トップページ作成
ここでは、
- ログインするためのメールアドレスとパスワード入力
- 新規登録をするための画面遷移
の2つを実装します。
まずはindex.phpを作成して、htmlの部分を書いていきます。
ログインのための機能の部分は<form>で囲って、情報を送信しやすくします。
<label>でメールアドレスの文字の部分を囲って、<input>と関連づけるために、forとnameに共通の変数を設定します。
同様にパスワードの部分も設定していきます。
新規登録するための画面遷移を設定します。
特殊なことをするわけではないので、aタグだけで画面遷移を書きます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログインページ</title>
</head>
<body>
<h1>ログインページ</h1>
<form method="POST" action="">
<div>
<label for="email">メールアドレス : </label>
<input type="text" name="email">
</div>
<div>
<label for="password">パスワード : </label>
<input type="password" name="password">
</div>
<input type="submit" value="送信">
</form>
<a href="./register.php">会員登録はこちら</a>
</body>
</html>
次にPHPの中身を書いていきます。
このPHPは、headerを使用しているため、htmlよりも前に記述します。
(headerより前に出力(htmlも含む)が行われていると、エラーが発生するそうなので、必ず最初に記述する)
ボタンが押下された際に、メールアドレスとパスワードが入力されているかを確認し、入っていなければ早期リターンで処理を中止します。
その後、メールアドレスとパスワードを変数に入力し、条件で両方に値が入っていることを確認します。
$email_value = $_POST['email'];
$password_value = $_POST['password'];
if(empty($email_value) || empty($password_value)) {
echo "メールアドレスとパスワードを入力してください";
exit;
}
データベースに接続して、SQLのクエリを実行する準備をします。
今回は、MAMPを使うので、アカウントの情報は、'mysql:host=localhost;dbname=login', 'root', 'root'を使用します。
エラーが発生した際に、何が起こったのかを表示できるようにするため、エラーモードを設定します。
メールアドレスを条件にして、そのレコードに保存されてあるパスワードを取得するクエリを文字列としてクエリを実行します。
実行した結果をフェッチして(連想配列として)変数に保存します。
$db = new PDO('mysql:host=localhost;dbname=login', 'root', 'root');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = 'SELECT password FROM login_info WHERE email = :email';
$stmt = $db->prepare($sql);
$stmt->bindParam(':email', $email_value, PDO::PARAM_STR);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
「返ってきたレコードが1件だけである」かつ「パスワードが合致した」場合、セッションを保存し、ログイン完了後のページに遷移します。
(パスワードはハッシュ化しているので、password_verify
で合致しているのかを判定しています。)
もし、レコードが2件以上返ってきた場合、登録できるメールアドレスは一意である想定なので、ユーザー側で対処できないエラーとしてメッセージを表示します。
もし、レコードの件数が0件である場合、ログインに失敗したメッセージを表示します。
if (count($results) === 1) {
$result = $results[0];
if (password_verify($password_value, $result['password'])) {
session_start();
$_SESSION['email'] = $email_value;
header('Location: ./login.php');
exit;
} else {
echo "メールアドレスもしくはパスワードが間違っています";
}
} else if (count($results) > 1) {
echo "予期せぬエラーが発生しました。";
} else {
echo "メールアドレスもしくはパスワードが間違っています";
}
また、try/catch文でデータベース操作が失敗した際のエラーを表示するようにします。
さらに、if($_SERVER["REQUEST_METHOD"] == "POST")
を記述してボタンを押下した際に動くよう設定します。
index.phpをまとめたコード
<?php
if($_SERVER["REQUEST_METHOD"] == "POST") {
$email_value = $_POST['email'];
$password_value = $_POST['password'];
if(empty($email_value) || empty($password_value)) {
echo "メールアドレスとパスワードを入力してください";
exit;
}
try {
$db = new PDO('mysql:host=localhost;dbname=login', 'root', 'root');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = 'SELECT password FROM login_info WHERE email = :email';
$stmt = $db->prepare($sql);
$stmt->bindParam(':email', $email_value, PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($results) === 1) {
$result = $results[0];
if (password_verify($password_value, $result['password'])) {
session_start();
$_SESSION['email'] = $email_value;
header('Location: ./login.php');
exit;
} else {
echo "メールアドレスもしくはパスワードが間違っています";
}
} else if (count($results) > 1) {
echo "予期せぬエラーが発生しました。";
} else {
echo "メールアドレスもしくはパスワードが間違っています";
}
} catch (PDOException $e) {
die("データベース接続失敗 : ". $e->getMessage());
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログインページ</title>
</head>
<body>
<h1>ログインページ</h1>
<form method="POST" action="">
<div>
<label for="email">メールアドレス : </label>
<input type="text" name="email">
</div>
<div>
<label for="password">パスワード : </label>
<input type="password" name="password">
</div>
<input type="submit" value="送信">
</form>
<a href="./register.php">会員登録はこちら</a>
</body>
</html>
ログイン後のページ作成
このページでは、
- ログインしているアカウントのメールアドレスとパスワードを変更する
- ログアウトする
の2種類を実装します。
ログイン時に保存したセッション情報をもとに現在ログインしているメールアドレスを表示します。
また、ログインページと同じように、変更用のメールアドレスとパスワード入力の<form>を設置します。
さらに、ログアウトするためのボタンを設置します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>会員ページ</title>
</head>
<body>
<h1>会員ページ</h1>
<?php
$email_session = $_SESSION['email'];
?>
<h2>ログイン中のメールアドレス : <?php echo $email_session; ?></h2>
<h2>登録内容を変更する</h2>
<form method="POST" action="">
<div>
<label for="email">メールアドレス</label>
<input type="text" name="email">
</div>
<div>
<label for="password">パスワードを入力</label>
<input type="password" name="password">
</div>
<input type="submit" name="change" value="変更する">
</form>
<form method="POST" action="">
<input type="submit" name="logout" value="ログアウト">
</form>
</body>
</html>
次に、PHPの中身を書いていきます。
このコードはログアウトボタンとbodyの閉じタグの間に記述します。
このPHPファイルでは、2つのPOSTメソッドが存在しているので、どちらが実行されたのかを条件分岐で判定しています。
ここでは、changeと付いたボタン(submit)の情報のみを取得して情報を更新します。
index.php
と同じような記述は割愛して、それ以外のところを見ていきます。
登録するメールアドレスが正しい形式なのかを判定して、不正なメールアドレスを入力しようとすると弾くようにしています。
SQLの部分を見ていきます。
大体は、ログインページと同じような記述ですが、主に異なる部分はUPDATE文とハッシュ化したパスワードの部分です。
すでにあるレコードを更新しにいくので、UPDATE文を使用します。
パスワードの漏洩を防ぐために、パスワードはハッシュ化して保存するようにしています。
これらのログイン情報の変更が完了すると、リロードして最新の情報を表示します。(HTML部分のメールアドレスの表示が更新されて、新しい表示に変わります。)
<?php
if(isset($_POST['change'])) {
if($_SERVER["REQUEST_METHOD"] == "POST") {
$email_value = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
$password_value = $_POST['password'];
if(empty($email_value) || empty($password_value)) {
echo "メールアドレスとパスワードを入力してください";
exit;
}
if (!filter_var($email_value, FILTER_VALIDATE_EMAIL)) {
echo "不正な形式のメールアドレスです。";
exit;
}
try {
$db = new PDO('mysql:host=localhost;dbname=login','root', 'root');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = 'UPDATE login_info SET email = :email, password = :password WHERE email = :old_email';
$stmt = $db->prepare($sql);
$hashed_password = password_hash($password_value, PASSWORD_DEFAULT);
$stmt->bindParam(':email', $email_value);
$stmt->bindParam(':password', $hashed_password);
$stmt->bindParam(':old_email', $email_session);
$stmt->execute();
$_SESSION['email'] = $email_value;
$_SESSION['message'] = '登録内容を変更しました';
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
} catch (PDOException $e) {
die("データベース接続失敗 : ". $e->getMessage());
}
}
}
?>
さらにhtmlよりも前にもPHPコードを記述していきます。
これは、ログアウトボタンを押下した際に処理をするコードです。
ログインページと同じく、headerを使用しているため、出力よりも前に記述する必要があります。
また、ログアウト後にはセッションの情報は必要ないため、セッション情報を削除します。
<?php
session_start();
if(isset($_POST['logout'])) {
session_unset();
session_destroy();
header('Location: ./index.php');
exit;
}
?>
login.phpをまとめたコード
<?php
session_start();
if(isset($_POST['logout'])) {
session_unset();
session_destroy();
header('Location: ./index.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>会員ページ</title>
</head>
<body>
<?php
if (isset($_SESSION['message'])) {
echo "<p>" . htmlspecialchars($_SESSION['message'], ENT_QUOTES, 'UTF-8') . "</p>";
unset($_SESSION['message']);
}
?>
<h1>会員ページ</h1>
<?php
$email_session = $_SESSION['email'];
?>
<h2>ログイン中のメールアドレス : <?php echo htmlspecialchars($email_session, ENT_QUOTES, 'UTF-8'); ?></h2>
<h2>登録内容を変更する</h2>
<form method="POST" action="">
<div>
<label for="email">メールアドレス</label>
<input type="text" name="email">
</div>
<div>
<label for="password">パスワードを入力</label>
<input type="password" name="password">
</div>
<input type="submit" name="change" value="変更する">
</form>
<form method="POST" action="">
<input type="submit" name="logout" value="ログアウト">
</form>
<?php
if(isset($_POST['change'])) {
if($_SERVER["REQUEST_METHOD"] == "POST") {
$email_value = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
$password_value = $_POST['password'];
if(empty($email_value) || empty($password_value)) {
echo "メールアドレスとパスワードを入力してください";
exit;
}
if (!filter_var($email_value, FILTER_VALIDATE_EMAIL)) {
echo "不正な形式のメールアドレスです。";
exit;
}
try {
$db = new PDO('mysql:host=localhost;dbname=login','root', 'root');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = 'UPDATE login_info SET email = :email, password = :password WHERE email = :old_email';
$stmt = $db->prepare($sql);
$hashed_password = password_hash($password_value, PASSWORD_DEFAULT);
$stmt->bindParam(':email', $email_value);
$stmt->bindParam(':password', $hashed_password);
$stmt->bindParam(':old_email', $email_session);
$stmt->execute();
$_SESSION['email'] = $email_value;
$_SESSION['message'] = '登録内容を変更しました';
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
} catch (PDOException $e) {
die("データベース接続失敗 : ". $e->getMessage());
}
}
}
?>
</body>
</html>
アカウント登録ページ作成
このページでは、
- アカウント登録を行う
- 登録後、自動的にログインページに遷移する
- ログインページに戻る
の3種類を実装していきます。
ログインページと同じような構造ですが、確認用パスワードを入力する欄を設置しています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登録画面</title>
</head>
<body>
<h1>登録画面</h1>
<form method="POST" action="">
<div>
<label for='email'>メールアドレス</label>
<input type="text" name='email'>
</div>
<div>
<label for='password'>パスワードを入力</label>
<input type="text" name='password'>
</div>
<div>
<label for='password_confirm'>パスワードを入力(確認用)</label>
<input type="text" name='password_confirm'>
</div>
<input type="submit" value="登録する">
</form>
<a href="./index.php">ログインページに戻る</a>
</body>
</html>
次に、htmlコードの前にPHPコードを記述していきます。
ここでも、同じくメールアドレス・パスワード・確認用パスワードが入力されているかを確認、パスワードと確認用パスワードが同じかどうかを確認しています。
SQLクエリの部分では、メールアドレスをWHERE文で絞り込んでいます。
これで返ってきたレコード数が1件以上あれば、既にメールアドレスが使用されているので、重複仕様を禁止するために登録できないよう早期リターンしています。
返ってきたレコード数が0件であるならば、入力したメールアドレスとハッシュ化したパスワードをINSERTして登録します。
さらに、自動的にログインページにリダイレクトする処理をしています。
<?php
if($_SERVER["REQUEST_METHOD"] == "POST") {
$email_value = $_POST['email'];
$password_value = $_POST['password'];
$password_confirm_value = $_POST['password_confirm'];
if(empty($email_value) || empty($password_value) || empty($password_confirm_value)) {
echo "メールアドレスとパスワードを入力してください";
exit;
}
if($password_value != $password_confirm_value) {
echo "パスワードとパスワード(確認用)が一致しません";
exit;
}
try {
$db = new PDO('mysql:host=localhost;dbname=login', 'root', 'root');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$duplicate_check = 'SELECT * FROM login_info WHERE email = "' . $email_value . '"';
$check_stmt = $db->prepare($duplicate_check);
$check_stmt->execute();
$cnt = $check_stmt->rowCount();
if($cnt > 0) {
echo "既に使われているメールアドレスです";
exit;
}
$insert_sql = 'INSERT INTO login_info (email, password) VALUES ("' . $email_value . '", "' . $password_value . '")';
$insert_stmt = $db->prepare($insert_sql);
$insert_stmt->execute();
echo "登録が完了しました<br>5秒後にログインページにリダイレクトします";
header('Refresh: 5; ./index.php');
exit;
} catch (PDOException $e) {
die("データベース接続失敗 : " . $e->getMessage());
}
}
?>
register.phpをまとめたコード
<?php
if($_SERVER["REQUEST_METHOD"] == "POST") {
$email_value = $_POST['email'];
$password_value = $_POST['password'];
$password_confirm_value = $_POST['password_confirm'];
if(empty($email_value) || empty($password_value) || empty($password_confirm_value)) {
echo "メールアドレスとパスワードを入力してください";
exit;
}
if (!filter_var($email_value, FILTER_VALIDATE_EMAIL)) {
echo "不正な形式のメールアドレスです。";
}
if($password_value != $password_confirm_value) {
echo "パスワードとパスワード(確認用)が一致しません";
exit;
}
try {
$db = new PDO('mysql:host=localhost;dbname=login', 'root', 'root');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$duplicate_check = 'SELECT COUNT(*) FROM login_info WHERE email = :email';
$check_stmt = $db->prepare($duplicate_check);
$check_stmt->bindParam(':email', $email_value, PDO::PARAM_STR);
$check_stmt->execute();
$cnt = $check_stmt->fetchColumn();
if($cnt > 0) {
echo "既に使われているメールアドレスです";
exit;
}
$hashed_password = password_hash($password_value, PASSWORD_DEFAULT);
$insert_sql = 'INSERT INTO login_info (email, password) VALUES (:email, :password)';
$insert_stmt = $db->prepare($insert_sql);
$insert_stmt->bindParam(':email', $email_value, PDO::PARAM_STR);
$insert_stmt->bindParam(':password', $hashed_password, PDO::PARAM_STR);
$insert_stmt->execute();
echo "登録が完了しました<br>5秒後にログインページにリダイレクトします";
header('Refresh: 5; URL=./index.php');
exit;
} catch (PDOException $e) {
error_log("データベースエラー: " . $e->getMessage());
die("エラーが発生しました。管理者にお問い合わせください。");
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登録画面</title>
</head>
<body>
<h1>登録画面</h1>
<form method="POST" action="">
<div>
<label for='email'>メールアドレス</label>
<input type="text" name='email'>
</div>
<div>
<label for='password'>パスワードを入力</label>
<input type="text" name='password'>
</div>
<div>
<label for='password_confirm'>パスワードを入力(確認用)</label>
<input type="text" name='password_confirm'>
</div>
<input type="submit" value="登録する">
</form>
<a href="./index.php">ログインページに戻る</a>
</body>
</html>
今後の課題
- 登録からログインを一度に行うこと
- アカウント登録時にユーザーネームも登録すること
- アカウント情報変更時にメールアドレス・パスワードを独立して変更できること
- method周りの理解
すぐに思いついたものだけでも改善すべき課題が多々あります。
作り終わったあとで、上記のような「これはこうしたほうがいいのではないか」と思うようなことがありました。
次回はこれらの課題を解決していきながら、PHPに対する知識をインプット/アウトプットしていきたいと思います。