MVC 到底是个什么结构?小迪把三块责任对上了号

简介: 小迪在路由闭包里用 new PDO 拼 SQL、再用 echo 吐 HTML。老陈三个问题把她问住:设计要改字号、产品要加软删除、公司要换数据库——三种完全不同的改动落在同一个文件里。MVC 不是三个文件夹,是按「改动原因」切开的三块责任。前半讲为什么要有 MVC,后半逐块对照 domain / view / controller 三个目录,最后附一张「我想改 X 去动哪个」对照表。

起因:小迪把 SQL 和 HTML 写在了一起

周三上午,小迪在写「文章详情页」。

她交上来的东西长这样:

// controller/article.php
if_get('/article/*', function ($article_id) {
   

    $pdo  = new PDO('mysql:host=127.0.0.1;dbname=blog', 'root', '123456');
    $stmt = $pdo->prepare("select * from article where id = ?");
    $stmt->execute([$article_id]);
    $row  = $stmt->fetch();

    echo '<html><body>';
    echo '<h1>' . htmlspecialchars($row['title']) . '</h1>';
    echo '<div>' . $row['content'] . '</div>';
    echo '</body></html>';
});

阿杰看了一眼,没说话。

老陈端着咖啡从后面路过,停下来:「这文件以后谁来改?」

「我改啊,有 bug 就改。」

「那我问你一个问题。」老陈把杯子放下,「三个月后,设计说标题要换成大字号带个副标题,你改哪儿?」

「改那个 echo 那行。」

「再问一个:产品说文章要改成软删除,查的时候得加条件,你改哪儿?」

「……也改这个文件。」

「再问一个:公司换数据库,或者要给文章加个『浏览量』字段,你改哪儿?」

小迪不说话了。

「你看,三种完全不同的改动,都落在同一个文件里。」老陈把杯子端起来,「这就是 MVC 要解决的事。」

先说结论:MVC 不是三个文件夹

「我知道 MVC。」小迪说,「就是 Model、View、Controller 三个文件夹嘛。」

「文件夹只是它留在磁盘上的痕迹。」老陈摇头,「你先别记那三个词,先记一句话:MVC 是按『改动原因』切的,不是按『代码种类』切的。」

他把小迪那段代码拆成三行问题:

你改它的原因 这块责任叫
表结构变了、字段变了、业务规则变了 Model
设计稿变了、排版变了、给用户看的东西变了 View
产品说这次请求该做什么变了(比如要加个校验、要跳转) Controller

「三种原因,互相之间没有因果关系。设计改字号,跟表里有没有 delete_time 一点关系都没有。既然如此,它们就不该待在同一个文件里。」

小迪点头:「所以 MVC 就是把会因为不同原因改动的东西分开。」

「对。而且这么分有个直接好处——你改字号的时候,不用担心碰到 SQL。」

为什么需要它:改动的影响半径

老陈在白板上画了两个圈。

左边那个圈里写「一个 300 行的文件」:

取数据库   →  判断业务   →  拼 HTML   →  输出

「没有分层的时候,你敲任何一个回车,影响的都是这 300 行。你不知道你改的这一处,会不会把另外两百行里某个假设打破。」

右边那三个圈,分开:

Controller  →  Model  →  Controller  →  View

「分层以后,你改 View,影响半径就是 View 那一个文件。这才是分层的真正收益,不是『看起来干净』,是『改坏了不容易连带』。」

小迪:「那如果不分层,是不是就一定出事?」

「不一定。」老陈说,「你一个人写一个只有两个页面的小工具,不分层还快一点。分层的成本是前期多写几个文件,收益是三个月后的自己少骂人。」

阿杰这时候插了一句:「我补一个更土的判断标准——你看这行代码会不会因为不同的人来改。会,就该分。」

这套框架里的 MVC:三块在哪儿

老陈把项目目录投到屏幕上。

controller/       # C:页面路由(闭包)
controller_api/   # C:接口路由(闭包)
view/             # V:Blade 模板
domain/           # M:领域层
  ├── entity/     #    表结构 + 工厂方法 + 关系
  ├── dao/        #    取数
  └── knowledge/  #    业务规则
frame/            # 框架核心库(ORM、DB、Cache、Queue、Blade、日志)

