Laravel Pipeline 組件的實現

Laravel 框架中有一個很是有趣的功能,就是 HTTP 中間件,咱們在定義路由的時候,經過中間件對訪問進行過濾。來自外部的請求首先通過全局中間件,若經過,則會繼續穿過層層路由組所設置的中間件,在到達目的路由,固然,目的路由也可能定義了箇中間件,經過後,該路由的處理對象(如控制器),獲得的就是一個通過過濾的請求了。php

開始

本文固然不是討論中間件如何使用,而是其實現的基礎。Laravel 框架中有一個組件叫作 Illuminate\Pipeline,意味 「管道」,咱們看看下面這個代碼示例:數組

<?php
use Illuminate\Pipeline\Pipeline;

$pipe1 = function ($poster, Closure $next) {
    $poster += 1;
    echo "pipe1: $poster\n";
    return $next($poster);
};

$pipe2 = function ($poster, Closure $next) {
    if ($poster > 7) {
        return $poster;
    }

    $poster += 3;
    echo "pipe2: $poster\n";
    return $next($poster);
};

$pipe3 = function ($poster, Closure $next) {
    $result = $next($poster);
    echo "pipe3: $result\n";
    return $result * 2;
};

$pipe4 = function ($poster, Closure $next) {
    $poster += 2;
    echo "pipe4 : $poster\n";
    return $next($poster);
};

$pipes = [$pipe1, $pipe2, $pipe3, $pipe4];

function dispatcher($poster, $pipes)
{
    echo "result: " . (new Pipeline)->send($poster)->through($pipes)->then(function ($poster) {
            echo "received: $poster\n";
            return 3;
        }) . "\n";
}

echo "==> action 1:\n";
dispatcher(5, $pipes);
echo "==> action 2:\n";
dispatcher(7, $pipes);

上述代碼執行結果以下:閉包

==> action 1:
pipe1: 6
pipe2: 9
pipe4 : 11
received: 11
pipe3: 3
result: 6
==> action 2:
pipe1: 8
result: 8

流程概覽

Pipeline 組件實現了一個過濾流程:框架

原始數據 ---> 【前置管道】 ---> 目標處理邏輯 ---> 【後置管道】 ---> 結果數據函數

經過這種機制,能夠將目標處理邏輯與過濾、認證等機制的代碼分離開來,這樣咱們就更容易讓代碼清晰和易於維護。經過前置、後置管道,在其中 「放置」 咱們須要過濾的邏輯便可,如上述代碼,雖然只是一個簡單的示例,就已經可以看得出,整個流程的動向,譬如咱們在上面示例中準備了四個過濾組件(中間件): pipe一、pipe二、pipe三、pipe4,其中 一、二、4 是前置,3 爲後置。post

輸入的原始數據爲 5,執行過程首先經過 1 號過濾組件,而後是 2 號,再而後是 4 號,到達目標處理邏輯後,再經過 3 號過濾組件,最終輸出結果。this

輸入原始數據爲 7,一樣是先通過 1 號過濾組件,隨後是 2 號,不過在 2 號中,直接返回告終果,這意味着過程被攔截,再也不繼續向下傳遞數據,至此結束並返回結果。spa

Laravel 框架中,原始數據是一個 Request 對象,經過所定義的前置中間件,開發者可在中間件中獲取 Request 的信息,好比用戶的 Session/Cookie 以及 Header 等,驗證數據是否完備等等,不完備或不符合要求的,則被攔截並返回一個響應告知。若能正常經過則繼續傳遞至最終的處理邏輯,如控制器的某個方法或者一個匿名函數。經過這種模式,咱們就實現了請求校驗和業務邏輯的分離,並且這樣十分便於開發和維護。.net

實現

前面說這麼多,不知道讀者是否已經有一套實現的思路了沒。code

Pipeline 這個組件的功能十分明確,實現這種相似功能的確定很多,選擇其做爲表明分析,緣由就是其實現的方式很是簡潔、有力,不但其實現原理如此,面對開發人員,它的調用方式也十分清晰,利用匿名函數使得前置與後置的調用都很直觀,本文分析的重點就在這裏。

實現的思路即便有了,在沒有很好地基礎以前,估計也很難去完成。固然不少人願意去閱讀其代碼,這樣就少走了很多彎路,在這裏,個人建議也是這樣。不過,不少人看到源碼也很迷惑,由於中間存在着很是多的回調,只要基礎不夠紮實,就很容易在期間產生諸多困惑。

不過,逐步分析和對基礎知識的補完,就會發現再複雜的框架也不過是零碎的功能有序的構建起來的。

array_reduce 的妙用

public function then(Closure $destination)
{
    $firstSlice = $this->getInitialSlice($destination);
    
    $callable = array_reduce(
        array_reverse($this->pipes), $this->getSlice(), $firstSlice
    );
    
    return $callable($this->passable);
}

