Escribe un microframework MVC completo - un enrutador con regex, controladores inyectados, vistas con búfer - para entender lo que los frameworks hacen por ti.
Abrir esta lección en KodokonEntender un framework significa saber escribir uno pequeño. Toda arquitectura MVC moderna se apoya en un controlador frontal: un único index.php recibe todas las peticiones (el servidor reescribe cada URL hacia él), y luego un enrutador asocia el método HTTP y la ruta a un manejador. El nuestro convierte patrones como /posts/{id} en expresiones regulares con grupos de captura con nombre: preg_match rellena entonces $found['id'] directamente, sin dividir manualmente la URL.
<?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';
}
}El controlador orquesta: lee los parámetros, consulta el modelo - la única capa autorizada a hablar con la base de datos - y elige una vista. Las dependencias entran a través del constructor: esto es inyección de dependencias sin contenedor, y promover propiedades readonly garantiza que nunca cambiarán después de la construcción. El modelo devuelve un null explícito cuando el recurso no existe, en lugar de false: el controlador puede entonces decidir el 404.
<?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]);
}
}La vista es un simple archivo PHP que solo muestra variables ya preparadas para ella. Dos mecanismos internos lo hacen posible: extract convierte el array de datos en variables locales, y el búfer de salida (ob_start y luego ob_get_clean) captura la salida para devolverla como una cadena en lugar de enviarla de inmediato - indispensable para componer plantillas anidadas o decidir las cabeceras después del renderizado.
<?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
);