說明
接上篇,runWithRequest
方法最後呼叫的dispatch
方法還沒有分析完,這裡接著分析該方法後面部分,程式碼如下:
public function dispatch(Request $request, $withRoute = null)
{
.
.
.
} else {
//如果沒有開啟路由,將執行這裡的語句
//$this->path()得到PATHINFO,比如/demo/hello
$dispatch = $this->url($this->path());
}
// $dispatch是think\route\dispatch\Url的例項,該類繼承了Controller類
// 且該類中沒有init方法,所以這裡執行的是其父類的init方法
// init方法主要解析出了控制器名和操作名
$dispatch->init($this->app);
// 將一個閉包註冊為中介軟體
// 該閉包呼叫了think\route\dispatch\Url類的run方法,返回一個response
$this->app->middleware->add(function () use ($dispatch) {
try {
$response = $dispatch->run();
} catch (HttpResponseException $exception) {
$response = $exception->getResponse();
}
return $response;
});
return $this->app->middleware->dispatch($request);
}
解析控制器名和操作名
Url解析之後,接下來執行$dispatch->init($this->app)
,執行分析參見以上程式碼註釋。init
方法及註釋分析如下:
public function init(App $app)
{
//父類的init呼叫了doRouteAfter方法
//其操作有;新增中介軟體,新增路由引數,繫結模型資料
// 記錄當前請求的路由規則,路由變數
parent::init($app);
// ["demo", "hello"]
$result = $this->dispatch;
if (is_string($result)) {
$result = explode('/', $result);
}
// 獲取控制器名
// "demo"
// 如果$result[0]為空,則使用預設控制器
$controller = strip_tags($result[0] ?: $this->rule->config('default_controller'));
// 如果控制器名稱中有點號
// 也就是多級控制器解析
// 比如,控制器類的檔案位置為app/index/controller/user/Blog.php
// 訪問地址可以使用:http://serverName/index.php/user.blog/index
// 官方文件建議使用路由,避免點號後面部分被識別為字尾
if (strpos($controller, '.')) {
$pos = strrpos($controller, '.');
//substr($controller, 0, $pos)為點號前面部分
//Str::studly:下劃線轉駝峰(首字母大寫)
$this->controller = substr($controller, 0, $pos) . '.' . Str::studly(substr($controller, $pos + 1));
} else {
$this->controller = Str::studly($controller);
}
// 獲取操作名
$this->actionName = strip_tags($result[1] ?: $this->rule->config('default_action'));
// 設定當前請求的控制器、操作
$this->request
->setController($this->controller)
->setAction($this->actionName);
}
注意該方法檔案位置: \vendor\topthink\framework\src\think\route\dispatch\Controller.php
。
將控制器操作新增到中介軟體
程式接著新增一個閉包到中介軟體,閉包裡面主要操作時呼叫了一個run
方法。這個方法藏得比較深,查詢過程如下:呼叫它的類think\route\dispatch\Url
並沒有run
方法,向其父類think\route\dispatch\Controller
查詢,也沒有,再往Controller
類的父類think\route\Dispatch
查詢,最後發現這個方法就位於這個類之中。run
方法主要操作時註冊控制器中介軟體和執行控制器操作,具體過程等程式真正調到再作分析。新增閉包到中間見後,中介軟體例項大概是這樣子的:
從上圖可以看出,route
型別中介軟體下,一共有三個中介軟體,前兩個是從app/middleware.php
載入進來的(之前配置的),最後一個是現在新增的。
中介軟體排程
接著來到dispatch
方法的最後一步:return $this->app->middleware->dispatch($request);
,獲取一箇中介軟體物件,然後呼叫中介軟體類的dispatch
方法,傳入的引數是一個think\Request
物件。dispatch
程式碼如下:
public function dispatch(Request $request, string $type = 'route')
{
//$this->resolve($type)是一個閉包\
//這裡執行一個閉包,傳入的引數為一個Request物件\
//這個閉包是一個多層巢狀的閉包
return call_user_func($this->resolve($type), $request);
}
實際是使用$this->resolve($type)
解析得到方法名,再傳入Request
物件呼叫。Middleware
類revolve
方法:
protected function resolve(string $type = 'route')
{
return function (Request $request) use ($type) {
// 從佇列中第一個位置刪除取出一個繫結的中介軟體
$middleware = array_shift($this->queue[$type]);
// 已沒有中介軟體,結束該方法
// 也就是遞迴終止條件
if (null === $middleware) {
throw new InvalidArgumentException('The queue was exhausted, with no response returned');
}
// 獲取中介軟體類及其處理函式、中介軟體引數
// 比如,$call 為:
//Array
//(
// [0] => think\middleware\LoadLangPack
// [1] => handle
//)
list($call, $param) = $middleware;
if (is_array($call) && is_string($call[0])) {
// 例項化
// 比如
// Array
//(
// [0] => think\middleware\LoadLangPack Object
// (
// )
//
// [1] => handle
//)
$call = [$this->app->make($call[0]), $call[1]];
}
try {
// 這裡遞迴呼叫「resovle」
$response = $this->app->invoke($call, [$request, $this->resolve($type), $param]);
} catch (HttpResponseException $exception) {
$response = $exception->getResponse();
}
if (!$response instanceof Response) {
throw new LogicException('The middleware must return Response instance');
}
return $response;
};
}
這個方法可能是分析到現在為止最複雜的了,它返回一個閉包,閉包中,又呼叫了自身,形成一個遞迴。假如先後載入了M1,M2,M3三個中介軟體,其執行順序是:執行M1→執行M2→執行M3→返回M3→返回M2→返回M1,整個過程像是橫穿過一個洋蔥。
舉個例子
為了更好理解中介軟體的執行順序,這裡舉一個例子演示一下。
首先,命令列依次執行以下程式碼,生成三個中介軟體:
php think make:middleware m1
php think make:middleware m2
php think make:middleware m3
這些操作會在app/middleware
資料夾下生成三個檔案,分別是 m1.php
,m2.php
,m3.php
。接著在這三個檔案的handle
方法都填充以下程式碼:
// 當前呼叫的類名
$class = __CLASS__;
// 前置執行邏輯
echo "我在".$class."前置行為中<br>";
$response = $next($request);
//後置執行 後置執行邏輯
echo "我在".$class."後置行為中<br>";
return $response;
最後,編輯app
目錄下的middleware.php
,新增以上三個中介軟體,程式碼如下:
return [
\app\middleware\m1::class,
\app\middleware\m2::class,
\app\middleware\m3::class,
];
同時,修改下Demo
控制器的Hello
方法,程式碼如下:
public function hello($name = 'ThinkPHP6')
{
echo "這裡是Demo控制器的Hello方法<br>";
return 'hello,' . $name;
}
以上程式碼準備好了,我們就可以通過瀏覽器訪問Demo
控制器的Hello
方法執行到以上程式碼,程式執行結果如下:
我在app\middleware\m1前置行為中
我在app\middleware\m2前置行為中
我在app\middleware\m3前置行為中
這裡是Demo控制器的Hello方法
我在app\middleware\m3後置行為中
我在app\middleware\m2後置行為中
我在app\middleware\m1後置行為中
hello,ThinkPHP6
執行過程示意圖: