Laravel 中间件处理的核心机制 Pipeline 关键分析
Illuminate/Pipeline/Pipeline.php
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
/**
* Get the final piece of the Closure onion.
*
* @param \Closure $destination
* @return \Closure
*/
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if (is_callable($pipe)) {
// If the pipe is an instance of a Closure, we will just call it directly but
// otherwise we'll resolve the pipes out of the container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
list($name, $parameters) = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
};
};
}
- 管道处理的核心方法then,实际上是一个非常有用的PHP函数array_reduce($array, $callback, $initial)的应用,只不过待处理数组变成了可调用的中间件函数(字符串或closure表示的),这些函数都只接受一个参数即Request实例;
- 处理函数因为有额外参数$passable(即 Request实例)需要传入,所以多包了一层闭包函数【注:关于多层包含的闭包函数,在Python装饰器原理中可以充分理解,尤其是阮一峰的逐步递进写完善的装饰器过程】;
- 初始值也是一个闭包函数,这个是真正的response响应函数。
$this->method = 'handle' 这个方法名,可通过via函数修改。
中间件类方法 handle($request, Closure $next) 有2个参数,这个对应闭包处理过后的中间件函数的返回值
return $pipe($passable, $stack)。
Laravel中间件逻辑,是闭包函数强大功能的一大体现。于是我们使用闭包函数,以函数式编程风格实现简化版的中间件流程:
function f1($f){
return function($x) use ($f){
echo 'middleware 1 begin.'.PHP_EOL;
$x += 1;
$x = $f($x);
echo 'middleware 1 end.'.PHP_EOL;
return $x;
};
}
function f2($f){
return function($x) use($f){
echo 'middleware 2 begin: '.PHP_EOL;
$x += 2;
$x = $f($x);
echo 'middleware 2 end.'.PHP_EOL;
return $x;
};
}
function respond(){
return function($x){
echo 'Generate some response.'.PHP_EOL;
return $x;
};
}
$x = 1;
$response = f2(f1(respond()))($x);
echo $response;
输出:
middleware 2 begin:
middleware 1 begin.
Generate some response.
middleware 1 end.
middleware 2 end.
4
可以看出为什么Laravel的Pipeline的实现需要反转中间件数组。
高阶函数的实现,可以通过层层传递"函数名字符串"的原始方法来实现,但是通过闭包函数实现更加简单。
array_reduce写法
$f1 = function($x, $f){
echo 'middleware 1 begin.'.PHP_EOL;
$x += 1;
$x = $f($x);
echo 'middleware 1 end.'.PHP_EOL;
return $x;
};
$f2 = function($x, $f){
echo 'middleware 2 begin: '.PHP_EOL;
$x += 2;
$x = $f($x);
echo 'middleware 2 end.'.PHP_EOL;
return $x;
};
$respond = function($x){
echo 'Generate some response.'.PHP_EOL;
return $x;
};
$middlewares = [$f1, $f2];
$initial = $respond;
$foo = array_reduce($middlewares, function($stack, $item){
return function($request) use ($stack, $item){
return $item($request, $stack);
};
}, $initial);
$x = 1;
echo $foo($x);
输出:
middleware 2 begin:
middleware 1 begin.
Generate some response.
middleware 1 end.
middleware 2 end.
4
本作品采用《CC 协议》,转载必须注明作者和本文链接