Capstone Project

Course 3 · Capstone
Capstone Project: A Small REST API With Auth, Database, and Tests
Every chapter of PHP Advanced, combined into one cohesive, testable API

This capstone brings the entire Advanced course together: a REST API (Chapter 7) backed by a Repository (Chapter 2), authenticated with tokens, tested with PHPUnit and mocking (Chapter 4), with CSRF/XSS/SQL-injection defences (Chapter 5) and basic dependency management (Chapter 1) — the shape of a genuinely real, small production API.

Ch 1
composer.json + PSR-4
Ch 2
Repository pattern for tasks
Ch 3
Router + thin controllers
Ch 4
PHPUnit + mocked repository
Ch 5
Token auth, hash_equals
Ch 7
JSON responses, status codes

What We're Building

A minimal "Task API" — authenticated CRUD over a list of tasks, each belonging to a user, with a token required for every request.

task-api/ composer.json — PSR-4: "App\\" → "src/" src/ Router.php Controllers/ TaskController.php Models/ TaskRepository.php — interface (Ch 2) MySqlTaskRepository.php — real implementation TaskNotFoundException.php Auth.php tests/ TaskControllerTest.php — uses a MOCKED repository public/index.php

The Repository Interface and Implementation

src/Models/TaskRepository.php
<?php namespace App\Models; interface TaskRepository { public function all(): array; public function find(int $id): array; // throws TaskNotFoundException public function create(string $title): int; public function delete(int $id): void; } ?>
src/Models/MySqlTaskRepository.php
<?php namespace App\Models; class TaskNotFoundException extends \Exception {} class MySqlTaskRepository implements TaskRepository { public function __construct(private \PDO $pdo) {} public function all(): array { return $this->pdo->query("SELECT * FROM tasks")->fetchAll(); } public function find(int $id): array { $stmt = $this->pdo->prepare("SELECT * FROM tasks WHERE id = :id"); $stmt->execute(['id' => $id]); $task = $stmt->fetch(); if (!$task) { throw new TaskNotFoundException("No task with ID $id"); } return $task; } public function create(string $title): int { $stmt = $this->pdo->prepare("INSERT INTO tasks (title) VALUES (:title)"); $stmt->execute(['title' => $title]); return (int)$this->pdo->lastInsertId(); } public function delete(int $id): void { $stmt = $this->pdo->prepare("DELETE FROM tasks WHERE id = :id"); $stmt->execute(['id' => $id]); } } ?>

Simple Token Authentication

src/Auth.php
<?php namespace App; class Auth { private const VALID_TOKEN = 'a1b2c3d4e5f6'; // in real use: per-user tokens, stored hashed in the DB public static function check(): bool { $header = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; $token = \str_replace('Bearer ', '', $header); return \hash_equals(self::VALID_TOKEN, $token); // Chapter 5's timing-safe comparison } } ?>

A request is expected to include Authorization: Bearer a1b2c3d4e5f6. This is deliberately simplified for the capstone's scope — real APIs typically use per-user tokens (often JWTs) issued at login and stored hashed, never a single hardcoded shared secret.

The Controller — Tying Auth, Repository, and JSON Together

src/Controllers/TaskController.php
<?php namespace App\Controllers; use App\Auth; use App\Models\TaskRepository; use App\Models\TaskNotFoundException; class TaskController { public function __construct(private TaskRepository $tasks) {} // depends on the INTERFACE, not MySqlTaskRepository private function requireAuth() { if (!Auth::check()) { jsonResponse(['error' => 'Unauthorized'], 401); } } public function index() { $this->requireAuth(); jsonResponse($this->tasks->all()); } public function show(string $id) { $this->requireAuth(); try { jsonResponse($this->tasks->find((int)$id)); } catch (TaskNotFoundException $e) { jsonResponse(['error' => 'Task not found'], 404); } } public function store() { $this->requireAuth(); $input = \json_decode(\file_get_contents('php://input'), true); if (empty($input['title'])) { jsonResponse(['error' => 'title is required'], 422); } $id = $this->tasks->create($input['title']); jsonResponse(['id' => $id], 201); } } ?>

Every endpoint calls requireAuth() first — Chapter 5's auth check, applied consistently. The constructor depends on the TaskRepository interface, not the concrete MySqlTaskRepository — exactly what makes the test below possible without touching a real database at all.

Testing the Controller — With a Mocked Repository

tests/TaskControllerTest.php
<?php use PHPUnit\Framework\TestCase; use App\Controllers\TaskController; use App\Models\TaskRepository; use App\Models\TaskNotFoundException; class TaskControllerTest extends TestCase { public function testShowReturns404ForMissingTask() { $mockRepo = $this->createMock(TaskRepository::class); $mockRepo->method('find')->willThrowException(new TaskNotFoundException()); $controller = new TaskController($mockRepo); // (capturing jsonResponse's output/status code omitted for brevity — // see Chapter 4's ob_start()/ob_get_clean() pattern for the full technique) $this->expectOutputRegex('/Task not found/'); $controller->show('999'); } } ?>

No real database, no real authentication header — this test verifies TaskController's own logic in complete isolation. This is the entire payoff of designing against an interface from the very start, demonstrated concretely rather than just described.

Deliberate simplifications for a capstone of this scope
The hardcoded single auth token, no rate limiting, and no pagination are all genuine gaps a production API would need to close — flagged here as known scope boundaries, not oversights, exactly matching the pattern from the Intermediate capstone.

Coding Challenge — Extend the Capstone

Final Challenge

Add a destroy(string $id) method to TaskController (auth-checked, returns 204 on success or 404 via TaskNotFoundException), and write a corresponding PHPUnit test using a mocked TaskRepository whose delete() method is asserted to have been called exactly once with the correct ID, using $mockRepo->expects($this->once())->method('delete')->with(5);.

📄 View solution

Course 3 Complete — PHP Advanced

  • Chapters 1-9 covered: Composer in depth, design patterns, MVC from scratch, PHPUnit/mocking, security in depth, performance/caching, framework overview, REST APIs, async/queues
  • This capstone combined Repository, mocking, token auth, and REST conventions into one small, genuinely testable API
  • Known, deliberate gaps: single hardcoded token rather than per-user auth, no rate limiting, no pagination
  • The PHP series (Fundamentals → Intermediate → Advanced) is now complete — 29 chapters across 3 courses, each with coding challenges and worked solutions