PHP 框架中介軟體實現

一隻賤熊貓發表於2019-03-04

0x00 前言

中介軟體是很多 PHP 框架都提供的功能,在初次認識它的時候我感到驚訝和興奮。因為它的作用太強大了,在沒有中介軟體之前我們不得不將許可權驗證和一些公共操作都寫在控制器方法裡,然後控制器就會變得很臃腫,降低了可讀性和可維護性。但有了中介軟體我們就可以這些操作都寫在中介軟體裡,然後通過使用不同的中介軟體組合不僅能夠實現需求還降低了程式碼的耦合度。既然中介軟體百般好,那它到底是如何實現的呢?在閱讀 LaravelSlim 的原始碼過程中(一個讓人感覺很費勁,很折磨,覺得自己很菜的受虐過程/(ㄒoㄒ)/~~),我發現其重點就是要將多箇中介軟體閉包(有些框架中介軟體並不是通過閉包實現但都屬於 callable 的範疇,為了行文方便統稱為閉包)通過 array_reduce 或迴圈的方式將其打包成為一個閉包的過程。

0x01 預熱

一說道中介軟體往往就會讓人聯想到這幅圖

middleware

看起來很神奇很嚇人的樣子但是仔細觀察一下,其實這個流程其實像不像函式的巢狀呼叫 Middleware2(Middleware1(App())) 呢?不過這樣巢狀呼叫顯然是錯的,因為 PHP 先執行了 App() 而不是 Middleware2 但是如果比作巢狀的閉包呢?下面給出示例程式碼:

$allMiddleware = function () {
    echo 'start middleware2' . PHP_EOL;
  
    (function () {
        echo 'start middleware1' . PHP_EOL;
        // app
        (function () {
            echo 'app' . PHP_EOL;
        })();
        // end app
        echo 'end middleware1' . PHP_EOL;
    })();
  
    echo 'end middleware2' . PHP_EOL;
};
$allMiddleware();

// 輸出
// start middleware2
// start middleware1
// app
// end middleware1
// end middleware2
複製程式碼

0x02 思考

嘿,上面的程式碼結果和預期一樣,但這程式碼似乎有點簡(ruo)陋(zhi)。但是其實上面程式碼中的 $allMiddleware 就是中介軟體組合之後的結果,所以我們已經摸到門檻了,現在請思考如何將下列中介軟體閉包自動組合?

// 資料庫中介軟體
$db = function (Closure $next) {
    echo '成功建立資料庫連線' . PHP_EOL;
    $next();
    echo '成功關閉資料庫連線' . PHP_EOL;
};
// 點贊中介軟體
$like = function (Closure $next) {
    echo '點贊+1' . PHP_EOL;
    $next();
    echo '點贊+2' . PHP_EOL;
};
// 內容閉包
$app = function () {
    echo '文章內容' . PHP_EOL;
};
複製程式碼

多了個引數 $next 是什麼鬼? $next 應該理解為「本中介軟體後所有中介軟體閉包函式打包成的一個閉包」,就以 Middleware2 而言,它的 $next 就是 Middleware1APP 打包成的閉包(參照上圖的最裡面兩層)。

0x03 解答

現給出答案程式碼如下:

// array_reduce 實現
$allMiddleware = [$like, $db];
$go = array_reduce($allMiddleware, function ($next, $middleware) {
    return function () use ($next, $middleware) {
        $middleware($next);
    };
}, $app);
$go();

// foreach 實現
$allMiddleware = [$like, $db];
$next = $app;
foreach ($allMiddleware as $middleware) {
    $next = function () use ($next, $middleware) {
        return $middleware($next);
    };
}
$next();
複製程式碼

兩種實現方式但原理一樣,所以只解釋 foreach 版本的實現。首先是將所有的中介軟體組成一個陣列,並將 $next 設定為 $app,然後開始迴圈的將 $nextmiddleware 組成一個新的閉包並賦值給 $next,這樣 $next 便不斷的將之前的閉包合併,最後變成一個。然後通過執行最後的 $next 即可得出結果。當然我空口白話的說可能還不好理解,最好的方式是將這程式碼自己在大腦執行一遍,還不行就動手畫一下(我第一次看到這個程式碼就是這樣才懂的/(ㄒoㄒ)/~~)然後就可以明白了。 此外不清楚 array_reduce 函式作用的可以 點此

0x04 好點的版本

為了好理解前面的中介軟體就只有 $next 引數,但實際框架的中介軟體都會有類似 $request 引數並且支援返回值 $response(比如 Laravel),下面是一個類似 Laravel 的中介軟體(僅僅是模仿,照虎畫貓)實現程式碼。一法通,萬法通,本菜逼就不解釋了(萬一涉及到我的知識盲區就 GG 了,O(∩_∩)O哈哈~)。

$db = function ($request, Closure $next) {
    echo '成功建立資料庫連線' . PHP_EOL;
    $response = $next($request);
    echo '成功關閉資料庫連線' . PHP_EOL;

    return $response;
};

$like = function ($request, Closure $next) {
    echo '點贊+1' . PHP_EOL;
    $response = $next($request);
    echo '點贊+2' . PHP_EOL;

    return $response;
};

$app = function ($request) {
    echo $request . PHP_EOL;
    return '一個無聊的返回值';
};

$allMiddleware = [$like, $db];
$next = $app;
foreach ($allMiddleware as $middleware) {
    $next = function ($request) use ($middleware, $next) {
        return $middleware($request, $next);
    };
}

$response = $next('O(∩_∩)O');
echo $response;
複製程式碼

0x05 總結

(⊙v⊙)嗯,一點點內容就水了這麼多,心滿意足O(∩_∩)O哈哈~。另外文章中若出現錯誤,希望大家能夠指出,若有疑問可以互相討論:-D。

我的部落格原文

相關文章