分析 Routing(7)

繼續昨天的 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,這也是想要寫這個主題的主要目的。