ThinkPHP生命周期始于public/index.php调用think\App::run(),依次执行环境配置加载、全局中间件、路由检测(返回Dispatch对象或抛出RouteNotFoundException)、应用分发、控制器中间件、控制器执行及app_end钩子;各阶段功能与限制明确,如路由前不可用$request参数、app_end不可修改响应。

ThinkPHP 的生命周期不是靠“读源码”硬背出来的,而是请求进来时,框架按固定顺序调用一系列关键钩子和初始化逻辑——理解它,关键在搞清「哪个阶段能干啥」「哪些地方不能改」。
入口文件触发 think\App::run() 后发生了什么
所有请求都从 public/index.php 开始,最终走到 think\App::run()。这不是一个黑盒方法,它内部是分步执行的:先加载基础配置和环境,再检测路由,然后才进入调度环节。这个阶段你还没法访问 $request 的完整参数,因为 Request 对象本身是在路由解析后才完成初始化的。
- 别在
App::run()调用前试图读取input()或param(),会返回空或默认值 - 全局中间件(如
app/middleware.php中定义的)在run()内部第一层就被执行,早于任何控制器逻辑 - 如果启用了多应用模式,
App::run()会先根据 URL 或域名决定进入哪个应用目录,之后才加载对应应用的配置
Route::check() 怎么影响后续流程
路由检测不是“匹配成功就跳转”,而是返回一个 Dispatch 对象,里面封装了模块、控制器、操作名、参数等全部调度信息。这个对象直接决定了接下来调用哪个控制器方法,也决定了是否走 RESTful 风格、是否启用资源路由。
- 如果路由没匹配上,
Route::check()返回null,框架会抛出think\exception\RouteNotFoundException,而不是继续往下走 - 自定义路由规则里用
allowCrossDomain()或option()设置的响应头,只对当前路由生效,不会污染其他路由 - 闭包定义的路由(如
Route::get('test', function(){}))跳过控制器自动解析,也就绕过了控制器中间件,但依然受全局中间件约束
控制器执行前的中间件链为什么有时不生效
中间件是否执行,取决于它被注册的位置和类型:全局中间件一定运行;应用级中间件(app/middleware.php)在应用初始化后加载;而控制器/方法级中间件(通过 middleware 属性或注解)只有在该控制器被实际调度到时才压入执行栈。
立即学习“PHP免费学习笔记(深入)”;
- 如果路由匹配到了一个不存在的控制器类,中间件根本不会执行——错误发生在实例化控制器阶段,早于中间件调用
-
return $next($request)是必须的,漏掉这句会导致后续中间件和控制器完全不执行,且无报错,只返回空响应 - 中间件中修改
$request的属性(如withParam())会影响后续控制器的param()获取结果,但不会改变原始 URL 参数
Hook::listen('app_end') 能做什么、不能做什么
这是整个生命周期最后一个可干预的钩子,在响应已生成、但尚未输出给客户端时触发。适合做日志记录、性能统计、缓存写入这类收尾动作,但不能再修改响应内容或重定向。
- 此时
Response对象已经完成渲染(比如视图已编译、JSON 已编码),$response->getContent()可以拿到最终字符串,但调用$response->redirect()无效 - 数据库连接通常还在,但事务如果没手动提交,这里 commit 有风险——因为可能已处于响应发送后的缓冲区刷新阶段
- 不要在这个钩子里 throw 异常,框架不会捕获,可能导致 PHP 致命错误或空白页
真正容易被忽略的是:生命周期里没有“统一出口”概念。你以为 app_end 是终点,但它不保证所有异步操作(如 curl_multi、Swoole 协程任务)已完成;如果你在中间件里启了协程,它的结束时间跟主生命周期无关。











