Chapter 18 ยท Final Project
Project Part 1: Plan & Setup
Time to build something real: the Student Records Manager โ a complete web application with login, a database, and full add/view/edit/delete of student records. Every single chapter you've studied appears in it. In this part we plan the app and build its foundation: database, folders, configuration, helpers and shared layout.
What we are building
- ๐ A login page โ only staff with an account may enter (sessions + password hashing).
- ๐ A student list โ a styled table of all students, with a search box (SELECT + LIKE).
- โ An add form โ validated, sticky, saving via prepared statements (INSERT).
- โ๏ธ An edit form โ loads an existing student into the form (SELECT + UPDATE).
- ๐๏ธ Delete โ with confirmation (DELETE).
- ๐ฌ Flash messages โ friendly green/red banners after each action (sessions again).
Classes & objects โ the Student and Database classes ยท
Constructors, visibility, static โ all over the code ยท Exceptions โ database error handling ยท
SQL & PDO โ every data operation ยท Forms & validation โ add/edit pages ยท
Sessions & hashing โ login. Nothing from the course is wasted.
Step 1 โ The database
In phpMyAdmin's SQL tab, run this entire script. Save it as setup.sql in your
project too โ a project should always carry its own database recipe:
setup.sqlCREATE DATABASE IF NOT EXISTS student_manager;
USE student_manager;
CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY,
student_number VARCHAR(20) NOT NULL UNIQUE,
full_name VARCHAR(100) NOT NULL,
program VARCHAR(100) NOT NULL,
fee_balance DECIMAL(10,2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO students (student_number, full_name, program, fee_balance) VALUES
('LGU2026-001', 'Chanda Mwila', 'BSc Computing', 5000.00),
('LGU2026-002', 'Mutinta Habeenzu', 'LLB', 7200.00),
('LGU2026-003', 'Bwalya Kunda', 'BBA', 4500.00);
We'll insert the admin user in Part 3 (we need PHP to cook the password hash first).
Step 2 โ The folder structure
Inside htdocs, create this tree. Organised folders are half the battle:
student-manager/
โโโ setup.sql โ the database recipe (above)
โโโ config.php โ settings in one place
โโโ helpers.php โ small shared functions
โโโ auth-check.php โ the bouncer (Part 3)
โโโ src/
โ โโโ Database.php โ the connection class
โ โโโ Student.php โ the star of Part 2
โโโ partials/
โ โโโ header.php โ shared top of every page
โ โโโ footer.php โ shared bottom of every page
โโโ index.php โ student list (Part 2)
โโโ create.php โ add form (Part 2)
โโโ edit.php โ edit form (Part 3)
โโโ delete.php โ delete handler (Part 3)
โโโ login.php โ (Part 3)
โโโ logout.php โ (Part 3)
โโโ style.css โ one small stylesheet
Step 3 โ Configuration
Settings change between your laptop and a live server โ so they live in exactly one file:
config.php<?php
// Database settings (class constants โ Chapter 09 in action)
class Config {
const DB_HOST = "localhost";
const DB_NAME = "student_manager";
const DB_USER = "root";
const DB_PASS = "";
const APP_NAME = "Student Records Manager";
const PROGRAMS = ["BSc Computing", "LLB", "BBA", "BEd Primary"];
}
Step 4 โ Helpers
helpers.php<?php
// Safe output everywhere (the XSS shield from Chapter 16)
function e(?string $value): string {
return htmlspecialchars($value ?? "", ENT_QUOTES, "UTF-8");
}
// Flash messages: store a one-time message on the session shelf...
function flash_set(string $type, string $message): void {
$_SESSION["flash"] = ["type" => $type, "message" => $message];
}
// ...and collect it exactly once, then wipe it (that's the "flash")
function flash_get(): ?array {
if (!isset($_SESSION["flash"])) return null;
$flash = $_SESSION["flash"];
unset($_SESSION["flash"]);
return $flash;
}
// Redirect and stop โ used after every successful save/delete
function redirect(string $to): void {
header("Location: $to");
exit;
}
After saving a student we redirect to the list page โ a completely new request, and PHP's amnesia strikes. The success message rides across on the session shelf, gets shown once, and disappears. (Redirect-after-save also prevents the "form re-submits when I refresh" annoyance โ a pattern called Post/Redirect/Get.)
Step 5 โ The Database class
Straight from Chapter 15, now reading its settings from Config:
src/Database.php<?php
class Database {
private static ?PDO $pdo = null;
public static function connect(): PDO {
if (self::$pdo === null) {
$dsn = "mysql:host=" . Config::DB_HOST .
";dbname=" . Config::DB_NAME . ";charset=utf8mb4";
self::$pdo = new PDO($dsn, Config::DB_USER, Config::DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return self::$pdo;
}
}
(Passing the options array as PDO's 4th constructor argument does the same as the two
setAttribute calls โ a tidier idiom you'll see in the wild.)
Step 6 โ The shared layout
Every page will sandwich its content between these two partials, so the design lives in one place:
partials/header.php<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($pageTitle ?? Config::APP_NAME) ?></title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="topbar">
<strong>๐ <?= e(Config::APP_NAME) ?></strong>
<nav>
<a href="index.php">Students</a>
<a href="create.php">+ Add student</a>
<?php if (isset($_SESSION["username"])): ?>
<span>Hi, <?= e($_SESSION["username"]) ?></span>
<a href="logout.php">Log out</a>
<?php endif; ?>
</nav>
</header>
<main>
<?php if ($flash = flash_get()): ?>
<div class="flash flash-<?= e($flash["type"]) ?>"><?= e($flash["message"]) ?></div>
<?php endif; ?>
partials/footer.php</main>
<footer class="foot">Built by a PHP OOP: Zero to Hero graduate ๐</footer>
</body>
</html>
style.css* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f6fa; color: #222; }
.topbar { background: #2c3e6b; color: #fff; padding: 14px 24px;
display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; }
.topbar nav { display: flex; gap: 16px; align-items: center; }
.topbar a { color: #ffd97a; text-decoration: none; }
main { max-width: 900px; margin: 30px auto; padding: 0 16px; }
table { width: 100%; border-collapse: collapse; background: #fff; }
th, td { border: 1px solid #dcdfe8; padding: 10px 12px; text-align: left; }
th { background: #e8ebf5; }
.btn { display: inline-block; padding: 8px 16px; border: none; border-radius: 6px;
background: #2c3e6b; color: #fff; text-decoration: none; cursor: pointer; }
.btn-danger { background: #b23b35; }
form.card { background: #fff; padding: 24px; border-radius: 10px; max-width: 480px; }
form.card label { display: block; margin: 12px 0 4px; font-weight: 600; }
form.card input, form.card select { width: 100%; padding: 9px; border: 1px solid #c6cad6;
border-radius: 6px; font-size: 1rem; }
.flash { padding: 12px 18px; border-radius: 8px; margin-bottom: 18px; }
.flash-success { background: #e3f6e8; border: 1px solid #7cc78f; color: #1e6b34; }
.flash-error { background: #fbe9e7; border: 1px solid #e39a94; color: #8f2b25; }
.errors { background: #fbe9e7; border: 1px solid #e39a94; color: #8f2b25;
padding: 12px 18px; border-radius: 8px; margin-bottom: 16px; }
.search { display: flex; gap: 8px; margin-bottom: 16px; }
.search input { flex: 1; padding: 9px; border: 1px solid #c6cad6; border-radius: 6px; }
.foot { text-align: center; color: #888; padding: 30px 0; font-size: .85rem; }
Checkpoint
Before Part 2, verify your foundation with a temporary test file:
test.php (delete after testing)<?php
session_start();
require "config.php";
require "helpers.php";
require "src/Database.php";
$pdo = Database::connect();
$count = $pdo->query("SELECT COUNT(*) AS n FROM students")->fetch()["n"];
echo "โ Connected. Students in database: $count";
Seeing that? Foundation complete. If you get a connection error, re-check the database name in
phpMyAdmin and the constants in config.php.
Why do database settings live in config.php instead of inside each page?
How does a success message survive the redirect after saving?
- Plan first: pages, features, and the database schema before any code.
- A project carries its own
setup.sqlso anyone can rebuild its database. - Organised folders:
src/for classes,partials/for shared layout pieces. config.phpholds all settings;helpers.phpholdse(), flash and redirect.- Post/Redirect/Get + session flash messages = friendly feedback without resubmission bugs.
- Always verify the foundation with a small test before building upward.