ThinkPHP 6.0 对中间件的实现进行了巨大的改进,比起之前版本的实现更加简洁、有序、精妙,可以说是实现了质的飞跃,紧跟世界潮流。这篇文章对其实现细节进行分析。
整个实现过程初看起来有点复杂,但只要掌握了其前后的递推关系,理解起来就可以豁然开朗。
接着上一篇,我们分析了runWithRequest
方法的前两行代码,接着分析它后面的代码:
protected function runWithRequest(Request $request)
{
.
.
.
return $this->app->middleware->pipeline()
->send($request)
->then(function ($request) {
return $this->dispatchToRoute($request);
});
}
中间件的执行都在最后的return
语句中。
$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类的其他方法。
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;
}
我们上面的 D「洋葱」一共只有两层,也就是有两层中间件的闭包,假设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→返回结果。
整个过程看起来虽然复杂,但不管中间件有多少层,只要理解了前后两层中间件的这种递推关系,洋葱是怎么一层层剥开又一层层返回的,来多少层都不在话下。
Bug天天改,头发日日疏,码字不易,如果有帮助到你,就点击"下方感谢"支持一下把.