これは、ユーザーアカウント管理システム、認証、役割、権限に関するシリーズの第2部です。最初の部分はここにあります。
データベース構成
user-accountsというMySQLデータベースを作成します。次に、プロジェクトのルートフォルダ(user-accountsフォルダ)でファイルを作成し、config.phpという名前を付けます。このファイルは、データベース変数を構成し、アプリケーションを作成したMySQLデータベースに接続するために使用されます。
config.php:
<?php
session_start(); // start session
// connect to database
$conn = new mysqli("localhost", "root", "", "user-accounts");
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// define global constants
define ('ROOT_PATH', realpath(dirname(__FILE__))); // path to the root folder
define ('INCLUDE_PATH', realpath(dirname(__FILE__) . '/includes' )); // Path to includes folder
define('BASE_URL', 'http://localhost/user-accounts/'); // the home url of the website
?>
また、ユーザー名などのログインしたユーザー情報を保存するために後で使用する必要があるため、セッションを開始しました。ファイルの最後に、ファイルのインクルードをより適切に処理するのに役立つ定数を定義しています。
これで、アプリケーションがMySQLデータベースに接続されました。ユーザーが詳細を入力してアカウントを登録できるフォームを作成しましょう。プロジェクトのルートフォルダにsignup.phpファイルを作成します:
signup.php:
<?php include('config.php'); ?>
<?php include(INCLUDE_PATH . '/logic/userSignup.php'); ?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>UserAccounts - Sign up</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
<!-- Custom styles -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<?php include(INCLUDE_PATH . "/layouts/navbar.php") ?>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<form class="form" action="signup.php" method="post" enctype="multipart/form-data">
<h2 class="text-center">Sign up</h2>
<hr>
<div class="form-group">
<label class="control-label">Username</label>
<input type="text" name="username" class="form-control">
</div>
<div class="form-group">
<label class="control-label">Email Address</label>
<input type="email" name="email" class="form-control">
</div>
<div class="form-group">
<label class="control-label">Password</label>
<input type="password" name="password" class="form-control">
</div>
<div class="form-group">
<label class="control-label">Password confirmation</label>
<input type="password" name="passwordConf" class="form-control">
</div>
<div class="form-group" style="text-align: center;">
<img src="http://via.placeholder.com/150x150" id="profile_img" style="height: 100px; border-radius: 50%" alt="">
<!-- hidden file input to trigger with JQuery -->
<input type="file" name="profile_picture" id="profile_input" value="" style="display: none;">
</div>
<div class="form-group">
<button type="submit" name="signup_btn" class="btn btn-success btn-block">Sign up</button>
</div>
<p>Aready have an account? <a href="login.php">Sign in</a></p>
</form>
</div>
</div>
</div>
<?php include(INCLUDE_PATH . "/layouts/footer.php") ?>
<script type="text/javascript" src="assets/js/display_profile_image.js"></script>
このファイルの最初の行には、config.phpがsignup.phpファイル内で提供するINCLUDE_PATH定数を使用する必要があるため、前に作成したconfig.phpファイルを含めています。このINCLUDE_PATH定数を使用して、データベースにユーザーを登録するためのロジックを保持するnavbar.php、footer.php、userSignup.phpも含まれています。これらのファイルはまもなく作成されます。
ファイルの終わり近くに、ユーザーがプロフィール画像をアップロードするためにクリックできる丸いフィールドがあります。ユーザーがこの領域をクリックしてコンピューターからプロフィール画像を選択すると、最初にこの画像のプレビューが表示されます。
この画像プレビューはjqueryで実現されます。ユーザーが[画像のアップロード]ボタンをクリックすると、JQueryを使用してプログラムでファイル入力フィールドがトリガーされます。これにより、ユーザーのコンピューターファイルが表示され、ユーザーがコンピューターを参照してプロフィール画像を選択できるようになります。彼らが画像を選択するとき、Jqueryを使用して画像を一時的に表示します。これを行うコードは、まもなく作成するdisplay_profile_image.phpファイルにあります。
まだブラウザで表示しないでください。まず、このファイルに私たちが借りているものを与えましょう。今のところ、assets / cssフォルダー内に、headセクションでリンクしたstyle.cssファイルを作成しましょう。
style.css:
@import url('https://fonts.googleapis.com/css?family=Lora');
* { font-family: 'Lora', serif; font-size: 1.04em; }
span.help-block { font-size: .7em; }
form label { font-weight: normal; }
.success_msg { color: '#218823'; }
.form { border-radius: 5px; border: 1px solid #d1d1d1; padding: 0px 10px 0px 10px; margin-bottom: 50px; }
#image_display { height: 90px; width: 80px; float: right; margin-right: 10px; }
このファイルの最初の行に、「Lora」という名前のGoogleフォントをインポートして、アプリのフォントをより美しくします。
このsignup.phpで次に必要なファイルは、navbar.phpファイルとfooter.phpファイルです。 include/layoutsフォルダ内に次の2つのファイルを作成します。
navbar.php:
<div class="container"> <!-- The closing container div is found in the footer -->
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">UserAccounts</a>
</div>
<ul class="nav navbar-nav navbar-right">
<li><a href="<?php echo BASE_URL . 'signup.php' ?>"><span class="glyphicon glyphicon-user"></span> Sign Up</a></li>
<li><a href="<?php echo BASE_URL . 'login.php' ?>"><span class="glyphicon glyphicon-log-in"></span> Login</a></li>
</ul>
</div>
</nav>
footer.php:
<!-- JQuery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</div> <!-- closing container div -->
</body>
</html>
signup.phpファイルの最後の行は、display_profile_image.jsという名前のJQueryスクリプトにリンクしており、その名前が示すとおりに機能します。このファイルをassets/jsフォルダー内に作成し、このコードをその中に貼り付けます:
display_profile_image.js:
$(document).ready(function(){
// when user clicks on the upload profile image button ...
$(document).on('click', '#profile_img', function(){
// ...use Jquery to click on the hidden file input field
$('#profile_input').click();
// a 'change' event occurs when user selects image from the system.
// when that happens, grab the image and display it
$(document).on('change', '#profile_input', function(){
// grab the file
var file = $('#profile_input')[0].files[0];
if (file) {
var reader = new FileReader();
reader.onload = function (e) {
// set the value of the input for profile picture
$('#profile_input').attr('value', file.name);
// display the image
$('#profile_img').attr('src', e.target.result);
};
reader.readAsDataURL(file);
}
});
});
});
そして最後に、userSignup.phpファイル。このファイルは、データベースで処理および保存するために登録フォームデータが送信される場所です。 include / logicフォルダ内にuserSignup.phpを作成し、その中にこのコードを貼り付けます:
userSignup.php:
<?php include(INCLUDE_PATH . "/logic/common_functions.php"); ?>
<?php
// variable declaration
$username = "";
$email = "";
$errors = [];
// SIGN UP USER
if (isset($_POST['signup_btn'])) {
// validate form values
$errors = validateUser($_POST, ['signup_btn']);
// receive all input values from the form. No need to escape... bind_param takes care of escaping
$username = $_POST['username'];
$email = $_POST['email'];
$password = password_hash($_POST['password'], PASSWORD_DEFAULT); //encrypt the password before saving in the database
$profile_picture = uploadProfilePicture();
$created_at = date('Y-m-d H:i:s');
// if no errors, proceed with signup
if (count($errors) === 0) {
// insert user into database
$query = "INSERT INTO users SET username=?, email=?, password=?, profile_picture=?, created_at=?";
$stmt = $conn->prepare($query);
$stmt->bind_param('sssss', $username, $email, $password, $profile_picture, $created_at);
$result = $stmt->execute();
if ($result) {
$user_id = $stmt->insert_id;
$stmt->close();
loginById($user_id); // log user in
} else {
$_SESSION['error_msg'] = "Database error: Could not register user";
}
}
}
このファイルにはさらに作業が必要だったため、最後に保存しました。まず、このファイルの先頭にcommon_functions.phpという名前の別のファイルを含めています。このファイルから派生した2つのメソッド、つまり、validateUser()とloginById()を使用しているため、このファイルを含めています。これらはまもなく作成されます。
include/logicフォルダに次のcommon_functions.phpファイルを作成します。
common_functions.php:
<?php
// Accept a user ID and returns true if user is admin and false if otherwise
function isAdmin($user_id) {
global $conn;
$sql = "SELECT * FROM users WHERE id=? AND role_id IS NOT NULL LIMIT 1";
$user = getSingleRecord($sql, 'i', [$user_id]); // get single user from database
if (!empty($user)) {
return true;
} else {
return false;
}
}
function loginById($user_id) {
global $conn;
$sql = "SELECT u.id, u.role_id, u.username, r.name as role FROM users u LEFT JOIN roles r ON u.role_id=r.id WHERE u.id=? LIMIT 1";
$user = getSingleRecord($sql, 'i', [$user_id]);
if (!empty($user)) {
// put logged in user into session array
$_SESSION['user'] = $user;
$_SESSION['success_msg'] = "You are now logged in";
// if user is admin, redirect to dashboard, otherwise to homepage
if (isAdmin($user_id)) {
$permissionsSql = "SELECT p.name as permission_name FROM permissions as p
JOIN permission_role as pr ON p.id=pr.permission_id
WHERE pr.role_id=?";
$userPermissions = getMultipleRecords($permissionsSql, "i", [$user['role_id']]);
$_SESSION['userPermissions'] = $userPermissions;
header('location: ' . BASE_URL . 'admin/dashboard.php');
} else {
header('location: ' . BASE_URL . 'index.php');
}
exit(0);
}
}
// Accept a user object, validates user and return an array with the error messages
function validateUser($user, $ignoreFields) {
global $conn;
$errors = [];
// password confirmation
if (isset($user['passwordConf']) && ($user['password'] !== $user['passwordConf'])) {
$errors['passwordConf'] = "The two passwords do not match";
}
// if passwordOld was sent, then verify old password
if (isset($user['passwordOld']) && isset($user['user_id'])) {
$sql = "SELECT * FROM users WHERE id=? LIMIT 1";
$oldUser = getSingleRecord($sql, 'i', [$user['user_id']]);
$prevPasswordHash = $oldUser['password'];
if (!password_verify($user['passwordOld'], $prevPasswordHash)) {
$errors['passwordOld'] = "The old password does not match";
}
}
// the email should be unique for each user for cases where we are saving admin user or signing up new user
if (in_array('save_user', $ignoreFields) || in_array('signup_btn', $ignoreFields)) {
$sql = "SELECT * FROM users WHERE email=? OR username=? LIMIT 1";
$oldUser = getSingleRecord($sql, 'ss', [$user['email'], $user['username']]);
if (!empty($oldUser['email']) && $oldUser['email'] === $user['email']) { // if user exists
$errors['email'] = "Email already exists";
}
if (!empty($oldUser['username']) && $oldUser['username'] === $user['username']) { // if user exists
$errors['username'] = "Username already exists";
}
}
// required validation
foreach ($user as $key => $value) {
if (in_array($key, $ignoreFields)) {
continue;
}
if (empty($user[$key])) {
$errors[$key] = "This field is required";
}
}
return $errors;
}
// upload's user profile profile picture and returns the name of the file
function uploadProfilePicture()
{
// if file was sent from signup form ...
if (!empty($_FILES) && !empty($_FILES['profile_picture']['name'])) {
// Get image name
$profile_picture = date("Y.m.d") . $_FILES['profile_picture']['name'];
// define Where image will be stored
$target = ROOT_PATH . "/assets/images/" . $profile_picture;
// upload image to folder
if (move_uploaded_file($_FILES['profile_picture']['tmp_name'], $target)) {
return $profile_picture;
exit();
}else{
echo "Failed to upload image";
}
}
}
このファイルの2つの重要な機能に注意を向けさせてください。それらは、getSingleRecord()とgetMultipleRecords()です。これらの関数は非常に重要です。アプリケーション全体のどこでも、データベースからレコードを選択する場合は、getSingleRecord()関数を呼び出して、SQLクエリを渡すだけだからです。複数のレコードを選択したい場合は、ご想像のとおり、適切なSQLクエリを渡してgetMultipleRecords()関数も呼び出すだけです。
これらの2つの関数は、SQLクエリ、変数タイプ(たとえば、「s」は文字列、「si」は文字列と整数などを意味します)、最後にすべての値の配列である3番目のパラメーターの3つのパラメーターを取ります。クエリを実行するには
たとえば、usernameが「John」で24歳のusersテーブルから選択する場合は、次のようにクエリを記述します。
$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query $user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query
関数呼び出しでは、「s」は文字列型を表し(ユーザー名「John」は文字列であるため)、「i」は整数を意味します(20歳は整数です)。この関数を使用すると、アプリケーションの100の異なる場所でデータベースクエリを実行する場合に、これら2行だけを実行する必要がなくなるため、作業が非常に簡単になります。関数自体にはそれぞれ約8〜10行のコードがあるため、コードを繰り返す必要はありません。これらのメソッドを一度に実装しましょう。
config.phpファイルは、データベース構成を保持しているため、データベースクエリが実行されるすべてのファイルに含まれます。したがって、これらのメソッドを定義するのに最適な場所です。 config.phpをもう一度開き、ファイルの最後に次のメソッドを追加します。
config.php:
// ...More code here ...
function getMultipleRecords($sql, $types = null, $params = []) {
global $conn;
$stmt = $conn->prepare($sql);
if (!empty($params) && !empty($params)) { // parameters must exist before you call bind_param() method
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return $user;
}
function getSingleRecord($sql, $types, $params) {
global $conn;
$stmt = $conn->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
$stmt->close();
return $user;
}
function modifyRecord($sql, $types, $params) {
global $conn;
$stmt = $conn->prepare($sql);
$stmt->bind_param($types, ...$params);
$result = $stmt->execute();
$stmt->close();
return $result;
}
プリペアドステートメントを使用していますが、これはセキュリティ上の理由から重要です。
ここで、common_functions.phpファイルに戻ります。このファイルには、後で他の多くのファイルで使用される4つの重要な機能が含まれています。
ユーザーが登録するときに、ユーザーが正しいデータを提供していることを確認したいので、このファイルが提供するvalidateUser()関数を呼び出します。プロフィール画像が選択されている場合は、このファイルが提供するuploadProfilePicture()関数を呼び出してアップロードします。
ユーザーをデータベースに正常に保存できたら、すぐにログインしたいので、このファイルが提供するloginById()関数を呼び出します。ユーザーがログインするときに、ユーザーが管理者であるか通常であるかを知りたいので、このファイルが提供するisAdmin()関数を呼び出します。それらが管理者であることがわかった場合(isAdmin()がtrueを返す場合)、ダッシュボードにリダイレクトします。通常のユーザーの場合は、ホームページにリダイレクトします。
したがって、common_functions.phpファイルが非常に重要であることがわかります。管理セクションで作業するときは、これらすべての関数を使用します。これにより、作業が大幅に削減され、コードの繰り返しが回避されます。
ユーザーがサインアップできるようにするには、usersテーブルを作成しましょう。ただし、usersテーブルはrolesテーブルに関連しているため、最初にrolesテーブルを作成します。
役割表:
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
)
ユーザーテーブル:
CREATE TABLE `users`(
`id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
`role_id` INT(11) DEFAULT NULL,
`username` VARCHAR(255) UNIQUE NOT NULL,
`email` VARCHAR(255) UNIQUE NOT NULL,
`password` VARCHAR(255) NOT NULL,
`profile_picture` VARCHAR(255) DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
CONSTRAINT `users_ibfk_1` FOREIGN KEY(`role_id`) REFERENCES `roles`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION
)
ユーザーテーブルは、多対1の関係にあるロールテーブルに関連しています。ロールがロールテーブルから削除された場合、以前にそのrole_idを属性として持っていたすべてのユーザーの値をNULLに設定する必要があります。これは、ユーザーが管理者ではなくなることを意味します。
テーブルを手動で作成する場合は、この制約を追加することをお勧めします。 PHPMyAdminを使用している場合は、ユーザーテーブルの[構造]タブをクリックし、次にリレーションビューテーブルをクリックして、最後に次のようにこのフォームに入力することで、これを行うことができます。
この時点で、システムはユーザーが登録できるようにし、登録後、自動的にログインします。ただし、ログイン後、loginById()関数に示されているように、ユーザーはホームページ(index.php)にリダイレクトされます。そのページを作成しましょう。アプリケーションのルートで、index.phpという名前のファイルを作成します。
index.php:
<?php include("config.php") ?>
<?php include(INCLUDE_PATH . "/logic/common_functions.php"); ?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>UserAccounts - Home</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
<!-- Custome styles -->
<link rel="stylesheet" href="static/css/style.css">
</head>
<body>
<?php include(INCLUDE_PATH . "/layouts/navbar.php") ?>
<?php include(INCLUDE_PATH . "/layouts/messages.php") ?>
<h1>Home page</h1>
<?php include(INCLUDE_PATH . "/layouts/footer.php") ?>
次に、ブラウザを開き、http://localhost/user-accounts/signup.phpにアクセスして、フォームにテスト情報を入力します(後でユーザーを使用してログインするため、テスト情報を覚えておいてください)。サインアップボタン。すべてがうまくいけば、ユーザーはデータベースに保存され、アプリケーションはホームページにリダイレクトされます。
ホームページには、まだ作成していないmessages.phpファイルが含まれているために発生するエラーが表示されます。すぐに作成しましょう。
include / layoutsディレクトリに、messages.phpという名前のファイルを作成します。
messages.php:
<?php if (isset($_SESSION['success_msg'])): ?>
<div class="alert <?php echo 'alert-success'; ?> alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
<?php
echo $_SESSION['success_msg'];
unset($_SESSION['success_msg']);
?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['error_msg'])): ?>
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
<?php
echo $_SESSION['error_msg'];
unset($_SESSION['error_msg']);
?>
</div>
<?php endif; ?>
ホームページを更新すると、エラーはなくなります。
この部分は以上です。次のパートでは、サインアップフォーム、ユーザーログイン/ログアウトの検証を続行し、管理セクションでの作業を開始します。これは大変な作業のように聞こえますが、私を信じてください。特に、管理セクションでの作業を容易にするコードをすでに作成しているので、簡単です。
フォローしていただきありがとうございます。あなたが一緒に来ていることを願っています。何か考えがあれば、下のコメントにドロップしてください。エラーが発生した場合、または何かがわからない場合は、コメントセクションでお知らせください。サポートさせていただきます。
次のパートでお会いしましょう。