PHP Fundamentals
User Registration System
User Registration System
Building a secure user registration system is a fundamental skill in web development. In this lesson, we'll create a complete registration system with proper validation, password hashing, and database integration.
Database Setup
First, let's create a users table to store user information:
SQL:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
full_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_login TIMESTAMP NULL,
is_active BOOLEAN DEFAULT TRUE,
INDEX idx_email (email),
INDEX idx_username (username)
);
Note: The password field is VARCHAR(255) to accommodate modern hashing algorithms like bcrypt, which produce 60-character hashes. We use 255 for future compatibility.
Registration Form HTML
Create a user-friendly registration form with proper validation attributes:
register.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Registration</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.register-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
max-width: 500px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: bold;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
.error {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
display: block;
}
.success {
background: #2ecc71;
color: white;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
text-align: center;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #5568d3;
}
.login-link {
text-align: center;
margin-top: 20px;
color: #555;
}
.login-link a {
color: #667eea;
text-decoration: none;
font-weight: bold;
}
.password-strength {
height: 5px;
margin-top: 5px;
border-radius: 3px;
transition: all 0.3s;
}
.strength-weak { background: #e74c3c; width: 33%; }
.strength-medium { background: #f39c12; width: 66%; }
.strength-strong { background: #2ecc71; width: 100%; }
</style>
</head>
<body>
<div class="register-container">
<h1>Create Account</h1>
<?php if (isset($success_message)): ?>
<div class="success"><?php echo $success_message; ?></div>
<?php endif; ?>
<form method="POST" action="register.php" id="registerForm">
<div class="form-group">
<label for="full_name">Full Name</label>
<input type="text" id="full_name" name="full_name"
value="<?php echo htmlspecialchars($full_name ?? ''); ?>"
required>
<?php if (isset($errors['full_name'])): ?>
<span class="error"><?php echo $errors['full_name']; ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username"
value="<?php echo htmlspecialchars($username ?? ''); ?>"
required>
<?php if (isset($errors['username'])): ?>
<span class="error"><?php echo $errors['username']; ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email"
value="<?php echo htmlspecialchars($email ?? ''); ?>"
required>
<?php if (isset($errors['email'])): ?>
<span class="error"><?php echo $errors['email']; ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
<div class="password-strength" id="strengthBar"></div>
<?php if (isset($errors['password'])): ?>
<span class="error"><?php echo $errors['password']; ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
<?php if (isset($errors['confirm_password'])): ?>
<span class="error"><?php echo $errors['confirm_password']; ?></span>
<?php endif; ?>
</div>
<button type="submit">Register</button>
</form>
<div class="login-link">
Already have an account? <a href="login.php">Login here</a>
</div>
</div>
<script>
// Password strength indicator
document.getElementById('password').addEventListener('input', function(e) {
const password = e.target.value;
const strengthBar = document.getElementById('strengthBar');
let strength = 0;
if (password.length >= 8) strength++;
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^a-zA-Z0-9]/.test(password)) strength++;
strengthBar.className = 'password-strength';
if (strength <= 1) strengthBar.classList.add('strength-weak');
else if (strength <= 3) strengthBar.classList.add('strength-medium');
else strengthBar.classList.add('strength-strong');
});
</script>
</body>
</html>
Registration Logic with Validation
Now let's add the PHP logic to handle form submission securely:
register.php (PHP logic at the top):
<?php
session_start();
// Database configuration
$host = 'localhost';
$dbname = 'your_database';
$db_username = 'your_username';
$db_password = 'your_password';
// Initialize variables
$errors = [];
$success_message = '';
$full_name = '';
$username = '';
$email = '';
// Create database connection
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $db_username, $db_password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get and sanitize input
$full_name = trim($_POST['full_name'] ?? '');
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
// Validate full name
if (empty($full_name)) {
$errors['full_name'] = 'Full name is required';
} elseif (strlen($full_name) < 2) {
$errors['full_name'] = 'Full name must be at least 2 characters';
} elseif (strlen($full_name) > 100) {
$errors['full_name'] = 'Full name must not exceed 100 characters';
}
// Validate username
if (empty($username)) {
$errors['username'] = 'Username is required';
} elseif (strlen($username) < 3) {
$errors['username'] = 'Username must be at least 3 characters';
} elseif (strlen($username) > 50) {
$errors['username'] = 'Username must not exceed 50 characters';
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$errors['username'] = 'Username can only contain letters, numbers, and underscores';
} else {
// Check if username already exists
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = ?');
$stmt->execute([$username]);
if ($stmt->fetch()) {
$errors['username'] = 'Username already taken';
}
}
// Validate email
if (empty($email)) {
$errors['email'] = 'Email is required';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Invalid email format';
} else {
// Check if email already exists
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$email]);
if ($stmt->fetch()) {
$errors['email'] = 'Email already registered';
}
}
// Validate password
if (empty($password)) {
$errors['password'] = 'Password is required';
} elseif (strlen($password) < 8) {
$errors['password'] = 'Password must be at least 8 characters';
} elseif (!preg_match('/[A-Z]/', $password)) {
$errors['password'] = 'Password must contain at least one uppercase letter';
} elseif (!preg_match('/[a-z]/', $password)) {
$errors['password'] = 'Password must contain at least one lowercase letter';
} elseif (!preg_match('/[0-9]/', $password)) {
$errors['password'] = 'Password must contain at least one number';
}
// Validate password confirmation
if (empty($confirm_password)) {
$errors['confirm_password'] = 'Please confirm your password';
} elseif ($password !== $confirm_password) {
$errors['confirm_password'] = 'Passwords do not match';
}
// If no errors, create the user
if (empty($errors)) {
try {
// Hash the password
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// Insert user into database
$stmt = $pdo->prepare('
INSERT INTO users (username, email, password, full_name)
VALUES (?, ?, ?, ?)
');
$stmt->execute([$username, $email, $hashed_password, $full_name]);
$success_message = 'Registration successful! You can now login.';
// Clear form fields
$full_name = '';
$username = '';
$email = '';
// Optionally redirect to login page after 2 seconds
// header('Refresh: 2; URL=login.php');
} catch (PDOException $e) {
$errors['general'] = 'Registration failed. Please try again.';
// Log the error for debugging
error_log($e->getMessage());
}
}
}
?>
Security Warning: Never store passwords in plain text! Always use
password_hash() with PASSWORD_DEFAULT, which currently uses bcrypt and will automatically upgrade to stronger algorithms in future PHP versions.
Helper Functions for Validation
Create reusable validation functions for cleaner code:
validation_helpers.php:
<?php
/**
* Validate username format and availability
*/
function validateUsername($username, $pdo) {
if (empty($username)) {
return 'Username is required';
}
if (strlen($username) < 3 || strlen($username) > 50) {
return 'Username must be between 3 and 50 characters';
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
return 'Username can only contain letters, numbers, and underscores';
}
// Check availability
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = ?');
$stmt->execute([$username]);
if ($stmt->fetch()) {
return 'Username already taken';
}
return null; // No error
}
/**
* Validate email format and availability
*/
function validateEmail($email, $pdo) {
if (empty($email)) {
return 'Email is required';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return 'Invalid email format';
}
// Check availability
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$email]);
if ($stmt->fetch()) {
return 'Email already registered';
}
return null;
}
/**
* Validate password strength
*/
function validatePassword($password) {
if (empty($password)) {
return 'Password is required';
}
if (strlen($password) < 8) {
return 'Password must be at least 8 characters';
}
if (!preg_match('/[A-Z]/', $password)) {
return 'Password must contain at least one uppercase letter';
}
if (!preg_match('/[a-z]/', $password)) {
return 'Password must contain at least one lowercase letter';
}
if (!preg_match('/[0-9]/', $password)) {
return 'Password must contain at least one number';
}
// Optional: check for special characters
// if (!preg_match('/[^a-zA-Z0-9]/', $password)) {
// return 'Password must contain at least one special character';
// }
return null;
}
/**
* Sanitize user input
*/
function sanitizeInput($data) {
$data = trim($data);
$data = stripslashes($data);
$data = htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
return $data;
}
?>
AJAX Registration (Optional Enhancement)
For a better user experience, implement AJAX registration:
register_ajax.php:
<?php
session_start();
header('Content-Type: application/json');
// Include validation helpers
require_once 'validation_helpers.php';
// Database connection
try {
$pdo = new PDO("mysql:host=localhost;dbname=your_database;charset=utf8mb4",
"username", "password");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Database error']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors = [];
$full_name = trim($_POST['full_name'] ?? '');
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
// Validate all fields
if (empty($full_name) || strlen($full_name) < 2) {
$errors['full_name'] = 'Full name must be at least 2 characters';
}
$username_error = validateUsername($username, $pdo);
if ($username_error) {
$errors['username'] = $username_error;
}
$email_error = validateEmail($email, $pdo);
if ($email_error) {
$errors['email'] = $email_error;
}
$password_error = validatePassword($password);
if ($password_error) {
$errors['password'] = $password_error;
}
if ($password !== $confirm_password) {
$errors['confirm_password'] = 'Passwords do not match';
}
if (!empty($errors)) {
echo json_encode(['success' => false, 'errors' => $errors]);
exit;
}
// Create user
try {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('
INSERT INTO users (username, email, password, full_name)
VALUES (?, ?, ?, ?)
');
$stmt->execute([$username, $email, $hashed_password, $full_name]);
echo json_encode([
'success' => true,
'message' => 'Registration successful!',
'redirect' => 'login.php'
]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Registration failed']);
error_log($e->getMessage());
}
}
?>
Pro Tip: Use AJAX for real-time validation. Check username/email availability as the user types, providing instant feedback without page reloads.
Exercise:
- Create the users table in your database
- Implement the registration form with all validation rules
- Test with various inputs: short passwords, invalid emails, duplicate usernames
- Add a "terms and conditions" checkbox that must be checked before registration
- Implement email verification (send a verification code to the user's email)
- Add CAPTCHA protection to prevent bot registrations
Best Practices Summary
- Always hash passwords using
password_hash()withPASSWORD_DEFAULT - Validate both client-side and server-side - never trust client-side validation alone
- Use prepared statements to prevent SQL injection
- Sanitize and validate all inputs before processing
- Check for duplicate usernames/emails before insertion
- Use HTTPS to encrypt data transmission
- Implement rate limiting to prevent brute force attacks
- Log registration attempts for security monitoring