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)
參考:
- IIFE:en.wikipedia.org/wiki/Immediately_...
- PHP箭頭函式:www.php.net/manual/zh/functions.ar...
- PHP array_reduce函式:www.php.net/manual/zh/function.arr...
- Laravel Pipeline:github.com/laravel/framework/tree/...
擴充套件閱讀:
本作品採用《CC 協議》,轉載必須註明作者和本文連結