Laravel 7.4 Kernelのコードを読む。
bind
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
Kernelは bootstrap/app.php
でサービスコンテナにシングルトンとしてbindされる。
abstractは Illuminate\Contracts\Http\Kernel
だ。
App\Http\Kernel.php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
Illuminate\Foundation\Http\Kernel.php
を継承している。ロジックはここに書かれてる。
App\Http\Kernel.phpは App\Httpにあるので実装者が挙動を変えることができる。
デフォルトでは
- $middleware
- $middlewareGroups
- $routeMiddleware
のプロパティが用意されている。
Illuminate\Contracts\Http\Kernel.php
まずはインターフェースを追う。結構シンプル。
interface Kernel
{
// 起動処理
public function bootstrap();
// requestをresponseに変換
public function handle($request);
// 終了処理
public function terminate($request, $response);
// LaravelApplicationインスタンスを返す
public function getApplication();
}
Illuminate\Foundation\Http\Kernel.php
Laravelが用意したHttpカーネルの実態。
Illuminate\Contracts\Http\Kernel.php
を継承している。
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$this->syncMiddlewareToRouter();
}
まずはコンストラクタ。
引数でApplicationとRouterを受け取ってプロパティに代入する。Kernelクラスはサービスコンテナでインスタンス化されるので引数はDI代入される。
引数に型指定してある。Applicationクラスのコンストラクタは型指定していないのになんでだろう。
protected function syncMiddlewareToRouter()
{
$this->router->middlewarePriority = $this->middlewarePriority;
foreach ($this->middlewareGroups as $key => $middleware) {
$this->router->middlewareGroup($key, $middleware);
}
foreach ($this->routeMiddleware as $key => $middleware) {
$this->router->aliasMiddleware($key, $middleware);
}
}
MiddlewareをRouteに渡してる。なぜKernelでやるのだろう・・・?(ConfigからMiddlewareを読み込むこともできると思うけど重要度的にKernelでやった方が良いのかな)
Kernelライフサイクル
Kernelのライフサイクルを追う。
handle関数
bootstrap/app.php
でKernelがBindされた後、public/index.php
でhandle関数が実行される。
handle関数ではrequestからresponseへの変換を行う。
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Throwable $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
結構シンプル。
$request->enableHttpMethodParameterOverride();
最初にHttpMethodParameterOverrideを有効化してる。これはLaravelではなくLaravelの元?になったSymfonyの機能。
HttpMethodはGETやPOSTに加えPUTやDELETE等がある。しかしHtmlのFormではGETとPOSTしか使えない(ブラウザが未対応)。擬似的に他Methodを使用する方法としてhidden属性で_methodパラメータにMethodを書いて送信する方法がある。ここではこの機能を有効化している。enableHttpMethodParameterOverride関数のコメントにはこれによりCSRFの脆弱性を引き起こすので注意するよう書かれている。
$response = $this->sendRequestThroughRouter($request);
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());
}
MiddlewareやRouterを通してresponse変換を行なっている。
- まずサービスコンテナにrequestの登録を行い、
- ファサードの'request'内部処理キャッシュを削除し、
- bootstrapを実行
- Middlewearとrouterに処理を渡してresponseをreturn;
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
bootstrapではApplicationのbootstrapWith関数にkernelが指定したbootstrapクラスを渡している。
bootstrap処理はApplication内部で行われる。
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
}
}
Application.phpのbootstrapWithでは引数のbootstrapクラスを$this->make
でインスタンス化しbootstrap関数を実行している。合わせてbootstrapイベントを実行している。
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
Pipelineではmiddlewareを実行し、最後にdispatchToRouter関数を実行している。
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
dispatchToRouter関数(が作成するfunction)では$requestを受け取りrouterを起動している。
dispatch関数では内部でdispatchToRoute関数を実行。
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
findRouteで$requestにマッチするrouteを見つけ、runRouteを実行する。
RouterとRouteがごっちゃにならないよう注意したい。
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
$this->runRouteWithinStack($route, $request)
でRouteMiddlewareに実行やControllerの起動を行う。
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が出てくる。最終的には$route->run()
でresponse(で使える値)を作成し、$this->prepareResponse
でHttpFoundation\Responseに変換する。
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()
);
}
コントローラーなら$this->runController()
を実行する。
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));
}
そうして遂にコントローラーの関数を実行する。
callAction関数が定義されているとそっち優先になるのか。
コントローラー関数の戻り値は大抵viewクラスになる。viewのままだとresponseとして使えないので呼び出し元でresponseに変換する。
public static function toResponse($request, $response)
{
if ($response instanceof Responsable) {
$response = $response->toResponse($request);
}
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif ($response instanceof Model && $response->wasRecentlyCreated) {
$response = new JsonResponse($response, 201);
} elseif (! $response instanceof SymfonyResponse &&
($response instanceof Arrayable ||
$response instanceof Jsonable ||
$response instanceof ArrayObject ||
$response instanceof JsonSerializable ||
is_array($response))) {
$response = new JsonResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response, 200, ['Content-Type' => 'text/html']);
}
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}
return $response->prepare($request);
型に合わせてresponse変換を行う。
そうして最終的に index.php
に戻ってくる。
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$response->send();
でHeaderへの書き込みやcontentをechoする。
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (\function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
public function sendContent()
{
echo $this->content;
return $this;
}
後はphp-fpmが終了通知を受け取ってhttpをレスポンスする。