說明
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→返回結果。
總結
整個過程看起來雖然複雜,但不管中介軟體有多少層,只要理解了前後兩層中介軟體的這種遞推關係,洋蔥是怎麼一層層剝開又一層層返回的,來多少層都不在話下。