ThinkPHP 6.0 管道模式與中介軟體的實現分析

tsin發表於2019-10-31

說明

ThinkPHP 6.0 RC5 開始使用了管道模式來實現中介軟體,比起之前版本的實現更加簡潔、有序。這篇文章對其實現細節進行分析。

首先我們從入口檔案public/index.php開始,$http = (new App())->http;
獲得一個http類的例項後呼叫它的run方法:$response = $http->run();,然後它的run方法又呼叫了runWithRequest方法:

protected function runWithRequest(Request $request)
{
    .
    .
    .

    return $this->app->middleware->pipeline()
        ->send($request)
        ->then(function ($request) {
            return $this->dispatchToRoute($request);
        });
}

中介軟體的執行都在最後的return語句中。

pipeline、through、send方法

$this->app->middleware->pipeline()pipeline方法:

public function pipeline(string $type = 'global')
{
    return (new Pipeline())  
           // array_map將所有中介軟體轉換成閉包,閉包的特點:
          // 1. 傳入引數:$request,請求例項; $next,一個閉包
          // 2. 返回一個Response例項
        ->through(array_map(function ($middleware) {
            return function ($request, $next) use ($middleware) {
                list($call, $param) = $middleware;
                if (is_array($call) && is_string($call[0])) {
                    $call = [$this->app->make($call[0]), $call[1]];
                }
                 // 該語句執行中介軟體類例項的handle方法,傳入的引數是外部傳進來的$request和$next
                 // 還有一個$param是中介軟體接收的引數
                $response = call_user_func($call, $request, $next, $param);

                if (!$response instanceof Response) {
                    throw new LogicException('The middleware must return Response instance');
                }
                return $response;
            };
            // 將中介軟體排序
        }, $this->sortMiddleware($this->queue[$type] ?? [])))
        ->whenException([$this, 'handleException']);
}

through方法程式碼:

public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();
    return $this;
}

前面呼叫through是傳入的array_map(...)把中介軟體封裝為一個個閉包,through則是把這些閉包儲存在Pipeline類的$pipes屬性中。

PHP的array_map方法簽名:

array_map ( callable $callback , array $array1 [, array $... ] ) : array

$callback迭代作用於每一個 $array的元素,返回新的值。所以,最後得到$pipes中每個閉包的形式特徵是這樣的(虛擬碼):

function ($request, $next) {
    $response = handle($request, $next, $param);
    return $response;
}

該閉包接收兩個引數,一個是請求例項,一個是回撥用函式,handle方法處理後得到相應並返回。

through返回一個Pipeline類的例項,接著呼叫send方法:

public function send($passable)
{
    $this->passable = $passable;
    return $this;
}

該方法很簡單,只是將傳入的請求例項儲存在$passable成員變數,最後同樣返回Pipeline類的例項,這樣就可以鏈式呼叫Pipeline類的其他方法。

then,carry方法

send方法之後,接著呼叫then方法:

return $this->app->middleware->pipeline()
            ->send($request)
            ->then(function ($request) {
                return $this->dispatchToRoute($request);
            });

這裡的then接收一個閉包作為引數,這個閉包實際上包含了控制器操作的執行程式碼。
then方法程式碼:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        //用於迭代的陣列(中介軟體閉包),這裡將其倒序
        array_reverse($this->pipes),
        // array_reduce需要的回撥函式
        $this->carry(),
        //這裡是迭代的初始值
        function ($passable) use ($destination) {
            try {
                return $destination($passable);
            } catch (Throwable | Exception $e) {
                return $this->handleException($passable, $e);
            }
        });

    return $pipeline($this->passable);
}

carry程式碼:

protected function carry()
{
    // 1. $stack 上次迭代得到的值,如果是第一次迭代,其值是後面的「初始值
    // 2. $pipe 本次迭代的值
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            try {
                return $pipe($passable, $stack);
            } catch (Throwable | Exception $e) {
                return $this->handleException($passable, $e);
            }
        };
    };
}

為了更方便分析原理,我們把carry方法內聯到then中去,並去掉錯誤捕獲的程式碼,得到:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes),
        function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                return $pipe($passable, $stack);
            };
        },
        function ($passable) use ($destination) {
            return $destination($passable);
        });

    return $pipeline($this->passable);
}

這裡關鍵是理解array_reduce以及$pipeline($this->passable)的執行過程,這兩個過程可以類比於「包洋蔥」和「剝洋蔥」的過程。
array_reduce第一次迭代,$stack初始值為:
(A)

