Skip to content

Lab: User Registration System (Part 1)

In this lab, you’ll implement a secure user registration system that allows users to create accounts. You’ll build the database schema, a UserModel for data operations, validation logic, and a registration form with proper security measures including password hashing.

Part 2 (Authentication & Authorization) covers login, sessions, middleware, and role-based access control.


  • Create a user database table with proper schema design
  • Build a UserModel with CRUD operations
  • Implement secure password hashing using password_hash()
  • Validate user input (required fields, email format, uniqueness, password strength)
  • Create registration forms with proper HTML structure
  • Build an AuthController extending BaseController
  • Apply the Result pattern for validation feedback
  • Prevent common security vulnerabilities (SQL injection, XSS)

  • Working Slim 4 application with BaseController pattern
  • Flash Messages lab completed (FlashMessage helper)
  • MySQL database connection using PDOService
  • BaseModel class available
  • Bootstrap CSS included in views
  • Access to phpMyAdmin for database management

Objective: Set up the database schema for storing user accounts.

Instructions:

  1. Open phpMyAdmin in your browser
  2. Select your project database
  3. Click on the “SQL” tab
  4. Execute the following SQL statement:
SQL Statement to Create the Users Table
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin', 'customer') DEFAULT 'customer',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
  1. Click “Go” to execute the query
  2. Verify the table was created by clicking on the “Structure” tab or run: DESCRIBE users;

Key Points:

  • UNIQUE constraints on email and username prevent duplicates
  • VARCHAR(255) for password_hash handles bcrypt/Argon2
  • ENUM restricts role values to ‘admin’ or ‘customer’

Create a UserModel class in the app/Domain/Models/ directory. This model will handle all database operations related to users.


Objective: Insert a new user with hashed password.

Why hash passwords? Never store plain text! If your database is compromised, hashed passwords protect users. Use password_hash() with bcrypt.

/**
* Create a new user account.
*
* @param array $data User data (first_name, last_name, username, email, password, role)
* @return int The ID of the newly created user
*/
public function createUser(array $data): int
{
// TODO: Hash the password using password_hash() with PASSWORD_BCRYPT
// Store the result in $hashedPassword variable
// TODO: Write an INSERT SQL query to insert a new user into the users table
// Insert: first_name, last_name, username, email, password_hash, role
// Use named parameters (e.g., :first_name, :last_name, etc.)
// TODO: Execute the query with appropriate parameters
// Use $hashedPassword for the password_hash field
// TODO: Return the last inserted ID
}

Hints:

  • password_hash($password, PASSWORD_BCRYPT) creates secure hash
  • Use named parameters in SQL for security
  • $this->execute() inherited from BaseModel
  • $this->lastInsertId() returns auto-increment ID

Objective: Find a user by email address.

/**
* Find a user by email address.
*
* @param string $email The email address to search for
* @return array|null User data array or null if not found
*/
public function findByEmail(string $email): ?array
{
// TODO: Write a SELECT SQL query to find a user by email
// Select all columns from the users table where email matches
// Use named parameter :email and LIMIT 1
// TODO: Execute the query and return the result
}

Hints:

  • Use LIMIT 1 for efficiency
  • $this->selectOne() returns array or null

Step 2.4: Implement findByUsername() Method

Section titled “Step 2.4: Implement findByUsername() Method”
/**
* Find a user by username.
*
* @param string $username The username to search for
* @return array|null User data array or null if not found
*/
public function findByUsername(string $username): ?array
{
// TODO: Write a SELECT SQL query to find a user by username
// Select all columns from the users table where username matches
// Use named parameter :username and LIMIT 1
// TODO: Execute the query and return the result
}

Step 2.5: Implement Existence Check Methods

Section titled “Step 2.5: Implement Existence Check Methods”

Objective: Check if email/username exists for validation during registration.

/**
* Check if an email address already exists in the database.
*
* @param string $email The email address to check
* @return bool True if email exists, false otherwise
*/
public function emailExists(string $email): bool
{
// TODO: Write a SELECT COUNT(*) query to count users with the given email
// Alias the count as 'count'
// Use named parameter :email
// TODO: Execute the query and return true if count > 0, false otherwise
}
/**
* Check if a username already exists in the database.
*
* @param string $username The username to check
* @return bool True if username exists, false otherwise
*/
public function usernameExists(string $username): bool
{
// TODO: Write a SELECT COUNT(*) query to count users with the given username
// Alias the count as 'count'
// Use named parameter :username
// TODO: Execute the query and return true if count > 0, false otherwise
}

Objective: Create an HTML form for user registration.

Create app/Views/auth/register.php:

app/Views/auth/register.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Authentication System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="text-center">Create Account</h3>
</div>
<div class="card-body">
<?= App\Helpers\FlashMessage::render() ?>
<form method="POST" action="register">
<div class="mb-3">
<label for="first_name" class="form-label">First Name</label>
<input type="text" class="form-control" id="first_name" name="first_name" required>
</div>
<div class="mb-3">
<label for="last_name" class="form-label">Last Name</label>
<input type="text" class="form-control" id="last_name" name="last_name" required>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
<div class="form-text">
Password must be at least 8 characters long and contain at least one number.
</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<input type="hidden" name="role" value="customer">
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>
<div class="mt-3 text-center">
<p>Already have an account? <a href="login">Login here</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Step 4: Create AuthController with Registration Logic

