资讯专栏INFORMATION COLUMN

深入理解Laravel中间件

xiaotianyi / 1093人阅读

摘要:中的中间件是中的一个重点本篇将从源码的角度去讲解中的中间件洞察中的中间件是如何运行的明白为何我们使用中间件的时候要进行那些步骤本篇文章假设读者已经掌握中间件的基本用法如果不了解其用法可以移步查看中间件的使用我们都知道使用中间件有三个步骤使用

Laravel中的中间件是laravel中的一个重点,本篇将从源码的角度去讲解Lravel中的中间件,洞察Laravel中的中间件是如何运行的,明白为何我们使用中间件的时候要进行那些步骤. 本篇文章假设读者已经掌握中间件的基本用法,如果不了解其用法,可以移步查看laravel中间件的使用

我们都知道,使用Laravel中间件有三个步骤:

使用php artisan生成一个中间件,这里假设生成一个TestMiddleware的中间件

重写TestMiddleware中的handle函数,其中代码逻辑写在return $next($request);之前或者之后表示在执行请求之前或者之后运行这段代码.

app/Http/Kernel.phprouteMiddleware注册一个中间件

然而,这上面的几点下来,你会不会一头雾水,为什么要执行这么些操作之后才能使用一个中间件.尤其是第二点?

中间件实现代码

你一定听过Laravel中间件的概念跟装饰器模式很像.简单来讲,装饰器模式就是在开放-关闭原则下动态的增加或者删除某一个功能.而Laravel的中间件也差不多是这个道理:

一个请求过来,在执行请求之前,可能要进行Cookie加密,开启回话,CSRF保护等等操作.但是每一个请求不一定都需要这些操作,而且,在执行请求之后也可能需要执行一些操作.我们需要根据请求的特性动态的增加一些操作.这些需求正好可以使用装饰器模式解决.

但是,Laravel中的中间件在代码实现上跟中间件 又有点区别,这里给出一段代码.真实的模拟了Laravel中间件的工作流程.

";
        $next();
    }
}


class ShowErrorsFromSession implements Milldeware {

    public static function handle(Closure $next)
    {
        echo "共享session中的Error变量 
"; $next(); } } class StartSession implements Milldeware { public static function handle(Closure $next) { echo "开启session
"; $next(); echo "关闭ession
"; } } class AddQueuedCookieToResponse implements Milldeware { public static function handle(Closure $next) { $next(); echo "添加下一次请求需要的cookie
"; } } class EncryptCookies implements Milldeware { public static function handle(Closure $next) { echo "解密cookie
"; $next(); echo "加密cookie
"; } } class CheckForMaintenacceMode implements Milldeware { public static function handle(Closure $next) { echo "确定当前程序是否处于维护状态
"; $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 "请求向路由传递,返回相应
"; }; $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(IlluminateContractsHttpKernel::class);

$response = $kernel->handle(
    $request = IlluminateHttpRequest::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.这三个函数分别做了

传递客户端请求requestPipeline对象

传递在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是一片的意思,这里这样子的命名方式是一种比喻 : 中间件的处理过程就上面讲的类似管道,处理中间件的过程比作剥洋葱,一个中间件的执行过程就是剥一片洋葱.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/22126.html

相关文章

  • 深入理解 Laravel Eloquent(二)——中间操作流(Builder)

    摘要:原文发表在我的个人网站深入理解二中间操作流本篇教程是该系列教材的第二篇,将主要讲述中中间操作流的概念。复杂用法示例下一步深入理解三模型间关系关联 原文发表在我的个人网站:深入理解 Laravel Eloquent(二)——中间操作流(Builder) 本篇教程是该系列教材的第二篇,将主要讲述 Eloquent 中中间操作流的概念。中间操作流是我自己总结并翻译的概念,支撑该功能的类...

    yy736044583 评论0 收藏0
  • 深入理解 Laravel 管道

    摘要:将请求传入到指定的中间件路由。用于处理任务的方法接收两个参数,第一个是一个可传递的对象,第二个是闭包,在运行最后一个管道后对象将被重定向到这个闭包。我希望这个实例能够让你对有更深如的了解,并知道如何使用它们。 这是一篇译文,原文 Understanding Laravel Pipelines。译文首发于 深入理解 Laravel 管道,转载请注明出处。 基本上,你可以使用 larave...

    paraller 评论0 收藏0
  • Laravel 深入核心系列教程

    摘要:前言年底了不太忙,最近一段时间也一直在研究,就想写篇关于比较深一点的教程系列啥的,于是就找到站长给开了写教程的渠道。优点的就是为艺术家创造的框架,它也是工程化的趋势。项目维护方便也是事实。如果有遇到问题可以直接在教程下面留言。 前言 年底了不太忙,最近一段时间也一直在研究laravel,就想写篇关于laravel比较深一点的教程系列啥的,于是就找到站长给开了写教程的渠道。由于第一次写,...

    wemall 评论0 收藏0
  • 深入理解 Laravel Eloquent(三)——模型间关系(关联)

    摘要:是什么是一个,全称为,翻译为对象关系映射如果只把它当成数组库抽象层那就太小看它了。所谓对象,就是本文所说的模型对象关系映射,即为模型间关系。至此,深入理解系列文章到此结束。 原文发表在我的个人网站:深入理解 Laravel Eloquent(三)——模型间关系(关联) 在本篇文章中,我将跟大家一起学习 Eloquent 中最复杂也是最难理解的部分——模型间关系。官方英文文档中...

    2501207950 评论0 收藏0
  • laravel框架应用和composer扩展包开发

    摘要:官方地址是目前最流行的框架,发展势头迅猛,应用非常广泛,有丰富的扩展包可以应付你能想到的各种应用场景,框架思想前卫,跟随时代潮流,提倡优雅代码,自称为工匠,其中的模板引擎容器以及扩展包为业务的开发提供了极大的便利。 laravel5.5+ laravel官方地址 laravel是目前最流行的php框架,发展势头迅猛,应用非常广泛,有丰富的扩展包可以应付你能想到的各种应用场景,lara...

    shevy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<