iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 18
1

繼續昨天的 runRoute(),直接來看原始碼:

protected function runRoute(Request $request, Route $route)
{
    // 把 route 資訊設定回 request 裡
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    // 觸發 RouteMatched 事件
    $this->events->dispatch(new Events\RouteMatched($route, $request));

    // 產生 Response
    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}

runRouteWithinStack() 取得結果後,再給 prepareResponse() 產生 Response。

protected function runRouteWithinStack(Route $route, Request $request)
{
    // 這是使用在測試想把 middleware 關閉的時候
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    // 產生 Route 專屬的 middleware
    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    // 送入到 Pipeline 執行
    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        // 實際執行 route 的地方在這裡:$route->run()
                        return $this->prepareResponse(
                            $request, $route->run()
                        );
                    });
}

Route 的 middleware 是用 gatherRouteMiddleware() 產生的:

public function gatherRouteMiddleware(Route $route)
{
    $middleware = collect($route->gatherMiddleware())->map(function ($name) {
        return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);
    })->flatten();

    return $this->sortMiddleware($middleware);
}

Route::gatherMiddleware() 會先取得 Route 裡面設定的 middleware,再交由 MiddlewareNameResolver::resolve() 解析成 Pipeline 可以使用的 middleware 格式;最後使用 sortMiddleware() 排序。

Middleware 的產生比較複雜,留著明天說明,我們先了解 gatherRouteMiddleware() 會產生屬於該 Route 的一組 middleware 就好。

接著,真正執行 Route 的地方就在 $route->run()

public function run()
{
    $this->container = $this->container ?: new Container;

    try {
        // 當 $action['uses'] 是字串的話,代表是 Controller,使用 controller 方法執行
        if ($this->isControllerAction()) {
            return $this->runController();
        }

        // 不是就用 callable 方法呼叫
        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

再繼續看 runController()runCallable() 之前,我們回憶一下:之前使用 Container::make() 的時候,它都會在建構的時候注入;今天的狀況則不大一樣,Laravel 是要在呼叫 controller method 的時候注入。可以預見的是,待會肯定會看到反射(Reflection)處理。

先看比較單純的 runCallable()

protected function runCallable()
{
    $callable = $this->action['uses'];

    // 使用 resolveMethodDependencies() 解析依賴,並把取到的 parameter 和 reflection 實例傳入
    return $callable(...array_values($this->resolveMethodDependencies(
        $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses'])
    )));
}

resolveMethodDependencies() 就不深入分析了,跟分析 Container 類似,只是這次的目標是 ReflectionFunction

runController() 的原始碼如下:

protected function runController()
{
    // 主要是 controllerDispatcher 的實例在處理
    // getController() 會取得 Controller 實例
    // getControllerMethod() 則是取得要呼叫的 method 名稱
    return $this->controllerDispatcher()->dispatch(
        $this, $this->getController(), $this->getControllerMethod()
    );
}

dispatch() 的實際程式碼如下:

public function dispatch(Route $route, $controller, $method)
{
    // 解析依賴
    $parameters = $this->resolveClassMethodDependencies(
        $route->parametersWithoutNulls(), $controller, $method
    );

    // 如果 controller 有定義 callAction(),就呼叫一下 
    if (method_exists($controller, 'callAction')) {
        return $controller->callAction($method, $parameters);
    }

    // 將解析到的依賴拿來呼叫 method
    return $controller->{$method}(...array_values($parameters));
}

resolveClassMethodDependencies() 有趣的地方是,會發現最後跟 callable 一樣,呼叫了 resolveMethodDependencies()

protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
    if (! method_exists($instance, $method)) {
        return $parameters;
    }

    return $this->resolveMethodDependencies(
        $parameters, new ReflectionMethod($instance, $method)
    );
}

回到 $route->run(),如果有寫過 Laravel 的話,會知道下面這些 return 都是可行的:

return view('welcome');
return redirect()->to('some-where');
return response()->json(['some' => 'data']);
return 'Hello Wrold';

但事實上,在分析 bootstrap 流程時,有提到最後會拿到 Response 輸出,因此中間應該有做了一些轉換,才能拿到正常的 Response,這是 prepareResponse() 的任務:

public function prepareResponse($request, $response)
{
    return static::toResponse($request, $response);
}

public static function toResponse($request, $response)
{
    // 如果有實作 Responsable 就直接使用 toResponse()
    if ($response instanceof Responsable) {
        $response = $response->toResponse($request);
    }

    // 如果是 PsrResponseInterface,就使用 Adapter 轉換
    if ($response instanceof PsrResponseInterface) {
        $response = (new HttpFoundationFactory)->createResponse($response);
        
    // 如果是 Eloquent Model,就使用 JsonResponse + 201
    } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
        $response = new JsonResponse($response, 201);
        
    // 如果不是 SymfonyResponse,但是是 array 或 json 可能的形式,就使用 JsonResponse + 200
    } elseif (! $response instanceof SymfonyResponse &&
               ($response instanceof Arrayable ||
                $response instanceof Jsonable ||
                $response instanceof ArrayObject ||
                $response instanceof JsonSerializable ||
                is_array($response))) {
        $response = new JsonResponse($response);
    
    // 如果不是 SymfonyResponse,這時預期會是字串,就直接轉換成 Laravel Response
    } elseif (! $response instanceof SymfonyResponse) {
        $response = new Response($response);
    }

    if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
        $response->setNotModified();
    }

    // 最後,因為 SymfonyResponse 理論上是全新剛產生的物件,所以會需要呼叫 prepare() 來初始化
    return $response->prepare($request);
}

到此,Request 傳入到產出 Response 的流程都全部講完了,相信讀者對整個流程會做什麼,或是不會做什麼,有一定程度的了解了。

就筆者的經驗來說,有踩過下面這幾個蠢事,但現在也得到了解答:

  • Middleware 的 handle() 以為它也有做依賴注入,實際上它只有傳入 Kernel 那邊所設定的 parameter,這是 Pipeline 的運作原理
  • Controller Action 可以定義 PSR-7 的 Request 與回傳 Response,但 Middleware 卻不能做一樣的事,這是今天才知道為何會這樣的
  • 為何要有 StartSession,Session 才會正常運作
  • 為何 Cookie 直接使用 cookie() 沒有用,要把實例加到 queue() 才行

知道內部運作的細節後,就也更正確的使用 Laravel,這也是筆者想要寫這個主題的目的之一。


上一篇
分析 Routing(6)
下一篇
分析 Marcoable
系列文
Laravel 原始碼分析46

尚未有邦友留言

立即登入留言