Section titled “Step 4: Create AuthController with Registration Logic”

Objective: Handle registration requests with validation.

Create app/Controllers/AuthController.php:

app/Controllers/AuthController.php
<?php
namespace App\Controllers;
use App\Domain\Models\UserModel;
use App\Helpers\FlashMessage;
use App\Helpers\SessionManager;
use DI\Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class AuthController extends BaseController
{
public function __construct(Container $container, private UserModel $userModel)
{
parent::__construct($container);
}
/**
* Display the registration form (GET request).
*/
public function register(Request $request, Response $response, array $args): Response
{
// TODO: Create a $data array with 'title' => 'Register'
// TODO: Render 'auth/register.php' view and pass $data
}
/**
* Process registration form submission (POST request).
*/
public function store(Request $request, Response $response, array $args): Response
{
// TODO: Get form data using getParsedBody()
// Store in $formData variable
// TODO: Extract individual fields from $formData:
// $firstName, $lastName, $username, $email, $password, $confirmPassword, $role
// Start validation
$errors = [];
// TODO: Validate required fields (first_name, last_name, username, email, password, confirm_password)
// If any field is empty, add error: "All fields are required."
// Hint: if (empty($firstName) || empty($lastName) || ...) { $errors[] = "..."; }
// TODO: Validate email format using filter_var()
// If invalid, add error: "Invalid email format."
// Hint: if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { ... }
// TODO: Check if email already exists using $this->userModel->emailExists($email)
// If exists, add error: "Email already registered."
// TODO: Check if username already exists using $this->userModel->usernameExists($username)
// If exists, add error: "Username already taken."
// TODO: Validate password length (minimum 8 characters)
// If too short, add error: "Password must be at least 8 characters long."
// TODO: Validate password contains at least one number
// If no number, add error: "Password must contain at least one number."
// Hint: if (!preg_match('/[0-9]/', $password)) { ... }
// TODO: Check if password matches confirm_password
// If not match, add error: "Passwords do not match."
// If validation errors exist, redirect back with error message
// TODO: Check if $errors array is not empty
// If errors exist:
// - Use FlashMessage::error() with the first error message
// - Redirect back to 'auth.register' route
// If validation passes, create the user
try {
// TODO: Create $userData array with keys:
// 'first_name', 'last_name', 'username', 'email', 'password', 'role'
// TODO: Call $this->userModel->createUser($userData)
// Store the returned user ID in $userId
// TODO: Display success message using FlashMessage::success()
// Message: "Registration successful! Please log in."
// TODO: Redirect to 'auth.login' route
} catch (\Exception $e) {
// TODO: Display error message using FlashMessage::error()
// Message: "Registration failed. Please try again."
// TODO: Redirect back to 'auth.register' route
}
}
}

Open app/Routes/web-routes.php and add:

use App\Controllers\AuthController;
// TODO: Create a GET route for '/register' that maps to AuthController::class 'register' method
// Set the route name to 'auth.register'
// TODO: Create a POST route for '/register' that maps to AuthController::class 'store' method

  1. Visit http://localhost/[your-app]/register
  2. Fill in all fields with valid data:
    • First Name: John
    • Last Name: Doe
    • Username: johndoe
    • Email: john@example.com
    • Password: password123
    • Confirm Password: password123
  3. Click “Register”
  4. Expected: Green success message and redirect to login page

Test each validation rule:

a) Empty fields:

  • Leave one or more fields empty
  • Expected: “All fields are required.”

b) Invalid email format:

  • Enter invalid email (e.g., “notanemail”)
  • Expected: “Invalid email format.”

c) Duplicate email:

  • Register with the same email again
  • Expected: “Email already registered.”

d) Duplicate username:

  • Register with the same username again
  • Expected: “Username already taken.”

e) Weak password:

  • Enter password with less than 8 characters
  • Expected: “Password must be at least 8 characters long.”

f) Password without number:

  • Enter password without numbers (e.g., “password”)
  • Expected: “Password must contain at least one number.”

g) Password mismatch:

  • Enter different values for password and confirm password
  • Expected: “Passwords do not match.”

Test Case 3: Password Hashing Verification

Section titled “Test Case 3: Password Hashing Verification”
  1. Register a new user with password “test123”
  2. Open phpMyAdmin and navigate to your e-commerce database
  3. Click on the users table and view the “Browse” tab
  4. Check the password_hash column for the newly registered user
  5. Expected: Long hashed string starting with $2y$ (bcrypt), NOT “test123”


Continue to Part 2: Login & Authentication to implement:

  • User login with password verification
  • Session management
  • Route protection with middleware
  • Role-based access control (admin vs customer)
  • Dashboard views