ThinkPHP生命周期本质是分步执行的固定链路:入口调App::run()→加载配置、注册自动加载与错误处理→执行全局中间件(此时Request已存在但未解析参数)→Route::check()匹配路由(失败则抛RouteNotFoundException)→成功后初始化完整Request并调度控制器;各阶段有明确能力边界,如全局中间件中不可用param(),app_end钩子无法修改已发送响应。

ThinkPHP 的生命周期不是靠死记硬背流程图应付面试的,关键在于搞清「每个阶段能用什么、不能动什么」——比如 Request 对象在路由检测前根本没初始化,这时候调 input() 肯定是空的。
think\App::run() 内部到底做了什么
入口文件 public/index.php 最终调用 think\App::run(),这个方法不是原子操作,而是分步执行的固定链路:
- 先加载基础配置(
base.php)、注册自动加载器(Loader::register())、设置错误处理(Error::register()) - 再执行全局中间件(
app/middleware.php里定义的),此时$request已存在但尚未完成参数解析 - 接着调用
Route::check()做路由匹配,返回Dispatch对象或抛出RouteNotFoundException - 只有路由成功后,才真正初始化完整
Request实例,并进入控制器调度环节
常见错误:在全局中间件里直接调 param() 或 get() 拿不到 URL 参数,因为 PATH_INFO 还没被路由规则解析;必须用 $request->server('PATH_INFO') 手动提取原始路径。
Route::check() 返回 null 时发生了什么
Route::check() 不是“找不到就跳默认页”,而是明确返回 null,框架会立刻抛出 think\exception\RouteNotFoundException,后续控制器、中间件、钩子全部不执行。
立即学习“PHP免费学习笔记(深入)”;
- 这个异常由
think\exception\Handle捕获,默认返回 404 页面 - 自定义 404 处理必须在
app/exception.php中重写render()方法,不能靠控制器兜底 - 闭包路由(如
Route::get('test', function(){}))虽然绕过控制器,但仍走Route::check()流程,匹配失败一样抛异常
容易踩的坑:有人在路由文件末尾加兜底规则 Route::any('', 'Index/index'),以为能 catch 所有未匹配请求,但实际它只匹配空路径,对 /xxx 依然无效。
控制器里依赖注入 Request 对象的时机问题
控制器方法参数中声明 Request $request 是安全的,因为框架保证在调用该方法前已完成 Request 初始化;但构造函数注入就有风险:
- 如果控制器被当作服务注册进容器(如通过
bind()),构造函数会在应用初始化早期执行,此时Request根本不存在 - 即使没注册容器,TP5.1+ 默认使用反射创建控制器实例,构造函数仍早于路由解析,
$request是个空壳 - 正确做法是只在 action 方法参数里接收
Request,或用input()/param()静态方法(它们内部会懒加载Request实例)
性能影响:每次调 input() 都会触发一次 Request 单例检查,高频调用建议先存到局部变量,而不是反复调用。
app_end 钩子为什么不能修改响应内容
app_end 是整个生命周期最后一个钩子,在 Response::send() 之后触发,此时 HTTP 头已发送、主体内容已刷出到 SAPI 缓冲区。
- 在这个钩子里调
response()->code(500)或echo 'xxx'完全无效,甚至可能触发 PHP warning:「headers already sent」 - 它唯一适合做的事是日志记录、资源清理(如关闭数据库连接)、异步上报(需确保不阻塞主线程)
- 想统一处理响应,应该用响应中间件,或者在控制器返回前用
response()->withHeader()设置头信息
最常被忽略的一点:app_end 在异常未被捕获导致脚本终止时也不会执行——它只在正常流程走到最后才会触发。真要兜底,得靠 register_shutdown_function(),但要注意它运行在请求结束阶段,很多对象可能已被销毁。



