上面的代碼就是 Pipeline 啓動過程的起點,固然在調用 then 方法以前咱們還有必要調用 sendthroughsend 是傳遞初始數據,through 則是傳遞須要經過的中間件構成的數組,不必贅述。

then 方法接受一個要求匿名函數的參數,該參數所接受的匿名函數,就是用於整個流程的邏輯處理部分的,數據穿過層層中間件,最終到達這裏,因此該匿名函數可接受一個參數,就是通過過濾的數據啦。該方法囊括着全部功能,可是代碼不過幾行,所以確定有額外的調度過程。

代碼中首先映入眼簾的就是 $this->getInitialSlice() ,該方法顧名思義,建立了一個初始化用的 Slice,這塊咱們先不細說,由於隨後就是本文的重點,亦是組件實現的 核心功能array_reduce 函數!。

array_reduce 函數的做用文檔上寫的十分詳細,可至官方中文文檔查閱:http://php.net/manual/zh/function.array-reduce.php

經過查閱文檔,咱們可經過示例瞭解其做用本質就是經過用戶自定義的方式去將一個數組合併成單一的一個值,所以該函數要求三個參數:待合併的數組、用於合併邏輯的回調函數、初始合併的值(亦或者特殊情境下的最終值),用於合併邏輯的回調須接受兩個參數值,分別是上一次處理邏輯處理的結果(第一次不存在處理結果,則默認爲空,若設置了 array_reduce 的第三個參數,則以該參數爲初始值)和待處理的數組項。

Pipeline 組件恰到好處的使用了它。咱們看獲得,Pipeline 首先將咱們用於處理的中間件數組經過 array_reverse 取相反順序(至於爲何這麼作後面大家就知道了),傳遞至 array_reduce 的第一個參數。第三個參數做爲 array_reduce 認定的默認處理對象,Pipeline 用的是先前經過 getInitalSlice 獲取到的(其實是用戶傳進來的目標邏輯處理函數)做爲值傳遞。

而後就是本文第二個介紹的重點,array_reduce 所接受的第二個參數,經過調用 $this->getSlice() 獲取的一個匿名函數!

實現的核心

array_reduce 的第二個參數要求傳遞一個回調函數用於處理數組合並,$this->getSlice() 返回的正是這個處理函數,我相信大家必定看到了 getSlice 返回的值,那麼我就將這個匿名函數單獨拿出來:

function ($stack, $pipe) {
    return function ($passable) use ($stack, $pipe) {
        if ($pipe instanceof Closure) {
            return call_user_func($pipe, $passable, $stack);
        }
        // 省略了一部分,該部分是針對中間件 「類」 而不是中間件匿名函數的,
        // 先前例子中咱們用的都是以匿名函數做爲數組傳遞進來的,所以只會進入上面那個條件,
        // 固然 Laravel 框架中,傳遞進來的則基本是中間件對象的類名,這段省略的代碼,
        // 和上面那個 if 中的本質的區別就是,省略的代碼中包含了中間件類的實例化過程並調用的是
        // 其 handle 方法而不是直接調用函數,僅此~~
    };
};

我知道你們看到的代碼有不少行,可是實際上就只有一行 return function() { ... };,被執行的也只有它。對於一些初學者,很容易產生一種錯覺:那個返回的 function 會在 return 前執行。既然是錯覺,那就意味着不會被執行,而是做爲一個值被返回,可能會被後續某個地方所調用!可能會被後續某個地方所調用!可能會被後續某個地方所調用!這裏只是個值!重要的事情說三遍。

雖然說會被後面所調用,但咱們依舊要在這裏提一下這個被返回的匿名函數,在這裏,它又有着另外一個名稱:閉包。閉包是由匿名函數(也成閉包函數)構成的一個總體,和普通的匿名函數有所不一樣,閉包中必定存在引用了外部數據並在內部操做的狀況。

這裏須要注意,返回的不只僅是個匿名函數,更是一個閉包,該閉包中引用了兩個外部值,分別是 array_reduce 提供給第二參數中的回調的兩個參數,即數組合並結果和當前待合併的值。

第一次執行時,$stack 就是咱們的目標處理邏輯代碼段,$pipe 則是第一個中間件;

第二次執行時,$stack 是第一次執行所返回的閉包,$pipe 則是第二個中間件,隨後以此類推。

最後一次執行,返回的結果仍舊是一個閉包,該閉包中所引用的外部數據是倒數第二次的執行返回的閉包,$pipe 是最後一箇中間件。隨後,該閉包在 then 方法中被調用,傳遞進了咱們經過 send 方法傳遞的值。

