📖 Admin User Management with PHP MVC

The updates include:

  • Expanding the user registration form and model logic
  • Modifying the database schema to support roles and block status
  • Creating controller methods for user management
  • Securing access to protected pages using session-based roles

Each section builds on the last, allowing you to incrementally develop the admin functionality needed for real-world applications.

🧩 Updating the Shared form-fields.php Partial

Since we already have a registration form using form-field partials, we will only need to update those form fields.

The views/profile/partials/form-fields.php file defines the input fields used in both the registration and edit profile forms. To support the full registration process, ensure this partial includes inputs for name, email, username, and password.

<div>
  <label for="name" class="form-label">Name:</label>
  <input type="text" class="form-control" name="name" id="name"
         value="<?= htmlspecialchars($user['name'] ?? $post['name'] ?? '') ?>">
  <?php if (!empty($errors['name'])): ?>
    <p class="text-danger"><?= $errors['name'] ?></p>
  <?php endif; ?>
</div>

<div>
  <label for="email" class="form-label">Email:</label>
  <input type="email" class="form-control" name="email" id="email"
         value="<?= htmlspecialchars($user['email'] ?? $post['email'] ?? '') ?>">
  <?php if (!empty($errors['email'])): ?>
    <p class="text-danger"><?= $errors['email'] ?></p>
  <?php endif; ?>
</div>

<div>
  <label for="username" class="form-label">Username:</label>
  <input type="text" class="form-control" name="username" id="username"
         value="<?= htmlspecialchars($user['username'] ?? $post['username'] ?? '') ?>">
  <?php if (!empty($errors['username'])): ?>
    <p class="text-danger"><?= $errors['username'] ?></p>
  <?php endif; ?>
</div>

<div>
  <label for="password" class="form-label">Password:</label>
  <input type="password" class="form-control" name="password" id="password">
  <?php if (!empty($errors['password'])): ?>
    <p class="text-danger"><?= $errors['password'] ?></p>
  <?php endif; ?>
</div>

Only editable fields are included here — values like role and is_blocked are controlled by the system and should not appear in the form.

Update the UserController::register() Method

The UserController::register() method has been updated to handle additional fields: username and password. Input validation now checks all fields, including password length and username presence.

Passwords are never stored directly — the method delegates secure hashing to the model layer. This separation of concerns aligns with MVC principles and helps ensure data integrity and application security.

