说明

更新日志: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."前置行为中
";

$response = $next($request);

//后置执行 后置执行逻辑

echo "我在".$class."后置行为中
";

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方法
";

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 协议》,转载必须注明作者和本文链接

Was mich nicht umbringt, macht mich stärker

更多推荐

php中间件运行代码_ThinkPHP6 核心分析(七):中间件的执行