「这框架在自己的文档里第一句话就写明了:单层 MVC PHP 框架。所以不用猜它有没有 MVC,它就是。」

目录 负责什么
M domain/entity/、domain/dao/、domain/knowledge/ 数据长什么样、怎么取、规则是什么
V view/(自研 Blade) 把已经算好的东西摆成页面
C controller/、controller_api/、controller_sse/ 这次请求要做什么

小迪盯着表:「C 为什么有三个目录?」

「因为它们的输出格式不一样。页面入口只出 HTML,接口入口只出 JSON,SSE 入口是流。三个 C 共用同一个 M,只是把结果交给不同的出口。」老陈说,「这也是这套框架的一句话原则:入口决定响应格式。」

Model:不是一个文件夹,是三层

「先说 M,因为它最胖。」老陈说,「胖到还得再切。」

第一层:Entity,管表长什么样。

// domain/entity/order.php
class order extends entity
{
   
    public $structs = [
        'user_id' => 0,
        'amount'  => 0,
        'status'  => 'pending',
    ];

    public static function create(int $user_id, float $amount): order
    {
   
        $order = parent::init();
        $order->user_id = $user_id;
        $order->amount  = $amount;
        return $order;
    }
}

「注意两件事。一是类名就是表名,order 类对应 order 表——不用你另外声明表名。二是 create() 是统一的工厂方法,必填参数放前面。」

还有五个字段不用你声明,基类自己管:

字段 说明
id 主键,通过 Redis INCR 生成
version 乐观锁版本号,每次更新 +1
create_time / update_time 创建、更新时间
delete_time 软删除时间,null 表示未删除

「软删除这条就是刚才问你的那个问题——产品要加软删除,你只需要在 Entity 这一层理解它;Controller 和 View 完全不用知道有这回事。」

第二层:DAO,管怎么取数。

// domain/dao/order.php
class order_dao extends dao
{
   
    protected $table_name   = 'order';
    protected $db_config_key = 'default';
}

用的时候不实例化,直接 dao():

$order = dao('order')->find($id);

$res        = dao('order')->find_all_paginated_by_current_page_and_column(
    $page, $size, ['status' => 'paid']
);
$orders     = $res['list'];
$pagination = $res['pagination'];

「所有 SQL 都收在这一层。 你在 Controller 里看到 dao('order')->find($id),就知道它要查什么,但不用关心怎么查。」

第三层:Knowledge,管业务规则。

「比如『下单要判断库存、要算优惠、要写流水』这种一段逻辑同时动三张表的事——它既不是纯取数,也不该塞进 Controller,就下沉到 domain/knowledge/。」

「这三层的加载方式也不一样。」老陈切到 domain/ 目录:

// domain/autoload.php
spl_autoload_register(function ($class_name) {
   

    $class_maps = [
        'demo'     => 'entity/demo.php',
        'demo_dao' => 'dao/demo.php',
    ];

    if (isset($class_maps[$class_name])) {
   
        include __DIR__.'/'.$class_maps[$class_name];
    }
});
// domain/load.php
include __DIR__.'/autoload.php';

// knowledge files
// include __DIR__.'/knowledge/demo.php';

「Entity 和 DAO 走 class map 自动加载,Knowledge 是显式 include。这个框架没有 Composer autoload,所以它用一张手写的映射表代替。 好处是你能一眼看完全项目有哪些类,坏处是新增类得跑一下 sh project/tool/classmap.sh。」

View:只负责把已经算好的东西摆上去

// view/order/detail.php
@include('layout/header')

<h1>{
   {
    $order->amount }}</h1>

@foreach ($items as $item)
    <div>{
   {
    $item->name }}</div>
@endforeach

@include('layout/footer')

小迪:「这不是 PHP 文件吗?」

「是 PHP 文件,但不是原生 PHP 语法,是这套框架自研的轻量 Blade。」老陈说,「它只支持十来个指令:

{
  { $var }}              输出变量
{
  { $var or '默认值' }}   带默认值的输出
{
  {
  { $var }}}            转义输出(防 XSS)
@if / @elseif / @else / @endif
@unless / @endunless
@foreach / @endforeach
@for / @endfor
@while / @endwhile
@include('layout/header')   引入子模板,自动继承变量
@php / @endphp              原生 PHP 块
{
  {-- 注释 --}}

