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方法主要操作时注册控制器中间件和执行控制器操作,具体过程等程序真正调到再作分析。添加闭包到中间见后,中间件实例大概是这样子的:

ThinkPHP6 源码阅读(七):执行中间件和控制器

从上图可以看出,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对象。Middlewarerevolve方法:

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.phpm2.phpm3.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方法前置行为区域对请求做拦截修改、判断请求的参数、重定向等操作;同理,也可以在后置行为区域对响应进行修改。
执行过程示意图:

ThinkPHP6 源码阅读(七):中间件的执行

参考

本作品采用《CC 协议》,转载必须注明作者和本文链接
Was mich nicht umbringt, macht mich stärker
讨论数量: 3

中间件的调用环节在6.0有更新噢

4年前 评论

@Fitz2015 是的,其实现已经变成管道模式了,我再另外写一篇分析吧。这篇可能5.1版本适用。

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!