概要
Laravelではthrowしたエラーを明示的にcatchしなくてもよしなにしてくれます。
エラーをカスタムしたいなと思ったときにどこでエラーをキャッチしているのか知りたかったのでLaravelの流れを読むことにしました。
start
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
Kernel::handle()
のtry句の中身を見ていきます。
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
enableHttpMethodParameterOverride()
はプロパティをtrueにセットしているだけなのでスルーします。
sendRequestThroughRouter()
は同じクラスに実装されています。
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
$this->app->instance('request', $request)
では$request
をサービスコンテナに登録しています。
Facade::clearResolvedInstance('request')
はファサードにキャッシュされているrequestをクリアしています。(キャッシュされるタイミングはあるのだろうか?)
$this->bootstrap()
でいろいろ準備をしてるみたいです。
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
$bootstrappers
プロパティの配列をbootstrapWith()
の引数にぶちこんでいきます。
public function bootstrapWith(array $bootstrappers)
{
foreach ($bootstrappers as $bootstrapper) {
$this->make($bootstrapper)->bootstrap($this);
}
}
サービスコンテナで順番にインスタンス化していって、その中のbootstrap()
を呼んでいます。
bootstrap()
が終わったら、sendRequestThroughRouter()
に戻ります。
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
ここでreturnしているので、Pipeline
クラスがresponseを生成しているみたいですね。
public function send($passable)
{
$this->passable = $passable;
return $this;
}
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
send()
はrequestをプロパティにセットして、through()
はmiddlewareをプロパティにセットしています。
Pipeline
のthen()
はmiddlewareを適用してrouterにdispatchを行います。(then()
については複雑なのでこちらの記事を読んでください。LaravelのPipeline::thenでmiddlewareを処理している流れをまとめてみた)
return $pipeline($this->passable)
は最終的にprepareDestination()
を呼んで、引数のClosureを呼び出しています。
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
try {
// ここが呼び出される
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
}
$destination
はthen()
の引数に渡されたものなので、$this->dispatchToRouter()
で生成したClosureが呼び出されます。
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
routerのdispatch()
を呼び出します。
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
protected function runRoute(Request $request, Route $route)
{
//...
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
$this->findRoute()
でリクエストされたルートやメソッド(get, postなど)で対象のルーティングを見つけてきます。
runRoute()
でレスポンスを返却しています。
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
Pipelineでmiddlewareを適用しつつ、$route->run()
でいよいよControllerに処理を委譲していきます。
public function run()
{
$this->container = $this->container ?: new Container;
try {
if ($this->isControllerAction()) {
return $this->runController();
}
return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}
return $controller->{$method}(...array_values($parameters));
}
ここまできてやっと$controller->{$method}
とコントローラーのメソッドが呼ばれました!
コントローラーで投げられた例外は、HttpResponseException
クラスなら、Route::run()
ないでcatchされレスポンスに変換されています。
public function run()
{
try {
if ($this->isControllerAction()) {
return $this->runController();
}
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
それ以外の例外は、Pipeline::prepareDestination()
でキャッチされ、handleException()
が呼ばれます。
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
try {
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
}
protected function handleException($passable, Exception $e)
{
if (! $this->container->bound(ExceptionHandler::class) ||
! $passable instanceof Request) {
throw $e;
}
$handler = $this->container->make(ExceptionHandler::class);
$handler->report($e);
$response = $handler->render($passable, $e);
if (method_exists($response, 'withException')) {
$response->withException($e);
}
return $response;
}