Laravel中的中间件是laravel中的一个重点,本篇将从源码的角度去讲解Lravel中的中间件,洞察Laravel中的中间件是如何运行的,明白为何我们使用中间件的时候要进行那些步骤. 本篇文章假设读者已经掌握中间件的基本用法,如果不了解其用法,可以移步查看laravel中间件的使用
我们都知道,使用Laravel中间件有三个步骤:
使用
php artisan
生成一个中间件,这里假设生成一个TestMiddleware
的中间件重写
TestMiddleware
中的handle
函数,其中代码逻辑写在return $next($request);
之前或者之后表示在执行请求之前或者之后运行这段代码.在
app/Http/Kernel.php
的routeMiddleware
注册一个中间件
然而,这上面的几点下来,你会不会一头雾水,为什么要执行这么些操作之后才能使用一个中间件.尤其是第二点?
中间件实现代码
你一定听过Laravel中间件的概念跟装饰器模式很像.简单来讲,装饰器模式就是在开放-关闭原则下动态的增加或者删除某一个功能.而Laravel的中间件也差不多是这个道理:
一个请求过来,在执行请求之前,可能要进行Cookie加密,开启回话,CSRF保护等等操作.但是每一个请求不一定都需要这些操作,而且,在执行请求之后也可能需要执行一些操作.我们需要根据请求的特性动态的增加一些操作.这些需求正好可以使用装饰器模式解决.
但是,Laravel中的中间件在代码实现上跟中间件 又有点区别,这里给出一段代码.真实的模拟了Laravel中间件的工作流程.
<?php
/**
* Created by PhpStorm.
* User: 89745
* Date: 2016/12/4
* Time: 13:56
*/
interface Milldeware {
public static function handle(Closure $next);
}
class VerfiyCsrfToekn implements Milldeware {
public static function handle(Closure $next)
{
echo '验证csrf Token <br>';
$next();
}
}
class ShowErrorsFromSession implements Milldeware {
public static function handle(Closure $next)
{
echo '共享session中的Error变量 <br>';
$next();
}
}
class StartSession implements Milldeware {
public static function handle(Closure $next)
{
echo '开启session <br>';
$next();
echo '关闭ession <br>';
}
}
class AddQueuedCookieToResponse implements Milldeware {
public static function handle(Closure $next)
{
$next();
echo '添加下一次请求需要的cookie <br>';
}
}
class EncryptCookies implements Milldeware {
public static function handle(Closure $next)
{
echo '解密cookie <br>';
$next();
echo '加密cookie <br>';
}
}
class CheckForMaintenacceMode implements Milldeware {
public static function handle(Closure $next)
{
echo '确定当前程序是否处于维护状态 <br>';
$next();
}
}
function getSlice() {
return function($stack,$pipe) {
return function() use($stack,$pipe){
return $pipe::handle($stack);
};
};
}
function then() {
$pipe = [
'CheckForMaintenacceMode',
'EncryptCookies',
'AddQueuedCookieToResponse',
'StartSession',
'ShowErrorsFromSession',
'VerfiyCsrfToekn'
];
$firstSlice = function() {
echo '请求向路由传递,返回相应 <br>';
};
$pipe = array_reverse($pipe);
$callback = array_reduce($pipe,getSlice(),$firstSlice);
call_user_func($callback);
}
then();
运行代码,输出
确定当前程序是否处于维护状态
解密cookie
开启session
共享session中的Error变量
验证csrf Token
请求向路由传递,返回相应
关闭ession
添加下一次请求需要的cookie
加密cookie
这段代码可能有点难懂,原因在于对于闭包函数(Closure),array_reduce
以及call_user_fun
函数,而且函数调用过程又是递归,可以尝试使用xdebug来调试执行.这里只提一点,array_reduce
中第二个参数是一个函数,这个函数需要两个参数:
第一个参数从
array_reduce
的第一个参数$pipe
数组中获得第二个参数为上一次调用的返回值.这个例子里面返回值是一个闭包函数.
如果还是不懂可以看这个例子.当理解这段代码之后,你会发现使用Laravel中间件步骤中的第2步瞬间就明白了.
源码解析
好了,通过上面的代码,我们已经解决了第二个问题.现在看看为什么使用中间件之前需要在app/Http/Kernel.php
注册中间件.
我们知道,所谓注册,也只是在$routeMiddleware
数组中添加一项而已.
要想知道,为什么中间件注册完之后就可以使用,我们需要从源码的角度去看看Laravel在底层为我们做了什么.
从Index.php文件分析,这里我们不分析Laravel源码的全部,只讲解有关中间件的部分,关于Laravel的分析,请关注我的其他文章.并且,这里只讲解全局中间件的运行过程,因为路由中间件和全局中间件的运行流程是一样的,但是路由中间件.涉及到路由分发过程,本次分析只专注于中间件,等到讲解路由的时候再对路由中间件进行展开分析.
有关中间件的步骤从Index.php的handle函数开始.
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
从这个函数开始处理一个请求,注意这里kernel有一个继承链,handle函数的真正实现在vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
文件中.
转到Http/Kernel.php
类,在看handle
函数之前,我们发现这类有两个protected
的成员:$middleware
跟$routeMiddleware
.看到这个我们就可以联想到我们注册中间件的那个文件app/Http/Kernel.php
,这个文件正是继承Illuminate/Foundation/Http/Kernel.php
的,所以我们明白了,当我们在app/Http/Kernel.php
注册的全局中间件会在这里被处理.
接着,我们看handle函数,调用了sendRequestThroughRouter
函数,进入这个函数,我们看到其函数体
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
//这里之后就会去处理中间件
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
可以看到这你实例化了一个Pipeline
.这个可以称之为管道,如果懂得linux中的管道概念的话,那么就可以理解这里命名为Pipeline
的原因:客户端发过来的请求被一个又一个的中间件处理,前一个中间件处理往之后的结果交给了下一个中间价,类似管道一样.
pipeline
之后调用了三个函数send, through, then
.这三个函数分别做了
传递客户端请求
request
到Pipeline
对象传递在
app/Http/Kernel.php
中定义的全局中间件到Pipeline
对象执行中间件,其中then函数的参数,
$this->dispatchToRouter()
返回的是一个回调函数,这个函数可以类比为我们示例代码中输出请求向路由传递,返回相应
的函数, 因为这里涉及到路由的工作流程,所以暂时这么理解,等到了分析路由的时候,我们再综合起来.
接下来,我们来看then
函数的代码
public function then(Closure $destination)
{
$firstSlice = $this->getInitialSlice($destination);
$pipes = array_reverse($this->pipes);
return call_user_func(
array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
);
}
发现then函数的代码跟我们上面的示例代码有点类似,其中 :
$pipe
就是保存了在app/Http/Kernel.php
中定义的全局中间件,具体逻辑可以看through
函数$this->passable
中保存的是客户端请求的实例对象requset
.具体逻辑可以从send
函数看到
而getInitialSlice
调用的函数只是对原有的destination
添加了一个$passable
的参数.这个$passabel
就是请求实例.
protected function getInitialSlice(Closure $destination)
{
return function ($passable) use ($destination) {
return call_user_func($destination, $passable);
};
}
了解了then函数里的所有信息, 下面执行的操作就跟我们上面的示例代码一样了,只要理解了上面代码的逻辑,这里Laravel的代码也是这么工作的,唯一不同的地方在于getSlice
返回的闭包函数,从上面的例子可以知道返回的闭包函数才是调用中间件的核心,我们来看下getSlice
到底是怎么工作的
protected function getSlice()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $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.
if ($pipe instanceof Closure) {
return call_user_func($pipe, $passable, $stack);
} else {
list($name, $parameters) = $this->parsePipeString($pipe);
return call_user_func_array([$this->container->make($name), $this->method],
array_merge([$passable, $stack], $parameters));
}
};
};
}
getSlice
函数的大体逻辑跟我们上面实例代码的逻辑差不多,只是我们上面调用在中间件的handle函数的时候直接使用$pipe::handle($stack);
,因为中间件里面的函数是静态函数.而在Laravel中,这里我们只是传递了要实例化的中间件的类名,所以在getSlice
里面还要去实例化每个要执行的中间件,
list($name, $parameters) = $this->parsePipeString($pipe);
return call_user_func_array([$this->container->make($name), $this->method], array_merge([$passable, $stack], $parameters));
上面两句代码中,第一句根据中间件的类名去分离出要实例化的中间件类,和实例化中间件可能需要的参数,
然后call_user_func_array
里面由于柔和了几行代码,所以这里分解一下,函数包含两个参数 :
[$this->container->make($name), $this->method]
为调用某个类中方法的写法,其中$this->container->make($name)
是使用服务容器去实例化要调用的中间件对象,$this->method
就是handle函数array_merge([$passable, $stack], $parameters)
为调用中间件所需要的参数,这里我们可以看到调用中间件的handle必然会传递两个参数:$passable
(请求实例$request)和下一个中间件的回调函数$stack
到这里,Laravel中间件的部分就结束了,这部分代码有点难以理解,尤其是一些具有函数式特性的函数调用,比如array_reduce
,以及大量的闭包函数和递归调用,大家一定要耐心分析,可以多使用dd
函数和xdebug
工具来逐步分析.
这里扯一句,函数then
里面可以看到有getSlice
和$firstSlice
的命名,这里slice
是一片的意思,这里这样子的命名方式是一种比喻 : 中间件的处理过程就上面讲的类似管道,处理中间件的过程比作剥洋葱,一个中间件的执行过程就是剥一片洋葱.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。