function ($passable) use ($destination) {
    return $destination($passable);
});

回撥函式的返回值為:
(B)

function ($passable) use ($stack, $pipe) {
    return $pipe($passable, $stack);
};

將A代入B可以得到第一次迭代之後的$stack的值:
(C)

function ($passable) use ($stack, $pipe) {
    return $pipe($passable, 
        function ($passable) use ($destination) {
            return $destination($passable);
        })
    );
};

第二次迭代,同理,將C代入B可得:
(D)

// 虛擬碼
// 每一層的$pipe都代表一箇中介軟體閉包
function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  //倒數第二層中介軟體
        function ($passable) use ($stack, $pipe) {
            return $pipe($passable,  //倒數第一層中介軟體
                function ($passable) use ($destination) {
                    return $destination($passable);  //包含控制器操作的閉包
                })
            );
        };
    );
};

以此類推,有多少箇中介軟體,就代入多少次,最後一次得到$stack就返回給$pipeline。由於前面對中介軟體閉包進行了倒序,排在前面的閉包被包裹在更裡層,所以倒序後的閉包越是後面的在外面,從正序來看,則變成越前面的中介軟體在最外層。

層層包裹好閉包後,我們得到了一個類似洋蔥結構的「超級」閉包D,該閉包的結構如上面的程式碼註釋所示。最後把$request物件傳給這個閉包,執行它:$pipeline($this->passable);,由此開啟一個類似剝洋蔥的過程,接下來我們看看這洋蔥是怎麼剝開的。

剝洋蔥過程分析

回顧上文,array_map(...)把每一箇中介軟體類加工成一個類似這種結構的閉包:

function ($request, $next) {
    $response = handle($request, $next, $param);
    return $response;
}

其中handle是中介軟體中的入口,其結構特點是這樣的:

public function handle($request, $next, $param) {
    // do sth ------ M1-1 / M2-1
    $response = $next($request);
    // do sth ------ M1-2 / M2-2
    return $response;
}

我們上面的「洋蔥」一共只有兩層,也就是有兩層中介軟體的閉包,假設M1-1,M1-2分別是第一個中介軟體handle方法的前置和後值操作點位,第二個中介軟體同理,是M2-1,M2-2。現在,讓程式執行$pipeline($this->passable),展開來看,也就是執行:

// 虛擬碼
function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($stack, $pipe) {
            return $pipe($passable,  
                function ($passable) use ($destination) {
                    return $destination($passable);  
                })
            );
        };
    );
}($this->passable)

此時,程式要求從:

return $pipe($passable,  
    function ($passable) use ($stack, $pipe) {
        return $pipe($passable,  
            function ($passable) use ($destination) {
                return $destination($passable);  
            })
        );
    };
);

返回值,也就是要執行第一個中介軟體閉包,$passable對應handle方法的$request引數,而下一層閉包

function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($destination) {
            return $destination($passable);  
        })
    );
}

則對應handle方法的$next引數。
要執行第一個閉包,即要執行第一個閉包的handle方法,其過程是:首先執行M1-1點位的程式碼,即前置操作,然後執行$response = $next($request);,這時程式進入執行下一個閉包,$next($request)展開來,也就是:

function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($destination) {
            return $destination($passable);  
        })
    );
}($request)

依次類推,執行該閉包,即執行第二個中介軟體的handle方法,此時,先執行M2-1點位,然後執行$response = $next($request),此時的$next閉包是:

function ($passable) use ($destination) {
    return $destination($passable);  
})

屬於洋蔥之芯——最裡面的一層,也就是包含控制器操作的閉包,展開來看:

function ($passable) use ($destination) {
    return $destination($passable);  
})($request)

最終,我們從return $destination($passable)中返回一個Response類的例項,也就是,第二層的$response = $next($request)語句成功得到了結果,接著執行下面的語句,也就是M2-2點位,最後第二層閉包返回結果,也就是第一層閉包的$response = $next($request)語句成功得到了結果,然後執行這一層閉包該語句後面的語句,即M1-2點位,該點位之後,第一層閉包也成功返回結果,於是,then方法最終得到了返回結果。

整個過程過來,程式經過的點位順序是這樣的:M1-1→M2-1→控制器操作→M2-2→M1-2→返回結果。

總結

整個過程看起來雖然複雜,但不管中介軟體有多少層,只要理解了前後兩層中介軟體的這種遞推關係,洋蔥是怎麼一層層剝開又一層層返回的,來多少層都不在話下。

Was mich nicht umbringt, macht mich stärker

相關文章