public function register()
{
    $post = [
        'name' => '',
        'email' => '',
        'username' => '',
        'password' => '',
    ];
    $errors = [];

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $post['name'] = trim($_POST['name'] ?? '');
        $post['email'] = trim($_POST['email'] ?? '');
        $post['username'] = trim($_POST['username'] ?? '');
        $post['password'] = $_POST['password'] ?? '';

        // Validate inputs
        if ($post['name'] === '') {
            $errors['name'] = 'Name is required.';
        }

        if (!filter_var($post['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = 'Please enter a valid email address.';
        }

        if ($post['username'] === '') {
            $errors['username'] = 'Username is required.';
        }

        if (strlen($post['password']) < 6) {
            $errors['password'] = 'Password must be at least 6 characters.';
        }

        // Save user if no errors
        if (empty($errors)) {
            $userId = UserModel::createUser($post);

            if ($userId) {
                header("Location: profile.php?id=$userId");
                exit;
            } else {
                $errors['db'] = '❌ Failed to save user to database.';
            }
        }
    }

    require 'views/profile/create.php';
}
    

🧱 Expanding the Users Table

To support user roles, authentication, and access control, your users table should include the following columns:

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  username VARCHAR(100) NOT NULL UNIQUE,
  password VARCHAR(255) NOT NULL,
  role ENUM('user', 'admin') NOT NULL DEFAULT 'user',
  is_blocked TINYINT(1) NOT NULL DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
name
The user's full name (used for display purposes).
email
Used for communication and unique user identification.
username
Used for login and shown in the UI (must be unique).
password
Stores a hashed version of the user's password.
role
Specifies the user's access level. Admins can manage other users.
is_blocked
Marks users as blocked (1) or active (0).
created_at
Records when the user was created.

🧠 Best Practice: Always set default values when adding new fields to avoid breaking existing rows.

🔐 Updating the Model Logic

When creating new users, assign the default role and status within the model — not from user input. Passwords should be securely hashed, and roles should always default to 'user'.

Model Method: createUser()

Update the UserModel::createUser() method to include all required fields:

public static function createUser($post)
{
    $db = static::getDB();
    $sql = "INSERT INTO users (name, email, username, password, role, is_blocked)
            VALUES (:name, :email, :username, :password, 'user', 0)";
    $stmt = $db->prepare($sql);

    $success = $stmt->execute([
        ':name' => $post['name'],
        ':email' => $post['email'],
        ':username' => $post['username'],
        ':password' => password_hash($post['password'], PASSWORD_DEFAULT),
    ]);

    return $success ? $db->lastInsertId() : false;
}
password_hash()
Encrypts the password using PHP’s built-in secure hashing function.

🛑 Security Reminder: Never allow users to choose their own role or is_blocked status. These should always be system-controlled.

🔄 Supporting Profile Updates

The shared form-fields.php partial is used in both the registration and profile edit views. Since we've added fields like username and password, the edit flow needs to support them too.

No changes are needed in the UserController::edit() method — it already loads the form with user data. However, we do need to update the model logic to handle these new fields when saving changes.

Update UserModel::updateUser()

This method now processes username updates and optionally changes the password if a new one is provided. If the password field is left blank, the original hash is preserved.

public static function updateUser($post) {
    global $pdo;

    $fields = [
        'name' => $post['name'],
        'email' => $post['email'],
        'username' => $post['username']
    ];

    $sql = "UPDATE users SET name = :name, email = :email, username = :username";

    if (!empty($post['password'])) {
        $fields['password'] = password_hash($post['password'], PASSWORD_DEFAULT);
        $sql .= ", password = :password";
    }

    $sql .= " WHERE id = :id";
    $fields['id'] = $post['id'];

    $stmt = $pdo->prepare($sql);
    return $stmt->execute($fields);
}

💡 Tip: Make sure your edit.php form includes a hidden id field so the model knows which user to update.

🧰 Admin Management Functions

These utility methods provide core functionality that will be used by the admin dashboard and other role-based views. While they may not be called immediately, they lay the groundwork for listing users, promoting roles, and blocking accounts — features typically managed via controller logic and routed from admin-facing pages.

By preparing these methods in the UserModel, the controller can later call them to handle specific admin actions without duplicating database logic.

Add the following methods to UserModel.php to support admin-level actions:

Get All Users

function getAllUsers() {
  $stmt = $pdo->query("SELECT id, username, role, is_blocked FROM users ORDER BY id ASC");
  return $stmt->fetchAll();
}

Update Role

function updateRole($userId, $newRole) {
  $stmt = $pdo->prepare("UPDATE users SET role = :role WHERE id = :id");
  $stmt->execute(['role' => $newRole, 'id' => $userId]);
}

Block/Unblock User

function setBlockStatus($userId, $blockStatus) {
  $stmt = $pdo->prepare("UPDATE users SET is_blocked = :blocked WHERE id = :id");
  $stmt->execute(['blocked' => $blockStatus, 'id' => $userId]);
}

👤 Role-Based Login and Sessions

With roles and access control now part of your data model, the next step is securing access to different parts of the application. These role-based login and session handling functions will allow you to:

  • Identify users and store their role after login
  • Restrict admin-only pages like admin/dashboard.php
  • Use session-based logic to personalize views or hide actions

This logic typically runs when the user logs in and persists across pages using PHP sessions. It enables a secure, consistent way to distinguish admin users from general users throughout the application.

Update your login logic to check role and block status, and store these values in the session for later use.

// In UserModel.php
function findByUsername($username) {

  $stmt = $pdo->prepare("SELECT id, username, password, role, is_blocked
                         FROM users WHERE username = :username");

  $stmt->execute(['username' => $username]);

  return $stmt->fetch();
}
// In controller login logic
if ($user['is_blocked']) {
  echo "Account is blocked. Please contact support.";
  exit;
}

$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['role']
Stores user role so protected pages can quickly check permissions.

Summary / Takeaways

  • Always control roles and block status from the server — never the client
  • Store role and status in the session to enforce access and display logic
  • Use role checks in both views and controllers for full protection
  • Soft delete is safer than permanent deletion and preserves data integrity

Additional Resources

Last updated: August 9, 2025 at 4:24 PM