Summary: in this tutorial, you’ll learn how to create a PHP registration form from scratch.
Introduction to PHP registration form
In this tutorial, you’ll create a user registration form that consists of the following input fields:
- Username
- Password
- Password confirmation
- Agreement checkbox
- Register button
When a user fills out the form and click the Register button, you need to:
- Sanitize & validate the user inputs.
- If the form data is not valid, show the form with the user inputs and error messages.
- If the form data is valid, insert the new user into the
users
database table, set a flash message, redirect the user to thelogin.php
page and display the flash message. Note that you’ll learn how to build a login form in the next tutorial.
Setup project structure
First, create a project root folder, e.g., auth
.
Second, create the following folders under the project root folder e.g
├── config
├── public
└── src
├── inc
└── libs
Code language: plaintext (plaintext)
The following describes the purpose of each folder:
Folder | Purpose |
---|---|
config | Store the configuration file such as database configuration |
public | Store the public files accessed directly by the users |
src | Store the source files that should not be exposed to the public |
src/inc | Store the commonly included files such as the header and footer of a page |
src/libs | Store the library files, e.g., validation, sanitization, etc. |
Remove the public from the URL
First, create register.php
in the public
folder. To access the register.php
page, you need to use the following URL:
http://localhost/auth/public/register.php
Code language: plaintext (plaintext)
To remove the public
from the above URL, you can use the URL Rewrite module of the Apache Web Server. To do it, you need to use a .htaccess
file.
Second, create .htaccess
file in the project root folder (auth
) and use the following code:
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]
</IfModule>
Code language: plaintext (plaintext)
The above directives instruct Apache to remove the public
from the URL. If you open the URL http://localhost/auth/register.php
, you’ll see an error.
To fix the error, you’ll need another .htaccess
file in the public
folder.
Third, create another .htaccess
in the public
folder and use the following directives:
<IfModule mod_rewrite.c>
Options -Multiviews
RewriteEngine On
RewriteBase /auth/public
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
</IfModule>
Code language: plaintext (plaintext)
Now, you can access the register.php
page without using the /public/
in the URL like this:
http://localhost/auth/register.php
Code language: plaintext (plaintext)
Create the registration form
First, create a registration form in the register.php
file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://www.phptutorial.net/app/css/style.css">
<title>Register</title>
</head>
<body>
<main>
<form action="register.php" method="post">
<h1>Sign Up</h1>
<div>
<label for="username">Username:</label>
<input type="text" name="username" id="username">
</div>
<div>
<label for="email">Email:</label>
<input type="email" name="email" id="email">
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" id="password">
</div>
<div>
<label for="password2">Password Again:</label>
<input type="password" name="password2" id="password2">
</div>
<div>
<label for="agree">
<input type="checkbox" name="agree" id="agree" value="yes"/> I agree
with the
<a href="#" title="term of services">term of services</a>
</label>
</div>
<button type="submit">Register</button>
<footer>Already a member? <a href="login.php">Login here</a></footer>
</form>
</main>
</body>
</html>
Code language: HTML, XML (xml)
If you access the URL http://localhost/auth/register.php
, you’ll see a user registration form.
Make the registration form more organized
First, create the header.php
file in the src/inc
folder and copy the header section of the register.php
file to the header.php
file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://www.phptutorial.net/app/css/style.css">
<title>Register</title>
</head>
<body>
<main>
Code language: HTML, XML (xml)
Second, create the footer.php
file in the src/inc
folder and copy the footer section of the register.php
file to the footer.php
file:
</main>
</body>
</html>
Code language: HTML, XML (xml)
Third, include the header.php
and footer.php
files from the inc
folder in the register.php
file. Typically, you use the require
or include
construct.
However, you may want to include the header.php
and footer.php
files in other files, e.g., login.php
. Therefore, the title of the page should not be fixed like this:
<title>Register</title>
Code language: HTML, XML (xml)
To make the <title>
tag dynamic, you can create a helpers.php
file in the src/libs
folder and define the view()
function that loads the code from a PHP file and passes data to it:
function view(string $filename, array $data = []): void
{
// create variables from the associative array
foreach ($data as $key => $value) {
$$key = $value;
}
require_once __DIR__ . '/../inc/' . $filename . '.php';
}
Code language: PHP (php)
This view()
function loads the code from a file without the need of specifying the .php
file extension.
The view()
function allows you to pass data to the included file as an associative array. In the included file, you can use the keys of elements as the variable names and the values as the variable values. Check out the variable variables for more detail.
For example, the following uses the view()
function to load the code from the header.php
file and makes the title
as a variable in the header.php
file:
<?php view('header', ['title' => 'Register']) ?>
Code language: PHP (php)
Since the header.php
file accept the title
as a variable, you need to update it as follows:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://www.phptutorial.net/app/css/style.css">
<title><?= $title ?? 'Home' ?></title>
</head>
<body>
<main>
Code language: PHP (php)
In this new header.php
file, the title
variable will default to Home
if it is not set.
To use the view()
function in the register.php
, the register.php
must include the helpers.php
.
To centrally include the files, you can create a bootstrap.php
file in the src
folder. The bootstrap.php
will include all the necessary files. And you include the bootstrap.php
file in the register.php
file.
The boostrap.php
file will be like this:
<?php
require_once __DIR__ . '/libs/helpers.php';
Code language: PHP (php)
And the register.php
file will look like the following:
<?php
require __DIR__ . '/../src/bootstrap.php';
?>
<?php view('header', ['title' => 'Register']) ?>
<form action="register.php" method="post">
...
</form>
<?php view('footer') ?>
Code language: PHP (php)
Process the registration form submission
The registration form submits to register.php
using the HTTP POST method. To process the form data, you can check if the HTTP request is POST at the beginning of the register.php
file like this:
if(strtoupper($_SERVER['REQUEST_METHOD']) === 'POST') {
// process the form
}
Code language: PHP (php)
Since the above code can be used on other pages, you can define the is_post_request()
function in the helpers.php
file to encapsulate it:
function is_post_request(): bool
{
return strtoupper($_SERVER['REQUEST_METHOD']) === 'POST';
}
Code language: PHP (php)
Similarly, you can also define the is_get_request()
function that returns true
if the current HTTP request is GET:
function is_get_request(): bool
{
return strtoupper($_SERVER['REQUEST_METHOD']) === 'GET';
}
Code language: PHP (php)
And at the beginning of the register.php
file, you can use the is_post_request()
function as follows:
<?php
require __DIR__ . '/../src/bootstrap.php';
if (is_post_request()) {
//...
}
Code language: PHP (php)
Sanitize & validate user inputs
To sanitize & validate user inputs, you can use the sanitize()
and valdiate()
functions developed in the sanitizing and validating input tutorial or you can use the filter()
function.
To use these functions, you need to:
- First, create the
sanitization.php
,validation.php
, andfilter.php
files in thesrc/libs
folder. - Second, add the code to these files (please look at the code in the end of this tutorial).
- Third, include these files in the
bootstrap.php
file.
The bootstrap.php
file will look like the following:
<?php
session_start();
require_once __DIR__ . '/libs/helpers.php';
require_once __DIR__ . '/libs/sanitization.php';
require_once __DIR__ . '/libs/validation.php';
require_once __DIR__ . '/libs/filter.php';
Code language: PHP (php)
The following shows how to use the filter()
function to sanitize and validate the user inputs:
$fields = [
'username' => 'string | required | alphanumeric | between: 3, 25 | unique: users, username',
'email' => 'email | required | email | unique: users, email',
'password' => 'string | required | secure',
'password2' => 'string | required | same: password',
'agree' => 'string | required'
];
// custom messages
$messages = [
'password2' => [
'required' => 'Please enter the password again',
'same' => 'The password does not match'
],
'agree' => [
'required' => 'You need to agree to the term of services to register'
]
];
[$inputs, $errors] = filter($_POST, $fields, $messages);
Code language: PHP (php)
In the filter()
function, you pass three arguments:
- The
$_POST
array stores the user inputs. - The
$fields
associative array stores the rules of all fields. - The
$messages
is a multidimensional array that specifies the custom messages for the the required and same rules of thepassword2
andagree
fields. This argument is optional. If you skip it, thefilter()
function will use default validation messages.
If the form is invalid, you need to redirect the users to the register.php
page using the post-redirect-get (PRG) technique. Also, you need to add the $inputs
and $errors
arrays to the $_SESSION
variable so that you can access them in the GET
request after redirection.
If the form is valid, you need to create a user account and redirect the users to the login.php
page using the PRG technique. To show a message once across pages, you can use the session-based flash messages.
Manage flash messages
To manage flash messages, you use the flash()
function defined in the flash message tutorial:
- First, create a new file
flash.php
in thesrc/libs
folder. - Second, add the code to the
flash.php
file. - Third, include the
flash.php
file in thebootstrap.php
.
To create a flash message in the register.php
file, you call the flash()
function:
flash(
'user_register_success',
'Your account has been created successfully. Please login here.',
'success'
);
Code language: PHP (php)
To show all the flash messages, you call the flash()
function without passing any arguments:
flash()
Code language: PHP (php)
For example, you can show the flash messages in the header.php
file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://www.phptutorial.net/app/css/style.css">
<title><?= $title ?? 'Home' ?></title>
</head>
<body>
<main>
<?php flash() ?>
Code language: PHP (php)
Set the error CSS class
When a form field has invalid data, e.g., wrong email address format, you need to highlight it by adding an error
CSS class.
The following defines the error_class()
function in the src/libs/helpers.php
file, which returns the 'error'
class if the $errors
array has an error associated with a field:
function error_class(array $errors, string $field): string
{
return isset($errors[$field]) ? 'error' : '';
}
Code language: PHP (php)
Redirect
After users register for accounts successfully, you need to redirect them to the login page. To do that, you can use the header()
function with the exit
construct:
header ('Location: login.php');
exit;
Code language: PHP (php)
The following defines a function called redirect_to()
function in the helpers.php
file to wrap the above code:
function redirect_to(string $url): void
{
header('Location:' . $url);
exit;
}
Code language: PHP (php)
If the form data is invalid, you can redirect users back to the register.php
page. Before doing it, you need to add the $inputs
and $errors
variables to the $_SESSION
variable so that you can access them in the subsequent request.
The following defines the redirect_with()
function that adds the elements of the $items
array to the $_SESSION
variable and redirects to a URL:
function redirect_with(string $url, array $items): void
{
foreach ($items as $key => $value) {
$_SESSION[$key] = $value;
}
redirect_to($url);
}
Code language: PHP (php)
Notice that the redirect_with()
function calls the redirect_to()
function to redirect users to a URL.
The following shows how to use the redirect_with()
function that adds the $inputs
and $errors
array and redirect to the register.php
page:
redirect_with('register.php', [
'inputs' => $inputs,
'errors' => $errors
]);
Code language: PHP (php)
If you want to set a flash message and redirect to another page, you can define a new helper function redirect_with_message()
like this:
function redirect_with_message(string $url, string $message, string $type=FLASH_SUCCESS)
{
flash('flash_' . uniqid(), $message, $type);
redirect_to($url);
}
Code language: PHP (php)
Note that the flash message’s name starts with the flash_
and is followed by a unique id returned by the uniqid()
function. It’ll look like this:
flash_615481fce49e8
We use a generated name for the flash message because it’s not important in this case. And we’ll display all the flash messages anyway.
For example, you can redirect users to the login page with a success message like this:
redirect_with_message(
'login.php',
'Your account has been created successfully. Please login here.'
);
Code language: JavaScript (javascript)
Flash session data
To get data from the $_SESSION
and remove it immediately, you can define a helper function session_flash()
:
function session_flash(...$keys): array
{
$data = [];
foreach ($keys as $key) {
if (isset($_SESSION[$key])) {
$data[] = $_SESSION[$key];
unset($_SESSION[$key]);
} else {
$data[] = [];
}
}
return $data;
}
Code language: PHP (php)
The session_flash()
function accepts a variable number of keys. It’s also known as a variadic function.
If a key exists in the $_SESSION
variable, the function adds the value of that key to the return array and unsets the value. Otherwise, the function will return an empty array for that key.
The following uses the array destructuring to get the $inputs
and $errors
from the $_SESSION
variable and unset them using the session_flash()
function:
[$errors, $inputs] = session_flash('errors', 'inputs');
Code language: PHP (php)
The boostrap.php
function is updated to include the call to session_start()
function and the flash.php
file:
<?php
session_start();
require_once __DIR__ . '/libs/helpers.php';
require_once __DIR__ . '/libs/flash.php';
require_once __DIR__ . '/libs/sanitization.php';
require_once __DIR__ . '/libs/validation.php';
require_once __DIR__ . '/libs/filter.php';
Code language: HTML, XML (xml)
Note that you need to call the session_start()
function to start a new session or resume an existing one to manage the session data.
The complete register.php file
The following shows the complete register.php
file:
<?php
require __DIR__ . '/../src/bootstrap.php';
$errors = [];
$inputs = [];
if (is_post_request()) {
$fields = [
'username' => 'string | required | alphanumeric | between: 3, 25 | unique: users, username',
'email' => 'email | required | email | unique: users, email',
'password' => 'string | required | secure',
'password2' => 'string | required | same: password',
'agree' => 'string | required'
];
// custom messages
$messages = [
'password2' => [
'required' => 'Please enter the password again',
'same' => 'The password does not match'
],
'agree' => [
'required' => 'You need to agree to the term of services to register'
]
];
[$inputs, $errors] = filter($_POST, $fields, $messages);
if ($errors) {
redirect_with('register.php', [
'inputs' => $inputs,
'errors' => $errors
]);
}
if (register_user($inputs['email'], $inputs['username'], $inputs['password'])) {
redirect_with_message(
'login.php',
'Your account has been created successfully. Please login here.'
);
}
} else if (is_get_request()) {
[$inputs, $errors] = session_flash('inputs', 'errors');
}
?>
<?php view('header', ['title' => 'Register']) ?>
<form action="register.php" method="post">
<h1>Sign Up</h1>
<div>
<label for="username">Username:</label>
<input type="text" name="username" id="username" value="<?= $inputs['username'] ?? '' ?>"
class="<?= error_class($errors, 'username') ?>">
<small><?= $errors['username'] ?? '' ?></small>
</div>
<div>
<label for="email">Email:</label>
<input type="email" name="email" id="email" value="<?= $inputs['email'] ?? '' ?>"
class="<?= error_class($errors, 'email') ?>">
<small><?= $errors['email'] ?? '' ?></small>
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" id="password" value="<?= $inputs['password'] ?? '' ?>"
class="<?= error_class($errors, 'password') ?>">
<small><?= $errors['password'] ?? '' ?></small>
</div>
<div>
<label for="password2">Password Again:</label>
<input type="password" name="password2" id="password2" value="<?= $inputs['password2'] ?? '' ?>"
class="<?= error_class($errors, 'password2') ?>">
<small><?= $errors['password2'] ?? '' ?></small>
</div>
<div>
<label for="agree">
<input type="checkbox" name="agree" id="agree" value="checked" <?= $inputs['agree'] ?? '' ?> /> I
agree
with the
<a href="#" title="term of services">term of services</a>
</label>
<small><?= $errors['agree'] ?? '' ?></small>
</div>
<button type="submit">Register</button>
<footer>Already a member? <a href="login.php">Login here</a></footer>
</form>
<?php view('footer') ?>
Code language: PHP (php)
To separate the logic and view parts, you can create a new file register.php
in the src
folder and add the first part of the public/register.php
file to it:
<?php
$errors = [];
$inputs = [];
if (is_post_request()) {
$fields = [
'username' => 'string | required | alphanumeric | between: 3, 25 | unique: users, username',
'email' => 'email | required | email | unique: users, email',
'password' => 'string | required | secure',
'password2' => 'string | required | same: password',
'agree' => 'string | required'
];
// custom messages
$messages = [
'password2' => [
'required' => 'Please enter the password again',
'same' => 'The password does not match'
],
'agree' => [
'required' => 'You need to agree to the term of services to register'
]
];
[$inputs, $errors] = filter($_POST, $fields, $messages);
if ($errors) {
redirect_with('register.php', [
'inputs' => $inputs,
'errors' => $errors
]);
}
if (register_user($inputs['email'], $inputs['username'], $inputs['password'])) {
redirect_with_message(
'login.php',
'Your account has been created successfully. Please login here.'
);
}
} else if (is_get_request()) {
[$inputs, $errors] = session_flash('inputs', 'errors');
}
Code language: HTML, XML (xml)
And the following shows the public/register.php
file:
<?php
require __DIR__ . '/../src/bootstrap.php';
require __DIR__ . '/../src/register.php';
?>
<?php view('header', ['title' => 'Register']) ?>
<form action="register.php" method="post">
<h1>Sign Up</h1>
<div>
<label for="username">Username:</label>
<input type="text" name="username" id="username" value="<?= $inputs['username'] ?? '' ?>"
class="<?= error_class($errors, 'username') ?>">
<small><?= $errors['username'] ?? '' ?></small>
</div>
<div>
<label for="email">Email:</label>
<input type="email" name="email" id="email" value="<?= $inputs['email'] ?? '' ?>"
class="<?= error_class($errors, 'email') ?>">
<small><?= $errors['email'] ?? '' ?></small>
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" id="password" value="<?= $inputs['password'] ?? '' ?>"
class="<?= error_class($errors, 'password') ?>">
<small><?= $errors['password'] ?? '' ?></small>
</div>
<div>
<label for="password2">Password Again:</label>
<input type="password" name="password2" id="password2" value="<?= $inputs['password2'] ?? '' ?>"
class="<?= error_class($errors, 'password2') ?>">
<small><?= $errors['password2'] ?? '' ?></small>
</div>
<div>
<label for="agree">
<input type="checkbox" name="agree" id="agree" value="checked" <?= $inputs['agree'] ?? '' ?> /> I
agree
with the
<a href="#" title="term of services">term of services</a>
</label>
<small><?= $errors['agree'] ?? '' ?></small>
</div>
<button type="submit">Register</button>
<footer>Already a member? <a href="login.php">Login here</a></footer>
</form>
<?php view('footer') ?>
Code language: JavaScript (javascript)
Since the register_user()
function doesn’t exist, you need to define it. Before doing that, you need to create a new database and the users
table.
Create a new database and the users table
We’ll use MySQL to store the user information. To interact with MySQL, you can use any MySQL client tool such as phpmyadmin or mysql.
First, create a new database called auth
in the MySQL database server:
CREATE DATABASE auth;
Code language: SQL (Structured Query Language) (sql)
Second, create a new table called users
to store the user information:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(25) NOT NULL UNIQUE,
email VARCHAR(320) NOT NULL UNIQUE,
password VARCHAR(256) NOT NULL,
is_admin TINYINT(1) not null default 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Code language: SQL (Structured Query Language) (sql)
The users
table has the following columns:
id
is the primary key. Since the id is an auto-increment column, MySQL will increase its value by one for each new row. In other words, you don’t need to provide the id when inserting a new row into the table.username
is a varying character column with theNOT NULL
andUNIQUE
constraints. It means that there will be no two rows with the same username.email
column is like theusername
column, which isNOT NULL
andUNIQUE
.password
column a varying column and not null.is_admin
is a tiny integer column. Its default value is zero. Ifis_admin
is zero, the user is not the admin. If theis_admin
is 1, the user is an admin which has more privileges than regular users.created_at
is a timestamp column that MySQL will update it to the current timestamp when you insert a new row into the table.updated_at
is a datetime column that MySQL will update it to the current timestamp automatically when you update an existing row.
Connect to the MySQL database
To connect to the MySQL database, you’ll use the PHP data object (or PDO) library.
First, create a new file database.php
file in the config
folder and add the following database parameters to the file:
<?php
const DB_HOST = 'localhost';
const DB_NAME = 'auth';
const DB_USER = 'root';
const DB_PASSWORD = '';
Code language: PHP (php)
Second, create the connection.php
file in the src/libs
folder.
Third, define the db()
function that connects the database once and returns a new PDO object. The db()
function uses the database configuration defined in the config/database.php
file.
function db(): PDO
{
static $pdo;
if (!$pdo) {
$pdo = new PDO(
sprintf("mysql:host=%s;dbname=%s;charset=UTF8", DB_HOST, DB_NAME),
DB_USER,
DB_PASSWORD,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
return $pdo;
}
Code language: PHP (php)
In the db()
function, the $pdo
is a static variable. When you call the db()
function for the first time, the $pdo
variable is not initialized. Therefore, the code block inside the if
statement executes that connects to the database and returns a new PDO object.
Since the $pdo
is a static variable, it’s still alive after the function db()
function completes. Therefore, when you call the db()
function again, it returns the PDO object. In other words, the function doesn’t connect to the database again.
Since creating a connection to the database is expensive in terms of time and resources, you should connect to the database once per request.
Define the register_user() function
First, create a new file called auth.php
in the src
folder.
Second, define the register_user()
function that inserts a new user into the users
table:
function register_user(string $email, string $username, string $password, bool $is_admin = false): bool
{
$sql = 'INSERT INTO users(username, email, password, is_admin)
VALUES(:username, :email, :password, :is_admin)';
$statement = db()->prepare($sql);
$statement->bindValue(':username', $username, PDO::PARAM_STR);
$statement->bindValue(':email', $email, PDO::PARAM_STR);
$statement->bindValue(':password', password_hash($password, PASSWORD_BCRYPT), PDO::PARAM_STR);
$statement->bindValue(':is_admin', (int)$is_admin, PDO::PARAM_INT);
return $statement->execute();
}
Code language: PHP (php)
When storing a password in the database, you never store it in plain text for security reasons. Instead, you should always hash the password before storing it.
To hash a password, you use the built-in password_hash()
function:
password_hash($password, PASSWORD_BCRYPT)
Code language: PHP (php)
For example, if the password is Password1
, the password_hash()
function returns the following hash:
<?php
echo password_hash('Password1', PASSWORD_BCRYPT);
Code language: PHP (php)
Output:
$2y$10$QlUdCEXY68bswdVsKlE.5OjHa7X8fvtCmlYLnIkfvbcGd..mqDfwq
Code language: plaintext (plaintext)
The PASSWORD_BCRYPT
argument instructs the password_hash()
function to use the CRYPT_BLOWFISH
algorithm which is very secure.
Since password_hash()
is a one-way function, hackers cannot “decrypt” it to the original plain text (Password1
). This provides a defense against the passwords being compromised when a database is leaked.
Put it all together
The project folder and file structure will be like the following:
├── config
| └── database.php
├── public
| └── register.php
└── src
├── auth.php
├── bootstrap.php
├── inc
| ├── footer.php
| └── header.php
├── libs
| ├── connection.php
| ├── flash.php
| ├── helpers.php
| ├── sanitization.php
| ├── validation.php
| └── filter.php
└── register.php
Code language: PHP (php)
config/database.php file
<?php
const DB_HOST = 'localhost';
const DB_NAME = 'auth';
const DB_USER = 'root';
const DB_PASSWORD = '';
Code language: PHP (php)
inc/auth.php file
/**
* Register a user
*
* @param string $email
* @param string $username
* @param string $password
* @param bool $is_admin
* @return bool
*/
function register_user(string $email, string $username, string $password, bool $is_admin = false): bool
{
$sql = 'INSERT INTO users(username, email, password, is_admin)
VALUES(:username, :email, :password, :is_admin)';
$statement = db()->prepare($sql);
$statement->bindValue(':username', $username, PDO::PARAM_STR);
$statement->bindValue(':email', $email, PDO::PARAM_STR);
$statement->bindValue(':password', password_hash($password, PASSWORD_BCRYPT), PDO::PARAM_STR);
$statement->bindValue(':is_admin', (int)$is_admin, PDO::PARAM_INT);
return $statement->execute();
}
Code language: PHP (php)
src/libs/connection.php
<?php
/**
* Connect to the database and returns an instance of PDO class
* or false if the connection fails
*
* @return PDO
*/
function db(): PDO
{
static $pdo;
if (!$pdo) {
$pdo = new PDO(
sprintf("mysql:host=%s;dbname=%s;charset=UTF8", DB_HOST, DB_NAME),
DB_USER,
DB_PASSWORD,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
return $pdo;
}
Code language: PHP (php)
src/libs/flash.php file
<?php
const FLASH = 'FLASH_MESSAGES';
const FLASH_ERROR = 'error';
const FLASH_WARNING = 'warning';
const FLASH_INFO = 'info';
const FLASH_SUCCESS = 'success';
/**
* Create a flash message
*
* @param string $name
* @param string $message
* @param string $type
* @return void
*/
function create_flash_message(string $name, string $message, string $type): void
{
// remove existing message with the name
if (isset($_SESSION[FLASH][$name])) {
unset($_SESSION[FLASH][$name]);
}
// add the message to the session
$_SESSION[FLASH][$name] = ['message' => $message, 'type' => $type];
}
/**
* Format a flash message
*
* @param array $flash_message
* @return string
*/
function format_flash_message(array $flash_message): string
{
return sprintf('<div class="alert alert-%s">%s</div>',
$flash_message['type'],
$flash_message['message']
);
}
/**
* Display a flash message
*
* @param string $name
* @return void
*/
function display_flash_message(string $name): void
{
if (!isset($_SESSION[FLASH][$name])) {
return;
}
// get message from the session
$flash_message = $_SESSION[FLASH][$name];
// delete the flash message
unset($_SESSION[FLASH][$name]);
// display the flash message
echo format_flash_message($flash_message);
}
/**
* Display all flash messages
*
* @return void
*/
function display_all_flash_messages(): void
{
if (!isset($_SESSION[FLASH])) {
return;
}
// get flash messages
$flash_messages = $_SESSION[FLASH];
// remove all the flash messages
unset($_SESSION[FLASH]);
// show all flash messages
foreach ($flash_messages as $flash_message) {
echo format_flash_message($flash_message);
}
}
/**
* Flash a message
*
* @param string $name
* @param string $message
* @param string $type (error, warning, info, success)
* @return void
*/
function flash(string $name = '', string $message = '', string $type = ''): void
{
if ($name !== '' && $message !== '' && $type !== '') {
// create a flash message
create_flash_message($name, $message, $type);
} elseif ($name !== '' && $message === '' && $type === '') {
// display a flash message
display_flash_message($name);
} elseif ($name === '' && $message === '' && $type === '') {
// display all flash message
display_all_flash_messages();
}
}
Code language: PHP (php)
src/inc/header.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://www.phptutorial.net/app/css/style.css">
<title><?= $title ?? 'Home' ?></title>
</head>
<body>
<main>
<?php flash() ?>
Code language: PHP (php)
src/inc/footer.php file
</main>
</body>
</html>
Code language: PHP (php)
src/libs/helpers.php
<?php
/**
* Display a view
*
* @param string $filename
* @param array $data
* @return void
*/
function view(string $filename, array $data = []): void
{
// create variables from the associative array
foreach ($data as $key => $value) {
$$key = $value;
}
require_once __DIR__ . '/../inc/' . $filename . '.php';
}
/**
* Return the error class if error is found in the array $errors
*
* @param array $errors
* @param string $field
* @return string
*/
function error_class(array $errors, string $field): string
{
return isset($errors[$field]) ? 'error' : '';
}
/**
* Return true if the request method is POST
*
* @return boolean
*/
function is_post_request(): bool
{
return strtoupper($_SERVER['REQUEST_METHOD']) === 'POST';
}
/**
* Return true if the request method is GET
*
* @return boolean
*/
function is_get_request(): bool
{
return strtoupper($_SERVER['REQUEST_METHOD']) === 'GET';
}
/**
* Redirect to another URL
*
* @param string $url
* @return void
*/
function redirect_to(string $url): void
{
header('Location:' . $url);
exit;
}
/**
* Redirect to a URL with data stored in the items array
* @param string $url
* @param array $items
*/
function redirect_with(string $url, array $items): void
{
foreach ($items as $key => $value) {
$_SESSION[$key] = $value;
}
redirect_to($url);
}
/**
* Redirect to a URL with a flash message
* @param string $url
* @param string $message
* @param string $type
*/
function redirect_with_message(string $url, string $message, string $type = FLASH_SUCCESS)
{
flash('flash_' . uniqid(), $message, $type);
redirect_to($url);
}
/**
* Flash data specified by $keys from the $_SESSION
* @param ...$keys
* @return array
*/
function session_flash(...$keys): array
{
$data = [];
foreach ($keys as $key) {
if (isset($_SESSION[$key])) {
$data[] = $_SESSION[$key];
unset($_SESSION[$key]);
} else {
$data[] = [];
}
}
return $data;
}
Code language: PHP (php)
inc/sanitization.php
<?php
const FILTERS = [
'string' => FILTER_SANITIZE_STRING,
'string[]' => [
'filter' => FILTER_SANITIZE_STRING,
'flags' => FILTER_REQUIRE_ARRAY
],
'email' => FILTER_SANITIZE_EMAIL,
'int' => [
'filter' => FILTER_SANITIZE_NUMBER_INT,
'flags' => FILTER_REQUIRE_SCALAR
],
'int[]' => [
'filter' => FILTER_SANITIZE_NUMBER_INT,
'flags' => FILTER_REQUIRE_ARRAY
],
'float' => [
'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
'flags' => FILTER_FLAG_ALLOW_FRACTION
],
'float[]' => [
'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
'flags' => FILTER_REQUIRE_ARRAY
],
'url' => FILTER_SANITIZE_URL,
];
/**
* Recursively trim strings in an array
* @param array $items
* @return array
*/
function array_trim(array $items): array
{
return array_map(function ($item) {
if (is_string($item)) {
return trim($item);
} elseif (is_array($item)) {
return array_trim($item);
} else
return $item;
}, $items);
}
/**
* Sanitize the inputs based on the rules an optionally trim the string
* @param array $inputs
* @param array $fields
* @param int $default_filter FILTER_SANITIZE_STRING
* @param array $filters FILTERS
* @param bool $trim
* @return array
*/
function sanitize(array $inputs, array $fields = [], int $default_filter = FILTER_SANITIZE_STRING, array $filters = FILTERS, bool $trim = true): array
{
if ($fields) {
$options = array_map(fn($field) => $filters[$field], $fields);
$data = filter_var_array($inputs, $options);
} else {
$data = filter_var_array($inputs, $default_filter);
}
return $trim ? array_trim($data) : $data;
}
Code language: PHP (php)
src/libs/validation.php
<?php
const DEFAULT_VALIDATION_ERRORS = [
'required' => 'The %s is required',
'email' => 'The %s is not a valid email address',
'min' => 'The %s must have at least %s characters',
'max' => 'The %s must have at most %s characters',
'between' => 'The %s must have between %d and %d characters',
'same' => 'The %s must match with %s',
'alphanumeric' => 'The %s should have only letters and numbers',
'secure' => 'The %s must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter and one special character',
'unique' => 'The %s already exists',
];
/**
* Validate
* @param array $data
* @param array $fields
* @param array $messages
* @return array
*/
function validate(array $data, array $fields, array $messages = []): array
{
// Split the array by a separator, trim each element
// and return the array
$split = fn($str, $separator) => array_map('trim', explode($separator, $str));
// get the message rules
$rule_messages = array_filter($messages, fn($message) => is_string($message));
// overwrite the default message
$validation_errors = array_merge(DEFAULT_VALIDATION_ERRORS, $rule_messages);
$errors = [];
foreach ($fields as $field => $option) {
$rules = $split($option, '|');
foreach ($rules as $rule) {
// get rule name params
$params = [];
// if the rule has parameters e.g., min: 1
if (strpos($rule, ':')) {
[$rule_name, $param_str] = $split($rule, ':');
$params = $split($param_str, ',');
} else {
$rule_name = trim($rule);
}
// by convention, the callback should be is_<rule> e.g.,is_required
$fn = 'is_' . $rule_name;
if (is_callable($fn)) {
$pass = $fn($data, $field, ...$params);
if (!$pass) {
// get the error message for a specific field and rule if exists
// otherwise get the error message from the $validation_errors
$errors[$field] = sprintf(
$messages[$field][$rule_name] ?? $validation_errors[$rule_name],
$field,
...$params
);
}
}
}
}
return $errors;
}
/**
* Return true if a string is not empty
* @param array $data
* @param string $field
* @return bool
*/
function is_required(array $data, string $field): bool
{
return isset($data[$field]) && trim($data[$field]) !== '';
}
/**
* Return true if the value is a valid email
* @param array $data
* @param string $field
* @return bool
*/
function is_email(array $data, string $field): bool
{
if (empty($data[$field])) {
return true;
}
return filter_var($data[$field], FILTER_VALIDATE_EMAIL);
}
/**
* Return true if a string has at least min length
* @param array $data
* @param string $field
* @param int $min
* @return bool
*/
function is_min(array $data, string $field, int $min): bool
{
if (!isset($data[$field])) {
return true;
}
return mb_strlen($data[$field]) >= $min;
}
/**
* Return true if a string cannot exceed max length
* @param array $data
* @param string $field
* @param int $max
* @return bool
*/
function is_max(array $data, string $field, int $max): bool
{
if (!isset($data[$field])) {
return true;
}
return mb_strlen($data[$field]) <= $max;
}
/**
* @param array $data
* @param string $field
* @param int $min
* @param int $max
* @return bool
*/
function is_between(array $data, string $field, int $min, int $max): bool
{
if (!isset($data[$field])) {
return true;
}
$len = mb_strlen($data[$field]);
return $len >= $min && $len <= $max;
}
/**
* Return true if a string equals the other
* @param array $data
* @param string $field
* @param string $other
* @return bool
*/
function is_same(array $data, string $field, string $other): bool
{
if (isset($data[$field], $data[$other])) {
return $data[$field] === $data[$other];
}
if (!isset($data[$field]) && !isset($data[$other])) {
return true;
}
return false;
}
/**
* Return true if a string is alphanumeric
* @param array $data
* @param string $field
* @return bool
*/
function is_alphanumeric(array $data, string $field): bool
{
if (!isset($data[$field])) {
return true;
}
return ctype_alnum($data[$field]);
}
/**
* Return true if a password is secure
* @param array $data
* @param string $field
* @return bool
*/
function is_secure(array $data, string $field): bool
{
if (!isset($data[$field])) {
return false;
}
$pattern = "#.*^(?=.{8,64})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$#";
return preg_match($pattern, $data[$field]);
}
/**
* Return true if the $value is unique in the column of a table
* @param array $data
* @param string $field
* @param string $table
* @param string $column
* @return bool
*/
function is_unique(array $data, string $field, string $table, string $column): bool
{
if (!isset($data[$field])) {
return true;
}
$sql = "SELECT $column FROM $table WHERE $column = :value";
$stmt = db()->prepare($sql);
$stmt->bindValue(":value", $data[$field]);
$stmt->execute();
return $stmt->fetchColumn() === false;
}
Code language: PHP (php)
src/libs/filter.php
<?php
/**
* Sanitize and validate data
* @param array $data
* @param array $fields
* @param array $messages
* @return array
*/
function filter(array $data, array $fields, array $messages = []): array
{
$sanitization = [];
$validation = [];
// extract sanitization & validation rules
foreach ($fields as $field => $rules) {
if (strpos($rules, '|')) {
[$sanitization[$field], $validation[$field]] = explode('|', $rules, 2);
} else {
$sanitization[$field] = $rules;
}
}
$inputs = sanitize($data, $sanitization);
$errors = validate($inputs, $validation, $messages);
return [$inputs, $errors];
}
Code language: HTML, XML (xml)
src/bootstrap.php
<?php
<?php
session_start();
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/libs/helpers.php';
require_once __DIR__ . '/libs/flash.php';
require_once __DIR__ . '/libs/sanitization.php';
require_once __DIR__ . '/libs/validation.php';
require_once __DIR__ . '/libs/filter.php';
require_once __DIR__ . '/libs/connection.php';
require_once __DIR__ . '/auth.php';
Code language: PHP (php)
public/register.php
<?php
require __DIR__ . '/../src/bootstrap.php';
require __DIR__ . '/../src/register.php';
?>
<?php view('header', ['title' => 'Register']) ?>
<form action="register.php" method="post">
<h1>Sign Up</h1>
<div>
<label for="username">Username:</label>
<input type="text" name="username" id="username" value="<?= $inputs['username'] ?? '' ?>"
class="<?= error_class($errors, 'username') ?>">
<small><?= $errors['username'] ?? '' ?></small>
</div>
<div>
<label for="email">Email:</label>
<input type="email" name="email" id="email" value="<?= $inputs['email'] ?? '' ?>"
class="<?= error_class($errors, 'email') ?>">
<small><?= $errors['email'] ?? '' ?></small>
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" id="password" value="<?= $inputs['password'] ?? '' ?>"
class="<?= error_class($errors, 'password') ?>">
<small><?= $errors['password'] ?? '' ?></small>
</div>
<div>
<label for="password2">Password Again:</label>
<input type="password" name="password2" id="password2" value="<?= $inputs['password2'] ?? '' ?>"
class="<?= error_class($errors, 'password2') ?>">
<small><?= $errors['password2'] ?? '' ?></small>
</div>
<div>
<label for="agree">
<input type="checkbox" name="agree" id="agree" value="checked" <?= $inputs['agree'] ?? '' ?> /> I
agree
with the
<a href="#" title="term of services">term of services</a>
</label>
<small><?= $errors['agree'] ?? '' ?></small>
</div>
<button type="submit">Register</button>
<footer>Already a member? <a href="login.php">Login here</a></footer>
</form>
<?php view('footer') ?>
Code language: PHP (php)
src/register.php
<?php
$errors = [];
$inputs = [];
if (is_post_request()) {
$fields = [
'username' => 'string | required | alphanumeric | between: 3, 25 | unique: users, username',
'email' => 'email | required | email | unique: users, email',
'password' => 'string | required | secure',
'password2' => 'string | required | same: password',
'agree' => 'string | required'
];
// custom messages
$messages = [
'password2' => [
'required' => 'Please enter the password again',
'same' => 'The password does not match'
],
'agree' => [
'required' => 'You need to agree to the term of services to register'
]
];
[$inputs, $errors] = filter($_POST, $fields, $messages);
if ($errors) {
redirect_with('register.php', [
'inputs' => $inputs,
'errors' => $errors
]);
}
if (register_user($inputs['email'], $inputs['username'], $inputs['password'])) {
redirect_with_message(
'login.php',
'Your account has been created successfully. Please login here.'
);
}
} else if (is_get_request()) {
[$inputs, $errors] = session_flash('inputs', 'errors');
}
Code language: PHP (php)
public/login.php
<?php
require __DIR__ . '/../src/bootstrap.php';
Code language: HTML, XML (xml)
The login.php
will be blank. And you’ll learn how to create the login form in the next tutorial.