PHP Registration Form

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
  • Email
  • Password
  • Password confirmation
  • Agreement checkbox
  • Register button
PHP Registration Form

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 the login.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
   └── libsCode language: plaintext (plaintext)

The following describes the purpose of each folder:

FolderPurpose
configStore the configuration file such as database configuration
publicStore the public files accessed directly by the users
srcStore the source files that should not be exposed to the public
src/incStore the commonly included files such as the header and footer of a page
src/libsStore 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.phpCode 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.phpCode 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:</code> more dynamic, you can define a new function called <code>view()</code> that loads the code from a file and passes data to it:</p>

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, and filter.php files in the src/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 the password2 and agree fields. This argument is optional. If you skip it, the filter() 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 the src/libs folder.
  • Second, add the code to the flash.php file.
  • Third, include the flash.php file in the bootstrap.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 the NOT NULL and UNIQUE constraints. It means that there will be no two rows with the same username.
  • email column is like the username column, which is NOT NULL and UNIQUE.
  • password column a varying column and not null.
  • is_admin is a tiny integer column. Its default value is zero. If is_admin is zero, the user is not the admin. If the is_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..mqDfwqCode 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.phpCode 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.

Did you find this tutorial useful?