CodeBlanks Logo Icon CodeBlanks

Challenge - Mini framework

PHP course · Advanced · Lesson 92/92

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

  1. 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.

  2. Match framework concepts

    Middleware
    Layer before/after request
    Router
    Finds handler by path
    dispatch
    Calls the matched handler
    callable
    Invokable callback
  3. Continue the middleware chain

    return ____;
    Show answer
    return $next($request);

All lessons in Advanced