ThinkPHP控制器由Container自动创建并注入依赖,必须继承app\BaseController且置于app/controller/下;手动new会绕过容器导致依赖缺失。

控制器不是手动 new 出来的,而是由 Container 解析并注入依赖
ThinkPHP 的控制器从不靠 new UserController() 实例化,所有控制器对象都由服务容器 think\Container 统一创建。你写的控制器类必须继承 app\BaseController,且放在 app/controller/ 下;否则容器根本找不到它。
常见错误现象:直接在其他地方 new User(),结果模型没注入、$this->request 为 null、$this->cache 报错——因为绕过了容器,依赖没加载。
- 构造方法里声明类型提示(如
public function __construct(\think\Request $request)),容器会自动传入已初始化的Request实例 - 若控制器方法参数带类型提示(如
public function detail(UserModel $user, int $id)),容器也会尝试解析并注入 - 容器还负责生命周期管理:一次请求只实例化一次控制器,方法执行完即销毁,不复用
路由匹配后调用 $dispatch->run() 才真正触发控制器执行
路由解析完成后,框架拿到的是一个 think\route\dispatch\Module 对象(不是控制器本身),它的 run() 方法才是控制器执行的真正入口。这个方法内部会调用 $this->exec(),最终走到抽象调度器的 exec() 实现中。
容易踩的坑:在中间件里提前 return 或 throw 异常,会导致 $dispatch->run() 根本不执行,控制器方法一毛没跑,但 HTTP 状态码可能还是 200——因为响应被中间件截胡了。
立即学习“PHP免费学习笔记(深入)”;
-
$dispatch对象里存着模块名、控制器名、方法名、参数数组等关键信息,全靠它驱动后续流程 - 控制器方法执行前,框架会先运行前置中间件、再调用
initialize()(如果定义了) - 控制器方法返回值必须是字符串 /
View/Response实例;直接echo或die会破坏响应生命周期
控制器方法返回值决定最终响应,autoResponse 机制自动封装
控制器方法不负责输出,只负责“返回”。框架在 $dispatch->run() 返回后,会进入 autoResponse 调度阶段:根据返回值类型自动包装成 HTTP 响应。
典型错误:写了个 return json(['code'=>0]),但没关掉调试模式,结果页面上多出一堆 debug 信息——因为 autoResponse 在调试模式下默认开启 trace 输出,和你的 JSON 混在一起了。
- 返回数组 → 自动转成 JSON(Content-Type: application/json)
- 返回字符串 → 直接作为响应体(Content-Type: text/html)
- 返回
View对象 → 调用模板引擎渲染(路径按controller/method规则匹配) - 返回
Response实例 → 完全接管响应头、状态码、内容
控制器内不能直接访问模型类文件,要用 model() 或 Db::name()
控制器里写 new UserModel() 是危险操作:它绕过容器,导致模型里的数据库连接、事件监听、验证器都不会生效。ThinkPHP 要求模型必须由容器托管,否则关联查询、自动时间戳、类型转换全失效。
真实场景中经常有人在控制器里手写 SQL 或拼接条件,结果缓存不生效、事务不回滚、日志没记录——问题就出在没走 model('User') 这条正路。
-
model('User')→ 从容器获取单例,自动绑定数据库连接与事件 -
Db::name('user')→ 走查询构建器,适合简单操作,但不触发模型事件 - 硬编码路径如
new \app\model\UserModel()→ 容器不接管,等于裸写 PDO
最易被忽略的一点:控制器的 __construct() 和 initialize() 不是等价的。__construct() 是 PHP 原生构造函数,只管依赖注入;initialize() 是框架钩子,在依赖注入完成后才执行,适合做权限检查、参数预处理——但很多人把初始化逻辑全塞进构造方法,结果依赖还没注入就去调 $this->request->param(),直接报错。



















