ThinkPHP6 原始碼閱讀(七):中介軟體的執行

tsin發表於2019-08-18

說明

接上篇,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方法主要操作時註冊控制器中介軟體和執行控制器操作,具體過程等程式真正調到再作分析。新增閉包到中間見後,中介軟體例項大概是這樣子的:

ThinkPHP6 原始碼閱讀(七):執行中介軟體和控制器

從上圖可以看出,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物件呼叫。Middlewarerevolve方法:

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.phpm2.phpm3.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

執行過程示意圖:

ThinkPHP6 原始碼閱讀(七):中介軟體的執行

參考

Was mich nicht umbringt, macht mich stärker

相關文章