ThinkPHP6 原始碼閱讀(八):控制器操作的執行

tsin發表於2019-08-18

說明

從前面的分析可以看到,控制器的呼叫被包裹在一個閉包裡面,然後新增到中介軟體後面,所以執行控制器之前會先執行其前面的中介軟體,然後,返回響應物件後,再以相反的方向將響應返回給上一層中介軟體,直到最外面一層中介軟體,最後將最終的響應返回。控制器相關的閉包程式碼如下:

$this->app->middleware->add(function () use ($dispatch) {
        try {
            $response = $dispatch->run();
        } catch (HttpResponseException $exception) {
            $response = $exception->getResponse();
        }
        return $response;
    });

下面分析程式如何到達這裡,並執行該閉包。

從中介軟體到控制器

整個過程關鍵的一個語句是:Middleware類的resolve方法中的$response = $this->app->invoke($call, [$request, $this->resolve($type), $param]);invoke方法的引數又包含了resolve方法自身,形成一個遞迴過程,這樣,程式就會先按已經載入進來的中介軟體,一層一層「resolve」下去,到會後一層,遞迴結束,再一層一層返回響應物件。

以上篇的例子為例,程式執行完m1,m2,m3中介軟體後,將接著執行控制器對應的中介軟體。我們從invoke方法開始分析。invoke方法程式碼如下:

public function invoke($callable, array $vars = [], bool $accessible = false)
{
    // 如果$callable是閉包
    if ($callable instanceof Closure) {
        return $this->invokeFunction($callable, $vars);
    }
    // $callable不是閉包的情況
    return $this->invokeMethod($callable, $vars, $accessible);
}

因為控制器對應的中介軟體是一個閉包,所以這裡會執行return $this->invokeFunction($callable, $vars);,其中,invokeFunction程式碼如下:

public function invokeFunction($function, array $vars = [])
{
    try {
        $reflect = new ReflectionFunction($function);

        $args = $this->bindParams($reflect, $vars);

        if ($reflect->isClosure()) {
            // 解決在`php7.1`呼叫時會產生`$this`上下文不存在的錯誤 (https://bugs.php.net/bug.php?id=66430)// 呼叫閉包
            // __invole是魔術方法,當物件被作為函式呼叫時,會觸發該方法
            // 比如,$obj是一個物件的話,$obj()將觸發__invoke方法
            // 這裡是在閉包上呼叫__invoke,將執行該閉包
            return $function->__invoke(...$args);
        } else {
            return $reflect->invokeArgs($args);
        }
    } catch (ReflectionException $e) {
        // 如果是呼叫閉包時發生錯誤則嘗試獲取閉包的真實位置
        if (isset($reflect) && $reflect->isClosure() && $function instanceof Closure) {
            $function = "{Closure}@{$reflect->getFileName()}#L{$reflect->getStartLine()}-{$reflect->getEndLine()}";
        } else {
            $function .= '()';
        }
        throw new Exception('function not exists: ' . $function, 0, $e);
    }
}

同樣的原因,程式接著執行return $function->__invoke(...$args);,該語句是觸發閉包的執行。這裡再貼一下該閉包及其前後的程式碼:

$this->app->middleware->add(function () use ($dispatch) {
    try {
        $response = $dispatch->run();
    } catch (HttpResponseException $exception) {
        $response = $exception->getResponse();
    }
    return $response;
});

執行閉包,首先會執行$response = $dispatch->run();run方法的程式碼如下:

public function run(): Response
{
    // HTTP的OPTIONS方法用於獲取目的資源所支援的通訊選項。
    if ($this->rule instanceof RuleItem && $this->request->method() == 'OPTIONS' && $this->rule->isAutoOptions()) {
        $rules = $this->rule->getRouter()->getRule($this->rule->getRule());
        $allow = [];
        foreach ($rules as $item) {
            $allow[] = strtoupper($item->getMethod());
        }

        return Response::create('', '', 204)->header(['Allow' => implode(', ', $allow)]);
    }

    //獲取路由引數定義
    $option = $this->rule->getOption();

    // 資料自動驗證
    if (isset($option['validate'])) {
        $this->autoValidate($option['validate']);
    }
    //這裡的exec方法位於Controller類
    $data = $this->exec();
    // 控制器操作返回的資料,進一步加工,設定Http報頭、狀態碼等,返回一個Response物件
    return $this->autoResponse($data);
}

分析詳見註釋。主要的邏輯在$data = $this->exec(),其中,注意exec方法位於think\route\dispatch\Controller類,其程式碼如下:

public function exec()
    {
        try {
            // A 例項化控制器
            $instance = $this->controller($this->controller);
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, 'controller not exists:' . $e->getClass());
        }

        // B 註冊控制器中介軟體
        $this->registerControllerMiddleware($instance);
        // 將一個閉包註冊為控制器中介軟體
        $this->app->middleware->controller(function (Request $request, $next) use ($instance) {
            // 獲取當前操作名
            $action = $this->actionName . $this->rule->config('action_suffix');
            //如果$instance->$action可呼叫
            if (is_callable([$instance, $action])) {
                //獲取請求引數
                $vars = $this->request->param();
                try {
                    //獲取控制器對應操作的反射類物件
                    $reflect = new ReflectionMethod($instance, $action);
                    // 嚴格獲取當前操作方法名
                    $actionName = $reflect->getName();
                    // 設定請求的操作名
                    $this->request->setAction($actionName);
                } catch (ReflectionException $e) {
                    $reflect = new ReflectionMethod($instance, '__call');
                    $vars    = [$action, $vars];
                    $this->request->setAction($action);
                }
            } else {
                // 操作不存在
                throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
            }
            // 呼叫反射執行類的方法
            // **到這一步才執行了控制器的操作**
            // 比如,控制器操作直接返回字串,$data='hello,ThinkPHP6'
            $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
            // 獲取響應類物件
            return $this->autoResponse($data);
        });
        //跟前面執行全域性(路由)中介軟體的原理一樣,只是這裡的type為controller
        return $this->app->middleware->dispatch($this->request, 'controller');
    }

詳細分析見程式碼註釋,以下再展開分析控制器的例項化和控制器中介軟體的註冊。

A 控制器的例項化

執行例項化的think\route\dispatch\Controller類的controller方法程式碼如下:

public function controller(string $name)
{
    // 是否使用控制器字尾
    $suffix = $this->rule->config('controller_suffix') ? 'Controller' : '';
    // 訪問控制器層名稱
    $controllerLayer = $this->rule->config('controller_layer') ?: 'controller';
    // 空控制器名稱
    $emptyController = $this->rule->config('empty_controller') ?: 'Error';
    //獲取控制器完整的類名
    $class = $this->app->parseClass($controllerLayer, $name . $suffix);
    // 如果這個類存在
    if (class_exists($class)) {
        //通過容器獲取例項(非單例模式)
        return $this->app->make($class, [], true);
        //不存在時,如果有空控制器的類存在
    } elseif ($emptyController && class_exists($emptyClass = $this->app->parseClass($controllerLayer, $emptyController . $suffix))) {
        //同理,例項化空控制器
        return $this->app->make($emptyClass, [], true);
    }
    // 如果找不到控制器的類,且連控控制器也沒有,丟擲錯誤
    throw new ClassNotFoundException('class not exists:' . $class, $class);
}

具體分析見註釋。

B 控制器中介軟體註冊

執行註冊的registerControllerMiddleware方法程式碼如下:

protected function registerControllerMiddleware($controller): void
{
    // 獲取反射類物件
    $class = new ReflectionClass($controller);
    // 檢查控制器類是否有middleware屬性
    if ($class->hasProperty('middleware')) {
        //提取middleware變數
        $reflectionProperty = $class->getProperty('middleware');
        //設定可見性為公有
        $reflectionProperty->setAccessible(true);
        //獲取middleware屬性的值
        $middlewares = $reflectionProperty->getValue($controller);
        //解析控制器中介軟體配置
        foreach ($middlewares as $key => $val) {
            if (!is_int($key)) {
                //如果有設定only屬性
                //$this->request->action(true)獲取當前操作名並轉為小寫
                //$val['only']各元素也轉為小寫,然後判斷當前操作是否在$val['only']裡面
                //不在則跳過(說明該操作不需要執行該中介軟體)
                if (isset($val['only']) && !in_array($this->request->action(true), array_map(function ($item) {
                    return strtolower($item);
                }, $val['only']))) {
                    continue;
                    //如果有設定except屬性,且當前操作在$val['except']裡面,說明當前操作不需要該中介軟體,跳過
                } elseif (isset($val['except']) && in_array($this->request->action(true), array_map(function ($item) {
                    return strtolower($item);
                }, $val['except']))) {
                    continue;
                } else {
                    //儲存中介軟體名稱或者類
                    $val = $key;
                }
            }
            //註冊控制器中介軟體,跟前面註冊路由中介軟體一樣原理,只是,中介軟體的type為controller
            $this->app->middleware->controller($val);
        }
    }
}

分析詳見註釋。

總結

長路漫漫,到這裡,終於分析完了runWithRequest方法,前面的分析,基本是圍繞著runWithRequest方法展開。現在,讓我們將目光轉回run方法:

public function run(Request $request = null): Response
{
    .
    .
    .
    try {
        $response = $this->runWithRequest($request);
    } catch (Throwable $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    }
    // 設定cookie並返回響應物件
    return $response->setCookie($this->app->cookie);
}

runWithRequest方法跑完,run方法也差不多結束了,最終它返回一個Response物件。

Was mich nicht umbringt, macht mich stärker

相關文章