亲手写一个完整的微型 MVC 框架 - 一个正则路由器、注入式控制器、带缓冲的视图 - 从而理解框架都替你做了些什么。
在 Kodokon 中打开本课理解一个框架,意味着知道如何写一个小的框架。每一种现代 MVC 架构都建立在前端控制器(front controller)之上:一个单独的 index.php 接收所有请求(服务器会把每个 URL 都重写到它),然后一个路由器把 HTTP 方法和路径匹配到某个处理器。我们的路由器会把像 /posts/{id} 这样的模式转换成带命名捕获组的正则表达式:接着 preg_match 会直接填好 $found['id'],无需手动切分 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';
}
}控制器负责编排:它读取参数,查询模型 - 唯一被允许与数据库对话的一层 - 并挑选一个视图。依赖通过构造函数传入:这是一种不借助容器的依赖注入,而把属性提升为 readonly 则保证它们在构造之后绝不会改变。当资源不存在时,模型会返回一个明确的 null 而不是 false:这样控制器就能据此决定返回 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]);
}
}视图是一个普通的 PHP 文件,只负责显示那些已经为它准备好的变量。两个内部机制让这成为可能:extract 会把数据数组转换成局部变量,而输出缓冲(先 ob_start 再 ob_get_clean)会捕获输出,把它作为字符串返回,而不是立刻发送出去 - 这对于组合嵌套模板、或在渲染之后再决定响应头都是不可或缺的。
<?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
);