ThinkPHP6 核心分析(七):中间件的执行
说明
更新日志:2019-10-29 6.0正式版中间件的逻辑改动较大,此篇分析只适用旧版本。新版使用“管道”的形式,最新的分析请看这篇:博客:ThinkPHP 6.0 管道模式与中间件的实现分析
接上篇,runWithRequest
方法最后调用的dispatch
方法还没有分析完,这里接着分析该方法后面部分,代码如下:
public function dispatch(Request $request, $withRoute = null)
{
.
.
.
} else {
//如果没有开启路由,将执行这里的语句
//$this->path()得到PATHINFO,比如/demo/hello
$dispatch = $this->url($this->path());
}
// $dispatch是think\route\dispatch\Url的实例,该类继承了Controller类
// 且该类中没有init方法,所以这里执行的是其父类的init方法
// init方法主要解析出了控制器名和操作名
$dispatch->init($this->app);
// 将一个闭包注册为中间件
// 该闭包调用了think\route\dispatch\Url类的run方法,返回一个response
$this->app->middleware->add(function () use ($dispatch) {
try {
$response = $dispatch->run();
} catch (HttpResponseException $exception) {
$response = $exception->getResponse();
}
return $response;
});
return $this->app->middleware->dispatch($request);
}
解析控制器名和操作名
Url解析之后,接下来执行$dispatch->init($this->app)
,执行分析参见以上代码注释。init
方法及注释分析如下:
public function init(App $app)
{
//父类的init调用了doRouteAfter方法
//其操作有;添加中间件,添加路由参数,绑定模型数据
// 记录当前请求的路由规则,路由变量
parent::init($app);
// ["demo", "hello"]
$result = $this->dispatch;
if (is_string($result)) {
$result = explode('/', $result);
}
// 获取控制器名
// "demo"
// 如果$result[0]为空,则使用默认控制器
$controller = strip_tags($result[0] ?: $this->rule->config('default_controller'));
// 如果控制器名称中有点号
// 也就是多级控制器解析
// 比如,控制器类的文件位置为app/index/controller/user/Blog.php
// 访问地址可以使用:http://serverName/index.php/user.blog/index
// 官方文档建议使用路由,避免点号后面部分被识别为后缀
if (strpos($controller, '.')) {
$pos = strrpos($controller, '.');
//substr($controller, 0, $pos)为点号前面部分
//Str::studly:下划线转驼峰(首字母大写)
$this->controller = substr($controller, 0, $pos) . '.' . Str::studly(substr($controller, $pos + 1));
} else {
$this->controller = Str::studly($controller);
}
// 获取操作名
$this->actionName = strip_tags($result[1] ?: $this->rule->config('default_action'));
// 设置当前请求的控制器、操作
$this->request
->setController($this->controller)
->setAction($this->actionName);
}
注意该方法文件位置: \vendor\topthink\framework\src\think\route\dispatch\Controller.php
。
将控制器操作添加到中间件
程序接着添加一个闭包到中间件,闭包里面主要操作时调用了一个run
方法。这个方法藏得比较深,查找过程如下:调用它的类think\route\dispatch\Url
并没有run
方法,向其父类think\route\dispatch\Controller
查找,也没有,再往Controller
类的父类think\route\Dispatch
查找,最后发现这个方法就位于这个类之中。run
方法主要操作时注册控制器中间件和执行控制器操作,具体过程等程序真正调到再作分析。添加闭包到中间见后,中间件实例大概是这样子的:
从上图可以看出,route
类型中间件下,一共有三个中间件,前两个是从app/middleware.php
加载进来的(之前配置的),最后一个是现在添加的。
中间件调度
接着来到dispatch
方法的最后一步:return $this->app->middleware->dispatch($request);
,获取一个中间件对象,然后调用中间件类的dispatch
方法,传入的参数是一个think\Request
对象。dispatch
代码如下:
public function dispatch(Request $request, string $type = 'route')
{
//$this->resolve($type)是一个闭包\
//这里执行一个闭包,传入的参数为一个Request对象\
//这个闭包是一个多层嵌套的闭包
return call_user_func($this->resolve($type), $request);
}
$this->resolve($type)
实际是一个闭包,传入的参数是一个Request
对象。Middleware
类revolve
方法:
protected function resolve(string $type = 'route')
{
return function (Request $request) use ($type) {
// 从队列中第一个位置删除取出一个绑定的中间件
$middleware = array_shift($this->queue[$type]);
// 已没有中间件,结束该方法
// 也就是递归终止条件
if (null === $middleware) {
throw new InvalidArgumentException('The queue was exhausted, with no response returned');
}
// 获取中间件类及其处理函数、中间件参数
// 比如,$call 为:
//Array
//(
// [0] => think\middleware\LoadLangPack
// [1] => handle
//)
list($call, $param) = $middleware;
if (is_array($call) && is_string($call[0])) {
// 实例化
// 比如
// Array
//(
// [0] => think\middleware\LoadLangPack Object
// (
// )
//
// [1] => handle
//)
$call = [$this->app->make($call[0]), $call[1]];
}
try {
// 这里递归调用「resovle」
$response = $this->app->invoke($call, [$request, $this->resolve($type), $param]);
} catch (HttpResponseException $exception) {
$response = $exception->getResponse();
}
if (!$response instanceof Response) {
throw new LogicException('The middleware must return Response instance');
}
return $response;
};
}
这个方法可能是分析到目前为止最复杂的了,它返回一个闭包,闭包中,又调用了自身,形成一个递归。假如先后加载了M1,M2,M3三个中间件,其执行顺序是:执行M1→执行M2→执行M3→返回M3→返回M2→返回M1,整个过程像是横穿过一个洋葱。
举个例子
为了更好理解中间件的执行顺序,这里举一个例子演示一下。
首先,命令行依次执行以下代码,生成三个中间件:
php think make:middleware m1
php think make:middleware m2
php think make:middleware m3
这些操作会在app/middleware
文件夹下生成三个文件,分别是 m1.php
,m2.php
,m3.php
。接着在这三个文件的handle
方法都填充以下代码:
// 当前调用的类名
$class = __CLASS__;
// 前置执行逻辑
echo "我在".$class."前置行为中<br>";
$response = $next($request);
//后置执行 后置执行逻辑
echo "我在".$class."后置行为中<br>";
return $response;
最后,编辑app
目录下的middleware.php
,添加以上三个中间件,代码如下:
return [
\app\middleware\m1::class,
\app\middleware\m2::class,
\app\middleware\m3::class,
];
同时,修改下Demo
控制器的Hello
方法,代码如下:
public function hello($name = 'ThinkPHP6')
{
echo "这里是Demo控制器的Hello方法<br>";
return 'hello,' . $name;
}
以上代码准备好了,我们就可以通过浏览器访问Demo
控制器的Hello
方法执行到以上代码,程序执行结果如下:
我在app\middleware\m1前置行为中
我在app\middleware\m2前置行为中
我在app\middleware\m3前置行为中
这里是Demo控制器的Hello方法
我在app\middleware\m3后置行为中
我在app\middleware\m2后置行为中
我在app\middleware\m1后置行为中
hello,ThinkPHP6
从这个执行过程可知,我们可以在中间件handle方法前置行为区域对请求做拦截修改、判断请求的参数、重定向等操作;同理,也可以在后置行为区域对响应进行修改。
执行过程示意图:
参考
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: