ThinkPHP 6.0 管道模式与中间件的实现分析

说明

ThinkPHP 6.0 RC5 开始使用了管道模式来实现中间件,比起之前版本的实现更加简洁、有序。这篇文章对其实现细节进行分析。

首先我们从入口文件public/index.php开始,$http = (new App())->http;
获得一个http类的实例后调用它的run方法:$response = $http->run();,然后它的run方法又调用了runWithRequest方法:

protected function runWithRequest(Request $request)
{
    .
    .
    .

    return $this->app->middleware->pipeline()
        ->send($request)
        ->then(function ($request) {
            return $this->dispatchToRoute($request);
        });
}

中间件的执行都在最后的return语句中。

pipeline、through、send方法

$this->app->middleware->pipeline()pipeline方法:

public function pipeline(string $type = 'global')
{
    return (new Pipeline())  
           // array_map将所有中间件转换成闭包,闭包的特点:
          // 1. 传入参数:$request,请求实例; $next,一个闭包
          // 2. 返回一个Response实例
        ->through(array_map(function ($middleware) {
            return function ($request, $next) use ($middleware) {
                list($call, $param) = $middleware;
                if (is_array($call) && is_string($call[0])) {
                    $call = [$this->app->make($call[0]), $call[1]];
                }
                 // 该语句执行中间件类实例的handle方法,传入的参数是外部传进来的$request和$next
                 // 还有一个$param是中间件接收的参数
                $response = call_user_func($call, $request, $next, $param);

                if (!$response instanceof Response) {
                    throw new LogicException('The middleware must return Response instance');
                }
                return $response;
            };
            // 将中间件排序
        }, $this->sortMiddleware($this->queue[$type] ?? [])))
        ->whenException([$this, 'handleException']);
}

through方法代码:

public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();
    return $this;
}

前面调用through是传入的array_map(...)把中间件封装为一个个闭包,through则是把这些闭包保存在Pipeline类的$pipes属性中。

PHP的array_map方法签名:

array_map ( callable $callback , array $array1 [, array $... ] ) : array

$callback迭代作用于每一个 $array的元素,返回新的值。所以,最后得到$pipes中每个闭包的形式特征是这样的(伪代码):

function ($request, $next) {
    $response = handle($request, $next, $param);
    return $response;
}

该闭包接收两个参数,一个是请求实例,一个是回调用函数,handle方法处理后得到相应并返回。

through返回一个Pipeline类的实例,接着调用send方法:

public function send($passable)
{
    $this->passable = $passable;
    return $this;
}

该方法很简单,只是将传入的请求实例保存在$passable成员变量,最后同样返回Pipeline类的实例,这样就可以链式调用Pipeline类的其他方法。

then,carry方法

send方法之后,接着调用then方法:

return $this->app->middleware->pipeline()
            ->send($request)
            ->then(function ($request) {
                return $this->dispatchToRoute($request);
            });

这里的then接收一个闭包作为参数,这个闭包实际上包含了控制器操作的执行代码。
then方法代码:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        //用于迭代的数组(中间件闭包),这里将其倒序
        array_reverse($this->pipes),
        // array_reduce需要的回调函数
        $this->carry(),
        //这里是迭代的初始值
        function ($passable) use ($destination) {
            try {
                return $destination($passable);
            } catch (Throwable | Exception $e) {
                return $this->handleException($passable, $e);
            }
        });

    return $pipeline($this->passable);
}

carry代码:

protected function carry()
{
    // 1. $stack 上次迭代得到的值,如果是第一次迭代,其值是后面的「初始值
    // 2. $pipe 本次迭代的值
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            try {
                return $pipe($passable, $stack);
            } catch (Throwable | Exception $e) {
                return $this->handleException($passable, $e);
            }
        };
    };
}

为了更方便分析原理,我们把carry方法内联到then中去,并去掉错误捕获的代码,得到:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes),
        function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                return $pipe($passable, $stack);
            };
        },
        function ($passable) use ($destination) {
            return $destination($passable);
        });

    return $pipeline($this->passable);
}

这里关键是理解array_reduce以及$pipeline($this->passable)的执行过程,这两个过程可以类比于「包洋葱」和「剥洋葱」的过程。
array_reduce第一次迭代,$stack初始值为:
(A)

function ($passable) use ($destination) {
    return $destination($passable);
});

回调函数的返回值为:
(B)

function ($passable) use ($stack, $pipe) {
    return $pipe($passable, $stack);
};

将A代入B可以得到第一次迭代之后的$stack的值:
(C)

function ($passable) use ($stack, $pipe) {
    return $pipe($passable, 
        function ($passable) use ($destination) {
            return $destination($passable);
        })
    );
};

第二次迭代,同理,将C代入B可得:
(D)

// 伪代码
// 每一层的$pipe都代表一个中间件闭包
function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  //倒数第二层中间件
        function ($passable) use ($stack, $pipe) {
            return $pipe($passable,  //倒数第一层中间件
                function ($passable) use ($destination) {
                    return $destination($passable);  //包含控制器操作的闭包
                })
            );
        };
    );
};

以此类推,有多少个中间件,就代入多少次,最后一次得到$stack就返回给$pipeline。由于前面对中间件闭包进行了倒序,排在前面的闭包被包裹在更里层,所以倒序后的闭包越是后面的在外面,从正序来看,则变成越前面的中间件在最外层。

层层包裹好闭包后,我们得到了一个类似洋葱结构的「超级」闭包D,该闭包的结构如上面的代码注释所示。最后把$request对象传给这个闭包,执行它:$pipeline($this->passable);,由此开启一个类似剥洋葱的过程,接下来我们看看这洋葱是怎么剥开的。

