Kodokon kodokon.com

Eine handgebaute MVC-Architektur

Schreibe ein komplettes Mikro-MVC-Framework - einen Regex-Router, injizierte Controller, gepufferte Views -, um zu verstehen, was Frameworks für dich erledigen.

11 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Ein Framework zu verstehen heißt, ein kleines schreiben zu können. Jede moderne MVC-Architektur ruht auf einem Front-Controller: Eine einzige index.php empfängt alle Anfragen (der Server schreibt jede URL dorthin um), dann ordnet ein Router die HTTP-Methode und den Pfad einem Handler zu. Unserer wandelt Muster wie /posts/{id} in reguläre Ausdrücke mit benannten Capture-Gruppen um: preg_match füllt danach direkt $found['id'], ohne dass du die URL manuell zerlegen musst.

PHP
<?php
declare(strict_types=1);

final class Router
{
    private array $routes = [];

    public function add(
        string $method,
        string $pattern,
        callable $handler
    ): void {
        $regex = preg_replace(
            '#\{(\w+)\}#',
            '(?P<$1>[^/]+)',
            $pattern
        );
        $this->routes[] = [
            $method, "#^$regex$#", $handler
        ];
    }

    public function dispatch(
        string $method,
        string $uri
    ): mixed {
        foreach ($this->routes as $route) {
            [$m, $re, $handler] = $route;
            if ($m !== $method) {
                continue;
            }
            if (preg_match($re, $uri, $found) === 1) {
                return $handler($found);
            }
        }
        http_response_code(404);
        return 'Page not found';
    }
}
Das Muster /posts/{id} wird zu #^/posts/(?P<id>[^/]+)$#.

Der Controller orchestriert: Er liest die Parameter, befragt das Model - die einzige Schicht, die mit der Datenbank sprechen darf - und wählt eine View. Die Abhängigkeiten kommen über den Konstruktor herein: Das ist Dependency Injection ohne Container, und mit readonly promotete Eigenschaften garantieren, dass sie sich nach der Konstruktion nie mehr ändern. Das Model gibt ein ausdrückliches null zurück, wenn die Ressource nicht existiert, statt false: Der Controller kann dann über den 404 entscheiden.

PHP
<?php
declare(strict_types=1);

final class PostModel
{
    public function __construct(
        private readonly PDO $pdo
    ) {}

    public function find(int $id): ?array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM posts WHERE id = :id'
        );
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row === false ? null : $row;
    }
}

final class PostController
{
    public function __construct(
        private readonly PostModel $posts
    ) {}

    public function show(array $params): string
    {
        $id = (int) $params['id'];
        $post = $this->posts->find($id);
        if ($post === null) {
            http_response_code(404);
            return render('not-found');
        }
        return render('post', ['post' => $post]);
    }
}
Die Funktion render wird im nächsten Block definiert.

Die View ist eine einfache PHP-Datei, die nur Variablen anzeigt, die bereits für sie vorbereitet wurden. Zwei interne Mechanismen machen das möglich: extract verwandelt das Daten-Array in lokale Variablen, und das Output-Buffering (ob_start, dann ob_get_clean) fängt die Ausgabe ab, um sie als Zeichenkette zurückzugeben, statt sie sofort zu senden - unverzichtbar, um verschachtelte Templates zusammenzusetzen oder nach dem Rendern über die Header zu entscheiden.

PHP
<?php
declare(strict_types=1);

function render(
    string $view,
    array $data = []
): string {
    extract($data, EXTR_SKIP);
    ob_start();
    require __DIR__ . "/views/$view.php";
    return (string) ob_get_clean();
}

$pdo = new PDO('sqlite:blog.db');
$controller = new PostController(
    new PostModel($pdo)
);

$router = new Router();
$router->add(
    'GET',
    '/posts/{id}',
    fn (array $p): string => $controller->show($p)
);

$path = parse_url(
    $_SERVER['REQUEST_URI'],
    PHP_URL_PATH
);

echo $router->dispatch(
    $_SERVER['REQUEST_METHOD'],
    (string) $path
);
Der komplette Front-Controller: Rendering, Verdrahtung und Dispatch.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was erzeugt in diesem Router die Umwandlung des Musters /posts/{id}?
    • Einen regulären Ausdruck mit einer benannten Capture-Gruppe, danach über $found['id'] erreichbar
    • Ein assoziatives Array der URL-Segmente
    • Einen einfachen Zeichenkettenvergleich, Segment für Segment
  2. Warum verwendet die Funktion render ob_start?
    • Um die HTML-Ausgabe vor dem Senden zu komprimieren
    • Um die Ausgabe der View abzufangen und als Zeichenkette zurückzugeben, statt sie sofort an den Browser zu senden
    • Um zu verhindern, dass die View PHP-Code ausführt
    • Um das Rendering der View auf der Festplatte zu cachen
  3. Welche Rolle hat der Front-Controller?
    • Statische Dateien schneller ausliefern
    • Die Berechtigungen des Nutzers vor jeder Seite prüfen
    • Der einzige Einstiegspunkt der Anwendung zu sein: Jede Anfrage wird dorthin umgeschrieben und dann an den Router delegiert