深刻理解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是一片的意思,這裏這樣子的命名方式是一種比喻 : 中間件的處理過程就上面講的相似管道,處理中間件的過程比做剝洋蔥,一箇中間件的執行過程就是剝一片洋蔥.

相關文章
相關標籤/搜索