Capstone Project

Course 2 · Capstone
Capstone Project: A Simple Blog With Login, CRUD Posts, and Database Storage
Bringing together OOP, PDO, sessions, exceptions, and validation into one small, structured application

This capstone combines every Intermediate chapter into one small but genuinely structured blog: a login system, a Post class backed by a real database, full CRUD, and proper input/output handling — the shape of a real (if minimal) PHP application.

Ch 1-2
OOP — Post, User classes
Ch 3
Exceptions for DB errors
Ch 4
PDO + prepared statements
Ch 5
Sessions, password_hash
Ch 6
require_once / autoload pattern
Ch 8
htmlspecialchars, filter_var
Ch 9
Regex slug generation

Project Structure

blog/ schema.sql — users + posts tables db.php — PDO connection, shared by every page Post.php — Post class: CRUD methods exceptions/ PostNotFoundException.php login.php logout.php index.php — list all posts (Read) new_post.php — Create, login required edit_post.php — Update, login required delete_post.php — Delete, login required

schema.sql — The Database Structure

schema.sql
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL ); CREATE TABLE posts ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, slug VARCHAR(200) NOT NULL, body TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

db.php — The Shared Connection

db.php
<?php try { $pdo = new PDO("mysql:host=localhost;dbname=blog_db;charset=utf8mb4", "db_user", "secret_password"); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); } catch (PDOException $e) { die("Database connection failed."); } ?>

Post.php — An OOP Wrapper Around CRUD

Post.php
<?php class PostNotFoundException extends Exception {} class Post { public function __construct(private PDO $pdo) {} private function slugify(string $title): string { $slug = strtolower($title); $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); // Chapter 9's regex, applied here return trim($slug, '-'); } public function create(string $title, string $body): int { $stmt = $this->pdo->prepare("INSERT INTO posts (title, slug, body) VALUES (:title, :slug, :body)"); $stmt->execute([ 'title' => $title, 'slug' => $this->slugify($title), 'body' => $body ]); return (int)$this->pdo->lastInsertId(); } public function find(int $id): array { $stmt = $this->pdo->prepare("SELECT * FROM posts WHERE id = :id"); $stmt->execute(['id' => $id]); $post = $stmt->fetch(); if (!$post) { throw new PostNotFoundException("No post found with ID $id"); } return $post; } public function all(): array { return $this->pdo->query("SELECT * FROM posts ORDER BY created_at DESC")->fetchAll(); } public function update(int $id, string $title, string $body) { $stmt = $this->pdo->prepare("UPDATE posts SET title = :title, body = :body WHERE id = :id"); $stmt->execute(['title' => $title, 'body' => $body, 'id' => $id]); } public function delete(int $id) { $stmt = $this->pdo->prepare("DELETE FROM posts WHERE id = :id"); $stmt->execute(['id' => $id]); } } ?>

Every database detail (the SQL, the placeholders, the fetch mode) lives inside Post — the rest of the application calls $post->create(...), $post->find($id), etc., without needing to know or repeat any SQL. find() throws a custom PostNotFoundException (Chapter 3's pattern) rather than silently returning something misleading like false.

new_post.php — A Protected Create Page

new_post.php
<?php session_start(); require __DIR__ . '/db.php'; require __DIR__ . '/Post.php'; if (!($_SESSION['loggedIn'] ?? false)) { header("Location: login.php"); exit; } $post = new Post($pdo); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $title = trim($_POST['title'] ?? ''); $body = trim($_POST['body'] ?? ''); if ($title === '' || $body === '') { $error = "Title and body are both required."; } else { $newId = $post->create($title, $body); header("Location: index.php"); exit; } } ?> <!-- form HTML, plus echoing $error with htmlspecialchars() if set -->

index.php — Listing Posts Safely

index.php
<?php require __DIR__ . '/db.php'; require __DIR__ . '/Post.php'; $post = new Post($pdo); $allPosts = $post->all(); ?> <?php foreach ($allPosts as $row): ?> <h2><?= htmlspecialchars($row['title']) ?></h2> <p><?= htmlspecialchars($row['body']) ?></p> <?php endforeach; ?>

Every echoed value from the database is wrapped in htmlspecialchars() — exactly Chapter 8's rule — since post titles and bodies are, ultimately, also a form of user input that was stored and is now being redisplayed.

Deliberate simplifications, for a capstone of this scope
This project skips a few things a genuinely production blog would need: CSRF protection on the forms, pagination for index.php, and authorisation checks confirming a post's author matches the logged-in user before allowing edit/delete. These are natural "what's missing" discussion points, not oversights — the goal here is demonstrating Chapters 1-9 working together cleanly, not shipping a complete product.

Coding Challenge — Extend the Capstone

Final Challenge

Add a findBySlug(string $slug): array method to the Post class, used to support pretty URLs like post.php?slug=my-first-post instead of an ID. It should throw PostNotFoundException if no matching post exists, exactly like find(). Then write post.php, which reads $_GET['slug'], calls the new method inside a try/catch, and either displays the post (escaped with htmlspecialchars) or a friendly "Post not found" message.

📄 View solution

Course 2 Complete — PHP Intermediate

  • Chapters 1-9 covered: OOP basics, inheritance/interfaces/traits, exceptions, PDO/CRUD, sessions/login, namespaces/Composer, JSON/cURL, file uploads/safe input, regex
  • This capstone combined all nine into a small, genuinely structured blog application with login-protected CRUD
  • Known, deliberate gaps: no CSRF protection, pagination, or post-ownership authorisation — flagged as discussion points, not oversights
  • Next: PHP Advanced (Course 3) — Composer in depth, design patterns, MVC architecture, testing, security in depth, and a REST API capstone