深刻理解Laravel中间件

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

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

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

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

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

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

中间件实现代码

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

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

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

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

相关文章
相关标签/搜索