剥洋葱过程分析

回顾上文,array_map(...)把每一个中间件类加工成一个类似这种结构的闭包:

function ($request, $next) {
    $response = handle($request, $next, $param);
    return $response;
}

其中handle是中间件中的入口,其结构特点是这样的:

public function handle($request, $next, $param) {
    // do sth ------ M1-1 / M2-1
    $response = $next($request);
    // do sth ------ M1-2 / M2-2
    return $response;
}

我们上面的「洋葱」一共只有两层,也就是有两层中间件的闭包,假设M1-1,M1-2分别是第一个中间件handle方法的前置和后值操作点位,第二个中间件同理,是M2-1,M2-2。现在,让程序执行$pipeline($this->passable),展开来看,也就是执行:

// 伪代码
function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($stack, $pipe) {
            return $pipe($passable,  
                function ($passable) use ($destination) {
                    return $destination($passable);  
                })
            );
        };
    );
}($this->passable)

此时,程序要求从:

return $pipe($passable,  
    function ($passable) use ($stack, $pipe) {
        return $pipe($passable,  
            function ($passable) use ($destination) {
                return $destination($passable);  
            })
        );
    };
);

返回值,也就是要执行第一个中间件闭包,$passable对应handle方法的$request参数,而下一层闭包

function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($destination) {
            return $destination($passable);  
        })
    );
}

则对应handle方法的$next参数。
要执行第一个闭包,即要执行第一个闭包的handle方法,其过程是:首先执行M1-1点位的代码,即前置操作,然后执行$response = $next($request);,这时程序进入执行下一个闭包,$next($request)展开来,也就是:

function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($destination) {
            return $destination($passable);  
        })
    );
}($request)

依次类推,执行该闭包,即执行第二个中间件的handle方法,此时,先执行M2-1点位,然后执行$response = $next($request),此时的$next闭包是:

function ($passable) use ($destination) {
    return $destination($passable);  
})

属于洋葱之芯——最里面的一层,也就是包含控制器操作的闭包,展开来看:

function ($passable) use ($destination) {
    return $destination($passable);  
})($request)

最终,我们从return $destination($passable)中返回一个Response类的实例,也就是,第二层的$response = $next($request)语句成功得到了结果,接着执行下面的语句,也就是M2-2点位,最后第二层闭包返回结果,也就是第一层闭包的$response = $next($request)语句成功得到了结果,然后执行这一层闭包该语句后面的语句,即M1-2点位,该点位之后,第一层闭包也成功返回结果,于是,then方法最终得到了返回结果。

整个过程过来,程序经过的点位顺序是这样的:M1-1→M2-1→控制器操作→M2-2→M1-2→返回结果。

总结

整个过程看起来虽然复杂,但不管中间件有多少层,只要理解了前后两层中间件的这种递推关系,洋葱是怎么一层层剥开又一层层返回的,来多少层都不在话下。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Was mich nicht umbringt, macht mich stärker
本帖由系统于 5年前 自动加精
讨论数量: 7

想请教一下,middleware类里面的pipeline方法里面闭包内的$request和$next是从那里引入的,前后文都没看到相关的变量,好像突然就出现了。

5年前 评论

@darksun $request的源头是在这里protected function runWithRequest(Request $request){...},依赖注入进来的,即调用该方法会自动实例化Request类,得到它的实例$request 。
$next指的是下一个要执行的中间件对应的闭包(中间件包装成闭包,通过array_map方法)。

5年前 评论

@tsin 可是我在runWithRequest方法里面开头dd,提示是request变量不存在,只有在pipeline匿名函数里面dd才显示出来。而且匿名函数调用外部变量,不是要用use引入吗,也没有地方用到use。

5年前 评论

@darksun 我这里有哦:

Request {#7 ▼
  #pathinfoFetch: array:3 [▶]
  #varPathinfo: "s"
  #varMethod: "_method"
  #varAjax: "_ajax"
  #varPjax: "_pjax"
  #rootDomain: ""
.
.
.

看了下,是在run方法中取得的实例:

public function run(Request $request = null): Response
    {
        //自动创建request对象
        $request = $request ?? $this->app->make('request', [], true);   # -->这里创建request对象
        $this->app->instance('request', $request);

        try {
            $response = $this->runWithRequest($request);
        } catch (Throwable $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        }

        return $response;
    }
5年前 评论

@tsin 刚刚重新试了一下,好像在 runWithRequest 方法又有了。不过还是想不太明白,没有用到use,是怎么把$request直接放到匿名函数里面的。在匿名函数外围dd $request,是不行的,dd $this->app->request可以拿到数据,但是还是想不通怎么放进匿名函数的。


public function pipeline(string $type = 'global')
    {
        dd($request);
        dd($this->app->request);
        return (new Pipeline())
            ->through(
                array_map(function ($middleware) {
                return function ($request, $next) use ($middleware) {
                    list($call, $param) = $middleware;
                    if (is_array($call) && is_string($call[0])) {
                        $call = [$this->app->make($call[0]), $call[1]];
                    }
                    $response = call_user_func($call, $request, $next, $param);
                    if (!$response instanceof Response) {
                        throw new LogicException('The middleware must return Response instance');
                    }
                    return $response;
                };
            }, $this->sortMiddleware($this->queue[$type] ?? []))
            )
            ->whenException([$this, 'handleException']);
    }
5年前 评论

@darksun 直接作为参数传给闭包呀,就像$next($request), $next 这个闭包就可以使用 $request了

5年前 评论

@tsin 理解了,这个时候只是返回一个闭包,并没有实际执行代码,实际执行代码是延后在then函数里面。多谢大佬。

5年前 评论

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