繼續昨天的 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 的流程都全部講完了,相信讀者對整個流程會做什麼,或是不會做什麼,有一定程度的了解了。
就筆者的經驗來說,有踩過下面這幾個蠢事,但現在也得到了解答:
handle()
以為它也有做依賴注入,實際上它只有傳入 Kernel 那邊所設定的 parameter,這是 Pipeline 的運作原理cookie()
沒有用,要把實例加到 queue()
才行知道內部運作的細節後,就也更正確的使用 Laravel,這也是筆者想要寫這個主題的目的之一。