理解Laravel中介軟體核心實現原理

woodong發表於2022-09-20

Laravel路由中介軟體是一個洋蔥模型,http請求會從第一個中介軟體經過第二個中介軟體、第三個中介軟體,最後到達控制器(一般情況下),然後再從第三個中介軟體返回至第二個中介軟體,再返回至第一個中介軟體,整個路線像洋蔥一樣一層一層的。

圖片

我們將其模型和功能簡單化,將每一箇中介軟體類中的handle方法看作是一個普通的方法,然後先理解洋蔥模型的左半部分。這非常簡單,假如有方法f1、f2、f3,其左邊的呼叫順序就是f1->f2->f3,即f3(f2(f1())),我們換一種程式碼形態:

$run = function() {   // 將匿名方法賦值給變數run,當執行 $run() 時,相當於呼叫了這一層方法,返回的是f1方法
    return function () {  // 相當於f1,當執行 $run()() 時,相當於呼叫了這一層方法,輸出f1,返回的是f2方法
        echo 'f1';
        return function () {// 相當於f2,當執行 $run()()() 時,相當於呼叫了這一層方法,輸出f2,返回的是f3方法
            echo 'f2';
            return function () { // 相當於f3,當執行 $run()()()() 時,相當於呼叫了這一層方法,輸出f3,沒有返回值
                echo 'f3';
            };
        };
    };
};

$run()()()(); // 輸出:f1 f2 f3

(示例1)

假如有N個方法,難道要$run()()()()...N;?這個問題可以透過以下程式碼結構解決:

$run = function() {              // 將匿名方法賦值給變數run
    return (function() {         // 相當於f1,自動呼叫,輸出f1並返回撥用結果
        echo 'f1';
        return (function() {     // 相當於f2,自動呼叫,輸出f2並返回撥用結果
            echo 'f2';
            return (function() { // 相當於f3,自動呼叫,輸出f3,沒有返回值
                echo 'f3';
            })();
        })();
    })();
};

$run(); // 輸出:f1 f2 f3

(示例2)

注:PHP7+版本,匿名方法自動呼叫方式(IIFE):(function(){})();php7以下版本:call_user_func(function(){})

上面兩段程式碼展示了洋蔥模型的左半部分,相信你已經知道右半部分怎麼實現了,沒錯就是這樣:

$run = function() {
    return (function() {
        echo 'f1-left';
        $result = (function() {
            echo 'f2-left';
            $result = (function() {
                echo 'f3';
            })();

            echo 'f2-right';
            return $result;
        })();

        echo 'f1-right';
        return $result;
    })();
};

$run(); // 輸出:f1-left f2-left f3 f2-right f1-right

(示例3)

我們再實現一個傳參版的:

$run = function(array $arr) {
    return (function(array $arr) {
        $arr[] = 'f1-left';
        $arr = (function(array $arr) {
            $arr[] = 'f2-left';
            $arr = (function(array $arr) {
                $arr[] = 'f3';
                return $arr;
            })($arr);

            $arr[] = 'f2-right';
            return $arr;
        })($arr);

        $arr[] = 'f1-right';
        return $arr;
    })($arr);
};

$arr = $run(['start']);
$arr[] = 'end';
print_r($arr); // 輸出:
/*
Array
(
   [0] => start
   [1] => f1-left
   [2] => f2-left
   [3] => f3
   [4] => f2-right
   [5] => f1-right
   [6] => end
)
*/

(示例4)

上面的例子都是展開的程式碼來展示原理,那我們怎麼樣才能實現下面這樣靈活的呼叫方式呢?

$arr = (new Pipeline(['f1', 'f2', 'f3', ...]))->run(['start']);
$arr[] = 'end';
// 定一個 Pipeline 類
class Pipeline
{
    // 存放f1 f2 f3...N的方法名
    protected $pipes = [];

    public function __construct(array $pipes)
    {
        $this->pipes = $pipes;
    }

    public function run($data)
    {
        // 需定義一個“芯”,這樣就f1 f2 f3都可以使用統一的引數,就不需要指定f3作為最中間的芯了
        $stack = function($data) {
            return $data;
        };

        // php7.4可以這樣寫: $stack = fn($data) => $data;

        // f1 f2 f3 變成 f3 f2 f1,因為需要先從“洋蔥芯”開始包裝
        $pipes = array_reverse($this->pipes);

        // 迴圈包裝每一個方法
        foreach ($pipes as $pipe) {
            // 每次迴圈,$stack的層級都會增加
            $stack = function ($data) use($pipe, $stack) {
                return $pipe($data, $stack);
            };
        }

        // 相當於上面例子中的 $run();
        return $stack($data);
    }
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法
 *
 * @return mixed
 */
function f1(array $arr, \Closure $next)
{
    $arr[] = 'f1-left';
    $arr = $next($arr);
    $arr[] = 'f1-right';
    return $arr;
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法
 *
 * @return mixed
 */
function f2(array $arr, \Closure $next)
{
    $arr[] = 'f2-left';
    $arr = $next($arr);
    $arr[] = 'f2-right';
    return $arr;
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法,這裡比上面例子中的f3多了一個引數,因為Pipeline::run中定義了一個“芯”
 *
 * @return mixed
 */
function f3(array $arr, \Closure $next)
{
    $arr[] = 'f3';
    $arr = $next($arr);
    return $arr;
}

$arr = (new Pipeline(['f1', 'f2', 'f3']))->run(['start']);
$arr[] = 'end';
print_r($arr);  // 輸出:
/*
Array
(
    [0] => start
    [1] => f1-left
    [2] => f2-left
    [3] => f3
    [4] => f2-right
    [5] => f1-right
    [6] => end
)
*/

(示例5)

比較難理解的是foreach部分,我們再分解一下,注意觀察一下$stack的值的變化:

// 第0次迴圈
$stack = function($data) {
    return $data;
};

// 迴圈每一個方法
$pipes = ['f3', 'f2', 'f1'];
foreach ($pipes as $pipe) {
    // 每次迴圈,$stack的層級都會增加
    $stack = function ($data) use($pipe, $stack) {
        return $pipe($data, $stack);
    };

    // 第1次迴圈
    $stack = function($data) {
        return f3($data, function($data) {
            return $data;
        });
    };

    // 第2次迴圈
    $stack = function($data) {
        return f2($data, function ($data) {
            return f3($data, function($data) {
                return $data;
            });
        });
    };

    // 第3次迴圈
    $stack = function($data) {
        return f1($data, function ($data) {
            return f2($data, function ($data) {
                return f3($data, function($data) {
                    return $data;
                });
            });
        });
    };
}

(示例6)

透過示例6的三次迴圈可以看出,剛好和示例4相吻合。到這裡我們透過上面的幾個示例可以明白Laravel路由中介軟體的核心實現原理。其實程式碼還可以使用array_reduce函式來代替示例5中的foreach,laravel中就是使用的這個方法

public function run($data)
{
    return array_reduce(array_reverse($this->pipes), function ($stack, $item) {
        return function ($data) use($item, $stack) {
            return $item($data, $stack);
        };
    }, fn($data) => $data)($data);
}

(示例7)

參考:

擴充套件閱讀:

原文:github.com/woodongwong/notes/issue...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章