上面的描述可能異常抽象,咱們讓其變得稍微直觀一些,我會將全部遍歷每一次執行帶來的變化體現出來。不過爲了方便理解,我須要改一下示例代碼,去掉中間的條件判斷,由於咱們如今重點是理解這個流程而不是其功能,新的代碼與執行結果以下:

<?php
use Illuminate\Pipeline\Pipeline;

$pipes = [
    function ($poster, $callback) {
        $poster += 1;
        return $callback($poster);
    },
    function ($poster, $callback) {
        $result = $callback($poster);

        return $result - 1;
    },
    function ($poster, $callback) {
        $poster += 2;

        return $callback($poster);
    }
];

echo (new Pipeline)->send(0)->through($pipes)->then(function ($poster) {
    return $poster;
}); // 執行輸出爲 2

上述代碼,咱們定義了三個中間件,同時咱們的目標邏輯代碼並沒作什麼特殊的事情,這樣咱們就能夠專一在執行流程上。下面便於分析,我作了一份僞代碼以及等式方便理解:

poster     = 0
f^0        = f(z)->{ z }                     // 定義目標處理邏輯
f^1        = f(z, y)->{ f^y( z + 1 ) }       // 定義中間件 1
f^2        = f(z, y)->{ result = f^y(z); result - 1 }  // 定義中間件 2
f^3        = f(z, y)->{ f^y( z + 2 ) }       // 定義中間件 3
f^getSlice = f(y, x)->{
    f(z)->{
        call( f^x(z, y) )
    }
}

callback = array_reduce([f^3, f^2, f^1], f^getSlice, f^0);
callback(poster)

>>> 執行上述過程

exec^1:
    // 第一次進行 reduce,y 是目標邏輯片斷,x 是最後一箇中間件,被閉包引用,
    // 閉包則做爲合併結果返回,在此定義爲 f^a。
    y   = f^0(z);
    x   = f^3;
    f^a = f(z)->{ call( f^x(z, y) ) }
exec^2:
    // 第二次進行,y 是上次處理返回的閉包(即 f^a),x 是第二個中間件,再次生成閉包返回。
    y   = f^a;
    x   = f^2;
    f^b = f(z)->{ call( f^x(z, y) ) }
exec^3:
    // 第三次也是最後一次合併,同第二次。如今三個數組項被合併,
    // 合併結果爲最後一次合併所返回的閉包。
    y   = f^b;
    x   = f^1;
    f^c = f(z)->{ call( f^x(z, y) ) }
exec^4:
    // 該閉包(最後一次合併結果)返回後,被調用,第一個參數爲 z = poster = 1,開始執行。
    // 該閉包的 z 參數即爲 1,其他如 x、y 值見 exec^3。
    call( f^c(0) ) = call( f^1(0, f^b) )
exec^5:
    // 繼續等式替換
    call( f^b(0 + 1) ) = call( f^2(0 + 1, f^a) )
exec^6:
    // 根據上已執行過程返回結果,已執行至中間件 2 的回調,繼續等式替換
    result = f^a(0 + 1); result - 1
exec^7:
    result = call( f^3(0 + 1 , f^0) ); result - 1
exec^8:
    result = call( f^0(0 + 1 + 2) ); result - 1
exec^9:
    result = 3; result - 1

// 處理結果
result: 2

分析

根據僞代碼,和執行過程,咱們能瞭解到先前經過 array_reverse 反序排列的中間件,因爲在本文中,此處閉包逆向傳遞下去的特性(由於所引用的外部參數中,是前一執行結果所返回的閉包),實際上依舊是按順序執行的,咱們在這裏也看到了如何利用該特性,實現前置和後置調用的原理以及攔截的原理。

前置調用時,先處理自上傳遞下來的結果,隨後調用下一個(由中間件構成的)閉包。後置調用時,先調用下一個(有中間件構成的)閉包,裏面仍舊可能無數的引用,直到其中的目標處理邏輯,最終返回結果,再處理。

攔截的原理就更簡單了,因爲攔截只存在於前置中間件,而前置中間件是先處理,而後調用傳遞進來的閉包並返回其值,而若這個值不是來自於一個閉包調用的結果,就意味着確定中間不存在調用關係,也就根本不會執行到閉包中的下一個中間件。

總結

以上就是整個 Pipeline 以及中間件的實現,我知道不少人依舊十分糾結,心裏充滿困惑。我仍舊建議老老實實,從 array_reduce 這個函數的實際功能着手,而後把每一步執行過程,寫下來,慢慢的就明白了。這篇文章不只僅只是 Laravel 組件的一個講解,更可能是從中發現 PHP 的一些基礎概念和知識,要知道在強大的 PHP 框架也是用 PHP 寫出的,本質上仍舊是在一個大的基礎上構建的小世界而已。

因此做爲一名 PHPer,永遠不要忘了,你是在寫 PHP 的代碼。

個人博客地址:https://www.insp.top

相關文章
相關標籤/搜索