基于 PHP 实现一个简易的 MVC 框架:路由、控制器与视图渲染

MVC(Model-View-Controller)是现代 Web 开发中最常见的架构模式之一。它将应用程序分为三个核心部分:模型负责数据逻辑,视图负责展示,控制器负责协调两者。虽然市面上有 Laravel、Symfony 等成熟框架,但从零实现一个简易 MVC 框架,能帮助我们更深刻地理解框架的运作机制。本文将从路由解析、控制器调度和视图渲染三个环节,构建一个可用的迷你 MVC 框架。

一、整体目录结构

一个清晰的目录结构是框架的基础。我们采用如下组织方式:

/mini-mvc
├── public/
│   └── index.php          // 唯一入口文件
├── app/
│   ├── Controllers/
│   │   └── HomeController.php
│   └── Views/
│       └── home.php
├── core/
│   ├── Router.php         // 路由解析
│   ├── Controller.php     // 控制器基类
│   └── View.php           // 视图渲染
└── routes.php             // 路由定义

所有请求都通过 public/index.php 进入,由它加载核心组件并启动框架。

二、入口文件与自动加载

入口文件负责定义常量、注册自动加载,并启动路由。

<?php
// public/index.php
define('BASE_PATH', dirname(__DIR__));

spl_autoload_register(function ($class) {
    $file = BASE_PATH . '/' . str_replace('\\', '/', $class) . '.php';
    if (file_exists($file)) {
        require $file;
    }
});

require BASE_PATH . '/routes.php';

use Core\Router;
Router::dispatch($_SERVER['REQUEST_URI']);

这里使用 spl_autoload_register 实现简单的 PSR-4 风格自动加载:将命名空间中的反斜杠转换为目录分隔符,再拼接 .php 后缀。这样 Core\Router 会自动加载 core/Router.php

三、路由解析

路由的核心任务是将 URL 映射到具体的控制器方法。我们支持形如 /user/show/1 的路径,并允许通过占位符捕获参数。

<?php
// core/Router.php
namespace Core;

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

    public static function get(string $path, array $action): void
    {
        self::$routes['GET'][$path] = $action;
    }

    public static function post(string $path, array $action): void
    {
        self::$routes['POST'][$path] = $action;
    }

    public static function dispatch(string $uri): void
    {
        $uri = parse_url($uri, PHP_URL_PATH);
        $method = $_SERVER['REQUEST_METHOD'];

        foreach (self::$routes[$method] ?? [] as $route => $action) {
            // 将 /user/{id} 转换为正则
            $pattern = preg_replace('#\{(\w+)\}#', '(?P<$1>[^/]+)', $route);
            $pattern = '#^' . $pattern . '$#';

            if (preg_match($pattern, $uri, $matches)) {
                $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
                [$controller, $methodName] = $action;
                $instance = new $controller();
                $instance->$methodName(...array_values($params));
                return;
            }
        }

        http_response_code(404);
        echo '404 Not Found';
    }
}

路由定义文件 routes.php 如下:

<?php
use App\Controllers\HomeController;

Router::get('/', [HomeController::class, 'index']);
Router::get('/user/{id}', [HomeController::class, 'show']);

当访问 /user/5 时,正则匹配会捕获 id=5,并作为参数传给 HomeController::show(5)

四、控制器基类与业务控制器

控制器基类封装视图渲染方法,让子类可以直接调用 $this->render()

<?php
// core/Controller.php
namespace Core;

abstract class Controller
{
    protected function render(string $view, array $data = []): void
    {
        View::render($view, $data);
    }
}

业务控制器继承它:

<?php
// app/Controllers/HomeController.php
namespace App\Controllers;

use Core\Controller;

class HomeController extends Controller
{
    public function index(): void
    {
        $this->render('home', [
            'title' => '欢迎来到迷你 MVC',
            'message' => '这是由 PHP 手写的简易框架。',
        ]);
    }

    public function show(int $id): void
    {
        $this->render('home', [
            'title' => "用户详情 #{$id}",
            'message' => "你正在查看 ID 为 {$id} 的用户。",
        ]);
    }
}

控制器只负责组织数据并选择视图,不掺杂 HTML,职责清晰。

五、视图渲染

视图层需要解决两个问题:一是将数据注入模板,二是支持模板嵌套或布局。这里采用“提取变量 + 输出缓冲”的方式。

<?php
// core/View.php
namespace Core;

class View
{
    public static function render(string $view, array $data = []): void
    {
        $file = BASE_PATH . '/app/Views/' . $view . '.php';

        if (!file_exists($file)) {
            throw new \RuntimeException("视图文件不存在: {$view}");
        }

        extract($data, EXTR_SKIP);
        ob_start();
        require $file;
        $content = ob_get_clean();

        // 如果有布局文件,则注入内容
        $layout = BASE_PATH . '/app/Views/layout.php';
        if (file_exists($layout)) {
            require $layout;
        } else {
            echo $content;
        }
    }
}

extract() 将数组键名转为变量名,ob_start() 开启输出缓冲,捕获视图内容后再交给布局文件。对应的视图文件:

<?php // app/Views/home.php ?>
<h1><?= htmlspecialchars($title) ?></h1>
<p><?= htmlspecialchars($message) ?></p>

布局文件:

<?php // app/Views/layout.php ?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title><?= htmlspecialchars($title ?? 'Mini MVC') ?></title>
</head>
<body>
    <main><?= $content ?></main>
</body>
</html>

注意输出时使用 htmlspecialchars 转义,避免 XSS 攻击。

六、运行与扩展方向

在项目根目录执行 php -S localhost:8000 -t public,访问 http://localhost:8000/http://localhost:8000/user/5,即可看到路由、控制器与视图协同工作的效果。

这个框架虽然只有百余行,但已经具备 MVC 的核心特征。后续可以从以下方向扩展:

  • 模型层:引入 PDO 封装,实现简单的查询构造器;
  • 中间件:在路由分发前后插入鉴权、日志等逻辑;
  • 请求与响应对象:将 $_GET$_POST 封装为 Request 类,统一响应格式;
  • 依赖注入容器:解耦控制器对具体类的依赖;
  • 异常处理:统一捕获错误并渲染错误页面。

结语

从零实现一个 MVC 框架,价值不在于替代成熟框架,而在于揭开框架的“黑箱”。路由的本质是 URL 到函数的映射,控制器的本质是请求的协调者,视图的本质是数据与模板的结合。理解了这三者,再去看 Laravel 或 Symfony 的源码,许多设计便会豁然开朗。希望本文能成为你深入 PHP 框架世界的一块垫脚石。

未经允许不得转载:任鹏个人博客 » 基于 PHP 实现一个简易的 MVC 框架:路由、控制器与视图渲染

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