Challenge - Mini framework
Build a simple mini framework core with middleware, routing and PDO.
Mini framework: middleware
In the final lesson you combine every part with a mini framework core. A middleware is a request-processing layer; it follows the process(array $request, callable $next): string signature. $next is the next step of the chain; calling it passes the request to the next middleware or the handler.
AuthMiddleware returns a structured error response when the user is missing; otherwise it continues the chain with $next($request). The Router registers routes with addRoute, adds middleware with use and calls the matched handler through dispatch. The middleware chain is built by reversing it with array_reverse and wrapping each step in a closure.
Middleware chain
To chain multiple middlewares, Router::dispatch builds a closure stack. The handler sits innermost; each middleware wraps it with a function(array $req) use ($m, $stack) { return $m->process($req, $stack); } closure. The request flows from the outside in — the onion model.
You reverse the middleware list with array_reverse so each step wraps the previous stack. Because AuthMiddleware is outermost, it checks the user first; if valid, $next($request) reaches the handler. Adding a new middleware becomes a single line: $router->use(...).
Exercise Write AuthMiddleware and Router
The starting code has all previous parts and the interface Middleware ready. You write the AuthMiddleware and Router classes.
The AuthMiddleware implements Middleware class uses a process(array $request, callable $next): string method to check the user: if $request["user"] is null it returns json_encode(["error" => "unauthorized"], JSON_UNESCAPED_UNICODE); otherwise it returns $next($request). The Router class holds private array $routes and array $middleware; addRoute(string $method, string $path, callable $handler): void adds a route; use(Middleware $m): void adds a middleware; dispatch(array $request): string finds the matching route, returns json_encode(["error" => "not found"], JSON_UNESCAPED_UNICODE) if none matches, otherwise builds the middleware chain with array_reverse and returns $stack($request).
Show example solution
string { if (($request["user"] ?? null) === null) { return json_encode(["error" => "unauthorized"], JSON_UNESCAPED_UNICODE); } return $next($request); } }. Write class Router { private array $routes = []; private array $middleware = []; public function addRoute(string $method, string $path, callable $handler): void { ... } public function use(Middleware $m): void { ... } public function dispatch(array $request): string { ... } }. In dispatch find the route with foreach; set $stack = $handler; foreach (array_reverse($this->middleware) as $m) { $stack = function(array $req) use ($m, $stack) { return $m->process($req, $stack); }; } return $stack($request); Then run the demo.
Example solution:
<?php
// Mini framework: write AuthMiddleware and Router
class ProductRepository {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->pdo->exec("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)");
$stmt = $this->pdo->prepare("INSERT INTO products (name, price) VALUES (?, ?)");
$stmt->execute(["Kale", 5]);
$stmt->execute(["Defter", 15]);
}
public function all(): array {
$rows = $this->pdo->query("SELECT id, name, price FROM products ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);
return array_map(fn($r) => [
"id" => (int)$r["id"],
"name" => $r["name"],
"price" => (int)$r["price"]
], $rows);
}
}
function handleGet(PDO $pdo): string {
$repo = new ProductRepository($pdo);
return json_encode(["products" => $repo->all()], JSON_UNESCAPED_UNICODE);
}
class AuthService {
private array $users = [];
private string $csrf = "";
public function register(string $user, string $pass): void {
$this->users[$user] = password_hash($pass, PASSWORD_DEFAULT);
}
public function login(string $user, string $pass): bool {
if (!isset($this->users[$user])) return false;
return password_verify($pass, $this->users[$user]);
}
public function csrfToken(): string {
if ($this->csrf === "") {
$this->csrf = bin2hex(random_bytes(8));
}
return $this->csrf;
}
public function applyHeaders(): void {
header("X-Frame-Options: DENY");
header("X-Content-Type-Options: nosniff");
}
}
interface NotificationChannel {
public function send(string $message): string;
}
class EmailChannel implements NotificationChannel {
public function send(string $message): string {
return "Email: " . $message;
}
}
class SmsChannel implements NotificationChannel {
public function send(string $message): string {
return "SMS: " . $message;
}
}
interface NotificationObserver {
public function onSent(string $log): void;
}
class LogObserver implements NotificationObserver {
public array $logs = [];
public function onSent(string $log): void {
$this->logs[] = $log;
}
class NotificationFactory {
public static function make(string $type): NotificationChannel {
return match ($type) {
"email" => new EmailChannel(),
"sms" => new SmsChannel(),
default => throw new InvalidArgumentException("Bilinmeyen kanal"),
};
}
}
class NotificationService {
private array $observers = [];
public function attach(NotificationObserver $o): void {
$this->observers[] = $o;
}
public function notify(string $type, string $message): string {
$channel = NotificationFactory::make($type);
$result = $channel->send($message);
foreach ($this->observers as $o) {
$o->onSent($result);
}
return $result;
}
}
class TestCase {
public int $passed = 0;
public int $failed = 0;
public function assertTrue(bool $cond, string $msg = ""): void {
if ($cond) { $this->passed++; } else { $this->failed++; echo "FAIL: $msg\n"; }
}
public function assertEqual($a, $b, string $msg = ""): void {
$this->assertTrue($a === $b, $msg);
}
}
class OrderService {
private array $orders = [];
public function placeOrder(string $product, int $qty): array {
$order = ["product" => $product, "qty" => $qty, "total" => $qty * 10];
$this->orders[] = $order;
return $order;
}
public function count(): int {
return count($this->orders);
}
}
class MockLogger {
public array $calls = [];
public function log(string $msg): void {
$this->calls[] = $msg;
}
}
class OrderServiceTest extends TestCase {
public function run(): void {
$this->testPlaceOrder();
$this->testCount();
}
public function testPlaceOrder(): void {
// Arrange
$svc = new OrderService();
// Act
$order = $svc->placeOrder("Pencil", 3);
// Assert
$this->assertEqual($order["total"], 30, "total calc");
}
public function testCount(): void {
// Arrange
$svc = new OrderService();
$logger = new MockLogger();
// Act
$svc->placeOrder("Notebook", 1);
$logger->log("order added");
// Assert
$this->assertEqual($svc->count(), 1, "count increased");
$this->assertEqual(count($logger->calls), 1, "log called");
}
}
interface Middleware {
public function process(array $request, callable $next): string;
}
class AuthMiddleware implements Middleware {
public function process(array $request, callable $next): string {
if (($request["user"] ?? null) === null) {
return json_encode(["error" => "unauthorized"], JSON_UNESCAPED_UNICODE);
}
return $next($request);
}
}
class Router {
private array $routes = [];
private array $middleware = [];
public function addRoute(string $method, string $path, callable $handler): void {
$this->routes[] = ["method" => $method, "path" => $path, "handler" => $handler];
}
public function use(Middleware $m): void {
$this->middleware[] = $m;
}
public function dispatch(array $request): string {
$handler = null;
foreach ($this->routes as $route) {
if ($route["method"] === $request["method"] && $route["path"] === $request["path"]) {
$handler = $route["handler"];
break;
}
}
if ($handler === null) {
return json_encode(["error" => "not found"], JSON_UNESCAPED_UNICODE);
}
$stack = $handler;
foreach (array_reverse($this->middleware) as $m) {
$stack = function(array $req) use ($m, $stack) {
return $m->process($req, $stack);
};
}
return $stack($request);
}
}
// Demo:
$router = new Router();
$router->use(new AuthMiddleware());
$router->addRoute("GET", "/products", function(array $req) {
return json_encode(["products" => []], JSON_UNESCAPED_UNICODE);
});
$req1 = ["method" => "GET", "path" => "/products", "user" => "ayse"];
echo $router->dispatch($req1) . "\n";
$req2 = ["method" => "GET", "path" => "/products", "user" => null];
echo $router->dispatch($req2) . "\n";
Test yourself
-
Which signature does a middleware function follow?
- function handle(array $req): string
- function run(): void
- function process(array $request, callable $next): string
- function dispatch(string $path): array
Show answer
function process(array $request, callable $next): string — Correct. The middleware process method takes the request and continues the chain with a callable $next.
-
Match framework concepts
- Middleware
- Layer before/after request
- Router
- Finds handler by path
- dispatch
- Calls the matched handler
- callable
- Invokable callback
-
Continue the middleware chain
return ____;Show answer
return $next($request);