In this practical project, you will learn how to develop a Student Management System using PHP, MySQL, PDO, HTML, Bootstrap, and JavaScript.
The project will allow an administrator to add students, display student records, search for students, update details, and delete records.
Project name: Student Management System
Technologies used:
Main features:
This lesson presents the core CRUD application. Authentication, role-based permissions, attendance, and fee management can be added as future features.
First, create a MySQL database named
student_management.
CREATE DATABASE student_management;
USE student_management;
CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
course VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Column explanation:
id uniquely identifies each student.name stores the student's name.email stores a unique email address.course stores the course name.created_at stores the creation timestamp.Run this SQL in phpMyAdmin or another MySQL client before running the PHP application.
Create a file named db.php. This file
establishes a reusable PDO connection.
File: db.php
<?php
$host = "localhost";
$dbname = "student_management";
$username = "YOUR_DB_USERNAME";
$password = "YOUR_DB_PASSWORD";
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
exit("Unable to connect to the database.");
}
?>
Replace the placeholder credentials with your actual MySQL credentials. Keep this file outside publicly accessible locations when your deployment structure allows it.
The $pdo variable will be used by other
PHP files to execute database queries.
Create add_student.php. This page displays
a form and saves a student using a prepared statement.
<?php
require_once "db.php";
$message = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$course = trim($_POST["course"] ?? "");
if ($name === "" || $course === "") {
$message = "Name and course are required.";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$message = "Enter a valid email address.";
} else {
try {
$stmt = $pdo->prepare(
"INSERT INTO students (name, email, course)
VALUES (:name, :email, :course)"
);
$stmt->execute([
"name" => $name,
"email" => $email,
"course" => $course
]);
header("Location: index.php?added=1");
exit;
} catch (PDOException $e) {
error_log($e->getMessage());
if ($e->getCode() === "23000") {
$message = "This email address may already exist.";
} else {
$message = "Unable to save the student.";
}
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Add Student</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="card shadow-sm mx-auto" style="max-width:600px;">
<div class="card-header bg-primary text-white">
Add New Student
</div>
<div class="card-body">
<?php if ($message !== ""): ?>
<div class="alert alert-danger">
<?= htmlspecialchars($message, ENT_QUOTES, "UTF-8") ?>
</div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label">Student Name</label>
<input type="text" name="name"
class="form-control" required maxlength="100">
</div>
<div class="mb-3">
<label class="form-label">Email Address</label>
<input type="email" name="email"
class="form-control" required maxlength="150">
</div>
<div class="mb-3">
<label class="form-label">Course</label>
<input type="text" name="course"
class="form-control" required maxlength="100">
</div>
<button type="submit" class="btn btn-success">
Save Student
</button>
<a href="index.php" class="btn btn-secondary">
Back
</a>
</form>
</div>
</div>
</div>
</body>
</html>
This example validates the basic fields and uses a prepared statement. A production application should also implement CSRF protection and appropriate authorization.
Create index.php to display student
records and search by name or course.
<?php
require_once "db.php";
$search = trim($_GET["search"] ?? "");
$sql = "SELECT id, name, email, course, created_at
FROM students";
$params = [];
if ($search !== "") {
$sql .= " WHERE name LIKE :name
OR course LIKE :course";
$params["name"] = "%" . $search . "%";
$params["course"] = "%" . $search . "%";
}
$sql .= " ORDER BY id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$students = $stmt->fetchAll();
function e($value) {
return htmlspecialchars(
(string) $value,
ENT_QUOTES,
"UTF-8"
);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Student Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<h1 class="mb-4">Student Management System</h1>
<?php if (isset($_GET["added"])): ?>
<div class="alert alert-success">
Student added successfully.
</div>
<?php endif; ?>
<form method="GET" class="row g-2 mb-3">
<div class="col-md-8">
<input type="search" name="search"
class="form-control"
placeholder="Search name or course"
value="<?= e($search) ?>">
</div>
<div class="col-md-4">
<button class="btn btn-primary" type="submit">
Search
</button>
<a href="index.php" class="btn btn-secondary">
Reset
</a>
</div>
</form>
<a href="add_student.php" class="btn btn-success mb-3">
+ Add Student
</a>
<div class="table-responsive">
<table class="table table-bordered table-striped bg-white">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Course</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (!$students): ?>
<tr>
<td colspan="5" class="text-center">
No students found.
</td>
</tr>
<?php else: ?>
<?php foreach ($students as $student): ?>
<tr>
<td><?= e($student["id"]) ?></td>
<td><?= e($student["name"]) ?></td>
<td><?= e($student["email"]) ?></td>
<td><?= e($student["course"]) ?></td>
<td>
<a class="btn btn-sm btn-primary"
href="edit_student.php?id=<?= (int) $student["id"] ?>">
Edit
</a>
<form method="POST"
action="delete_student.php"
class="d-inline"
onsubmit="return confirm('Delete this student?');">
<input type="hidden" name="id"
value="<?= (int) $student["id"] ?>">
<input type="hidden" name="csrf_token"
value="<?= e($_SESSION["csrf_token"] ?? "") ?>">
<button type="submit"
class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</body>
</html>
Before using the delete form above, add the session and CSRF initialization described in the security section below. The example illustrates the listing layout and prepared search query.
Create edit_student.php. The page retrieves
a student by ID and updates the student's details.
<?php
require_once "db.php";
$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
http_response_code(400);
exit("Invalid student ID.");
}
$stmt = $pdo->prepare(
"SELECT id, name, email, course
FROM students WHERE id = :id"
);
$stmt->execute(["id" => $id]);
$student = $stmt->fetch();
if (!$student) {
http_response_code(404);
exit("Student not found.");
}
$message = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$course = trim($_POST["course"] ?? "");
if ($name === "" || $course === "") {
$message = "Name and course are required.";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$message = "Enter a valid email address.";
} else {
try {
$stmt = $pdo->prepare(
"UPDATE students
SET name = :name,
email = :email,
course = :course
WHERE id = :id"
);
$stmt->execute([
"name" => $name,
"email" => $email,
"course" => $course,
"id" => $id
]);
header("Location: index.php?updated=1");
exit;
} catch (PDOException $e) {
error_log($e->getMessage());
$message = "Unable to update student details.";
}
}
$student["name"] = $name;
$student["email"] = $email;
$student["course"] = $course;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Edit Student</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="card mx-auto shadow-sm" style="max-width:600px;">
<div class="card-header bg-primary text-white">
Edit Student
</div>
<div class="card-body">
<?php if ($message !== ""): ?>
<div class="alert alert-danger">
<?= htmlspecialchars($message, ENT_QUOTES, "UTF-8") ?>
</div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name"
class="form-control" required maxlength="100"
value="<?= htmlspecialchars($student["name"], ENT_QUOTES, "UTF-8") ?>">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email"
class="form-control" required maxlength="150"
value="<?= htmlspecialchars($student["email"], ENT_QUOTES, "UTF-8") ?>">
</div>
<div class="mb-3">
<label class="form-label">Course</label>
<input type="text" name="course"
class="form-control" required maxlength="100"
value="<?= htmlspecialchars($student["course"], ENT_QUOTES, "UTF-8") ?>">
</div>
<button class="btn btn-success" type="submit">
Update Student
</button>
<a href="index.php" class="btn btn-secondary">
Cancel
</a>
</form>
</div>
</div>
</div>
</body>
</html>
This illustrates the update query. Before deploying it, protect the form with authentication and CSRF validation, as shown in the next section.
Create delete_student.php. Deletion should
use POST rather than a GET link, because it changes
application data.
<?php
session_start();
header("Content-Type: text/plain; charset=UTF-8");
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Allow: POST");
http_response_code(405);
exit("Method not allowed.");
}
$token = $_POST["csrf_token"] ?? "";
if (
!is_string($token) ||
empty($_SESSION["csrf_token"]) ||
!hash_equals($_SESSION["csrf_token"], $token)
) {
http_response_code(403);
exit("Invalid security token.");
}
// Require an authenticated administrator here.
// A CSRF token alone does not provide authorization.
$id = filter_input(INPUT_POST, "id", FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
http_response_code(400);
exit("Invalid student ID.");
}
require_once "db.php";
$stmt = $pdo->prepare(
"DELETE FROM students WHERE id = :id"
);
$stmt->execute(["id" => $id]);
header("Location: index.php?deleted=1");
exit;
?>
Initialize the CSRF token on pages that display forms.
For example, put the following near the beginning of
index.php, before any HTML output:
<?php
session_start();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
require_once "db.php";
?>
Use the same session initialization on the add and edit pages and include the token in their forms. Validate it before processing every state-changing request. Also check that the logged-in user has permission to delete the selected record.
Test the application in this order:
add_student.php and add a student.index.php and verify the record.Common problems:
On a production server, log errors privately and do not display database exceptions or stack traces to visitors.
Extend the Student Management System into a complete application with an administrator dashboard.
Required features:
Suggested folder structure:
student_management/
|
|-- db.php
|-- index.php
|-- add_student.php
|-- edit_student.php
|-- delete_student.php
|
|-- admin/
| |-- login.php
| |-- dashboard.php
| |-- students.php
| |-- courses.php
| |-- fees.php
| |-- attendance.php
|
|-- assets/
| |-- css/
| |-- js/
|
|-- database.sql
Final goal: Build a working CRUD application using PHP and MySQL, then expand it with secure login, fee management, and attendance features.
1. Which PHP extension is used in this project to connect to MySQL?
2. Which SQL operation changes an existing student record?
3. Why should a delete form use CSRF protection?