「没有 @extends、@section、@yield——布局用 @include 拼,不用继承。」

「它在实现上就是十个正则编译阶段,编译完的产物落在 view/blade/ 下面,文件名是 order-detail.blade.php(把斜杠换成横杠)。第一次访问编译一次,之后直接读缓存。」

「所以 V 这层的性能关键不是『模板有多快』,而是别让模板走进业务。你在模板里写一句 dao('user')->find(...),这层就该重开缓存了,而且这个页面再也没法被第二个入口复用。」

Controller:这个框架的 C 是闭包,不是类

「最后一层,也是最反直觉的一层。」老陈说,「这套框架里,没有 Controller 类。」

// controller/article.php
if_get('/article/*', function ($article_id) {
   

    $article = dao('article')->find($article_id);

    otherwise_error_code('USER_NOT_FOUND', $article->is_not_null());

    return render('article/detail', [
        'article' => $article,
    ]);
});

小迪:「这不就是个闭包吗?Controller 呢?」

「闭包就是 Controller。」老陈敲了敲白板,「框架文档原话是『路由即闭包,控制器即函数』。原因是它的加载方式就是这么直白,你看 public/index.php 最后几行:

include CONTROLLER_DIR.'/base.php';
not_found();

include 把 controller/ 下的文件读进来,文件里的 if_get(...) 在 include 那一刻就执行了;它拿当前请求路径去匹配,匹配上就跑闭包、然后 exit。」

「所以这里的『Controller』不是一个类,是这次请求命中到的那一个闭包。」

不过框架给 C 立了三条规矩:

规矩 原文意思
闭包保持简洁 长逻辑不写在路由里
不直接操作数据库 走 DAO 和 Entity,别在闭包里拼 SQL
不在路由文件里封装函数 复杂逻辑下沉 domain/knowledge/

「你这三条合起来看,就是在保护上面那张表——C 只做编排,不做事。」

还有一条是硬契约:页面入口的闭包必须返回字符串。返回数组?框架直接抛错:

页面路由必须返回字符串(HTML),实际返回:array,请将该路由迁移到 controller_api/ 目录

「它不允许你的 C 在这里吐出 JSON。要出 JSON,去 controller_api/——那也是同一个 C 层,只是换了个出口。」

一条请求走一遍 MVC

老陈把三块连起来画了一遍:

