Lab: User Registration System (Part 1)
Resources
Section titled “Resources”Overview
Section titled “Overview”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.
Learning Objectives
Section titled “Learning Objectives”- 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)
Prerequisites
Section titled “Prerequisites”- 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
Step 1: Create the Users Database Table
Section titled “Step 1: Create the Users Database Table”Objective: Set up the database schema for storing user accounts.
Instructions:
- Open phpMyAdmin in your browser
- Select your project database
- Click on the “SQL” tab
- Execute the following SQL statement:
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);- Click “Go” to execute the query
- 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’
Step 2: Create the UserModel Class
Section titled “Step 2: Create the UserModel Class”Step 2.1: Class Structure
Section titled “Step 2.1: Class Structure”Create a UserModel class in the app/Domain/Models/ directory. This model will handle all database operations related to users.
Step 2.2: Implement createUser() Method
Section titled “Step 2.2: Implement createUser() Method”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
Step 2.3: Implement findByEmail() Method
Section titled “Step 2.3: Implement findByEmail() Method”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 1for 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}Step 3: Create the Registration Form View
Section titled “Step 3: Create the Registration Form View”Objective: Create an HTML form for user registration.
Create 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:
<?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 } }}Step 5: Register Routes
Section titled “Step 5: Register Routes”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' methodTesting Your Implementation
Section titled “Testing Your Implementation”Test Case 1: Valid Registration
Section titled “Test Case 1: Valid Registration”- Visit
http://localhost/[your-app]/register - Fill in all fields with valid data:
- First Name: John
- Last Name: Doe
- Username: johndoe
- Email: john@example.com
- Password: password123
- Confirm Password: password123
- Click “Register”
- Expected: Green success message and redirect to login page
Test Case 2: Validation Errors
Section titled “Test Case 2: Validation Errors”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”- Register a new user with password “test123”
- Open phpMyAdmin and navigate to your e-commerce database
- Click on the
userstable and view the “Browse” tab - Check the password_hash column for the newly registered user
- Expected: Long hashed string starting with
$2y$(bcrypt), NOT “test123”
Test Your Understanding
Section titled “Test Your Understanding”Next Steps
Section titled “Next Steps”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