深入理解Laravel中间件

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

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

  1. 使用php artisan生成一个中间件,这里假设生成一个TestMiddleware的中间件
  2. 重写TestMiddleware中的handle函数,其中代码逻辑写在return $next($request);之前或者之后表示在执行请求之前或者之后运行这段代码.
  3. app/Http/Kernel.phprouteMiddleware注册一个中间件

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

中间件实现代码

你一定听过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.这三个函数分别做了

  • 传递客户端请求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://segmentfault.com/a/1190000007715254

爱生活,爱技术

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注