nginx  →  public/index.php
       →  bootstrap.php(加载 frame/ 核心库)
       →  if_verify:把这次请求的闭包包进 unit_of_work
       →  include controller/*.php          ← C 登场
       →  命中闭包 → dao('article')->find() ← M 上场
       →  return render('article/detail')   ← V 渲染,返回字符串
       →  echo 出去 → PHP-FPM → 浏览器

「这里有三个细节值得你看一眼。」

一、M 不认识 V,V 不认识 M。

「render() 收的是已经取好的数据,它不知道这些数据是从 MySQL 来的还是从 Redis 来的。反过来 DAO 也不知道数据最后会被渲染成 HTML 还是 JSON。两边的唯一交集是 Controller 手里的那个数组——这就是解耦的具体样子。」

二、render() 返回的是字符串,不是直接输出。

function render(string $view, array $args = [])
{
   
    if (! empty($args)) {
   
        extract($args);
    }

    $render = view_compiler();

    ob_start();

    include $render($view);

    $echo = ob_get_contents();

    ob_end_clean();

    return $echo;
}

「ob_start 开缓冲、include 模板、把缓冲内容取出来、清掉缓冲、返回字符串。所以 V 层对外只暴露一件事:给我视图名和数据,还你一段 HTML。它不 exit,也不碰响应头——那些是 Controller 和框架的事。」

三、事务不属于 C,也不属于 M。

「还记得 if_verify 里那层 unit_of_work 吗?它是包在整个闭包外面的。所以你写业务的时候不用管什么时候提交——闭包跑完,框架扫描你改过的实体,生成 SQL、开事务、提交、校验乐观锁,全在后面。」

「这就是分层的好处最直观的一次体现: 事务这件事,加进来的时候你没改任何一层的代码。」

小迪的三个问题

「MVC 是不是每个框架都长一样?」

「不是。」老陈说,「这套框架的文档里有一整节在讲它故意砍掉了什么:没有 Service 层、没有 Repository 接口、没有 DI 容器、没有 DTO、没有事件总线。它只保留了 Entity / DAO / Knowledge / View / 路由闭包。」

「理由写在 README 里:那些中间层是为『人类要把大项目切成小块、跨长周期维护』设计的。但 AI 辅助编程下,一个文件里能看懂全部逻辑,分层越多,AI 多一层猜错路径的机会。」

「那为什么 Model 里还分三个目录?这不是也分层了吗?」

「因为这是同类责任里的细分,不是横向加了一层。」老陈说,「表结构、取数、规则,三样东西的改动原因还是不一样的——加个字段不用动 Knowledge,改个优惠规则不用动 Entity。它跟『Controller → Service → Repository』那种为抽象而抽象的层不一样。」

「Controller 里能写 if 吗?」

「能。但你得问一句这个 if 是什么性质的。」老陈画了两个方框:

  • if ($user->is_null()) { redirect('/login'); } —— 这次请求怎么走,这是 C 的活儿
  • if ($stock >= $count) { ... } —— 业务规则,这属于 M

「分不清的时候用上次那条判定线:这个判断要是被别人绕过,出问题的是数据还是体验? 动数据的,往下沉。」

收尾

小迪把那段代码重写了。三块分开了:

// controller/article.php —— C:只做编排
if_get('/article/*', function ($article_id) {
   

    $article = dao('article')->find($article_id);

    otherwise_error_code('USER_NOT_FOUND', $article->is_not_null());

    return render('article/detail', ['article' => $article]);
});
// domain/entity/article.php —— M:表结构
class article extends entity
{
   
    public $structs = [
        'title'   => '',
        'content' => '',
        'author_id' => 0,
    ];

    public static function create(string $title, string $content, int $author_id): article
    {
   
        $article = parent::init();
        $article->title     = $title;
        $article->content   = $content;
        $article->author_id = $author_id;
        return $article;
    }
}
// view/article/detail.php —— V:只摆结果
@include('layout/header')

<h1>{
   {
    $article->title }}</h1>
<div>{
   {
   {
    $article->content }}}</div>

@include('layout/footer')

小迪对着这三段看了一会儿:「所以之前那个 300 行的文件,现在是三个文件。」

「而且三种改动落在三个文件上。」老陈说,「设计改字号,只碰第三个;表加字段,只碰第二个;产品说没登录要跳走,只碰第一个。这就是 MVC,一句话:让三种改动互不打扰。」

阿杰在旁边摸鱼五分钟,抬头说了一句:「我以前的判断标准是『看这文件打开有多少行』,现在换成『看这文件会因为几种原因被改』——感觉比数字靠谱。」

「你这个标准可以留着。」老陈把咖啡喝完,「Model 管真相对不对,View 管好不好看,Controller 管这次要做什么。分不清一个东西该放哪儿,就问它是为了哪一种改动而生。」


附:我想改 X,去动哪个

我想改的东西 动哪一层 具体位置
加一个字段、改字段默认值 Model domain/entity/
加个软删除、加乐观锁行为 Model domain/entity/ 基类已内置,直接用
换查询条件、加一个筛选 Model domain/dao/
「下单要判断库存+算优惠+写流水」 Model domain/knowledge/
页面排版、样式、加个区块 View view/<模块>/
加一个页面、改跳转逻辑、改校验 Controller controller/<模块>.php
加一个 JSON 接口 Controller controller_api/<模块>.php(路由以 /api/ 开头)
加一个推送流 Controller controller_sse/<模块>.php
加个数据库(跨库) Model DAO 的 db_config_key

想自己走一遍?

git clone https://github.com/smarty-kiki/php-vibe-coding-frame my-project && cd my-project
sh project/tool/start_development_server.sh   # 需要 Docker

打开 http://localhost 看到 hello world 之后,照着上面那条「一条请求走一遍 MVC」的链子走一遍:从 public/index.php 进,在 controller/base.php 找到那个命中 / 的闭包,再顺着 render('index/index') 找到 view/index/index.php。三个文件看完,MVC 就落地了。

目录
相关文章
|
16天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8239 19
|
15天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2562 14
|
14天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1873 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
13天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
9天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
9天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)
|
22天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2473 1

热门文章

最新文章