<?php OOP: Zero to Hero Chapter 18 of 20

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

The skills map

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;
}
Why flash messages need the session

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";
โœ“ Connected. Students in database: 3

Seeing that? Foundation complete. If you get a connection error, re-check the database name in phpMyAdmin and the constants in config.php.

Quick quiz

Why do database settings live in config.php instead of inside each page?

Quick quiz

How does a success message survive the redirect after saving?

๐Ÿ”‘ Key points
  • Plan first: pages, features, and the database schema before any code.
  • A project carries its own setup.sql so anyone can rebuild its database.
  • Organised folders: src/ for classes, partials/ for shared layout pieces.
  • config.php holds all settings; helpers.php holds e(), 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.