Schreibe ein komplettes Mikro-MVC-Framework - einen Regex-Router, injizierte Controller, gepufferte Views -, um zu verstehen, was Frameworks für dich erledigen.
Diese Lektion in Kodokon öffnenEin 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
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';
}
}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
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 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
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
);