Read the Source (1) Laravelのライフサイクルを理解する
ソースコードを読もう!との趣旨で新しいシリーズを開始します。
最近の仕事ではLaravelが使われているのでそこから始まろうと。基本的にソースコードと公式のドキュメントをメインに参考します。バージョンは8ですが若干違いがあるかもしれません。
public/index.phpからの鳥瞰図
全てのリクエストがpublic/index.phpとのファイルに送られます。中身を見てみると:
/*
|--------------------------------------------------------------------------
| Check If The Application Is Under Maintenance
|--------------------------------------------------------------------------
|
| If the application is in maintenance / demo mode via the "down" command
| we will load this file so that any pre-rendered content can be shown
| instead of starting the framework, which could cause an exception.
|
*/
if (file_exists(__DIR__.'/../storage/framework/maintenance.php')) {
require __DIR__.'/../storage/framework/maintenance.php';
}
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| this application. We just need to utilize it! We'll simply require it
| into the script here so we don't need to manually load our classes.
|
*/
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request using
| the application's HTTP kernel. Then, we will send the response back
| to this client's browser, allowing them to enjoy our application.
|
*/
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Kernel::class);
$response = tap($kernel->handle(
$request = Request::capture()
))->send();
$kernel->terminate($request, $response);
このファイルで行われていることについて:
- オートロードの設定を読み込みます。こちらの設定は
composer.json
のautoloadの内容を反映します。 - Laravelアプリケーション(以下は「アプリ」で)のインスタンスが作られます。インスタンスはbootstrap/app.phpファイルから取得していますが、また次に詳しく読みます。
- アプリのkernelインスタンスを作ります。HTTP kernelまたはconsole kernelとの2種類のkernelが存在しますが、前者は名前通りHTTPリクエストの処理、後者はスケジュールタスク・cronやコンソールコマンドの処理を担います。
- リクエストの種類によって、それぞれのkernelに処理(handle)されて、そのレスポンスをリターンして送ります。
- 最後にkernelインスタンスがこのリクエストを断ち切ります。
bootstrap/app.phpでバイディング
ここでbootstrap/app.php
で何が起こったのかを見てみます:
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;
いきなり$app
が作られましたが、このIlluminate\Foundation\Application
が実際のアプリの容器(container)となっています。定義では次のようになっています。
class Application extends Container implements ApplicationContract, HttpKernelInterface
{...}
そのコンテナー・容器クラスには、singletonのメソッドが:
/**
* Register a shared binding in the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
最初は何じゃこりゃ??と思いました。ここはバイディングという行為を理解する必要があります。バイディングとは、簡単にいえば、マッピングを作ることで、Laravelでマッピングの「キー」を使うことで、「ヴァリュー」の部分が取得・実行できるようになることです。これらのキーバリューペアがコンテナーに保存されていて、Laravelのアプリインスタンス自身がそのコンテナーとなります。
コンテナークラスの実現には、bindメソッドとmakeメソッドがあり、makeメソッドにキーを渡して実行することで、バリューの部分を取得することになります。コンテナークラスのシンプルバージョンを見てみるとわかりやすくなるかもしれません:
class Container {
// キーバリューペアの配列
protected $bindings = [];
// bindで新しいkvを追加
public function bind($key, $value)
{
$this->bindings[$key] = $value;
}
// makeでバリューを取得
public function make($key)
{
if (isset($this->bindings[$key])) {
// callableなら実行し、でなければ値をリターン
if (is_callable($this->bindings[$key])) {
return call_user_func($this->bindings[$key]);
} else {
return $this->bindings[$key];
}
}
}
}
singleto
nメソッドはバイディングの一種です(特殊なbind
メソッドとして見られる)。このメソッドでのバイディングでは、「常に一つのインスタンス」しか存在しないことを保証してくれます。もちろん、Laravelのアプリは一つしか作らないのでここはsingleton
が必要になると。
さて、bootstrap/app.php
に戻りますが、ここで何が起こったかは理解できるようになるでしょう。
- アプリのコンテナーを作ります
- コンテナーにインターフェースをキーに、実現のクラスをヴァリューに、2種類の
kernel
とExceptionHandler
をバイディングします - 最後はバイディングされたコンテナー、
$app
をリターンします
インターフェースとそのインターフェースを実現したクラスをバイディングすることは、今後もあっちこっちに見かけると思います。ベースとなるのは、上記のバイディングとなりますが、インターフェイスをキーにすることで、実現するクラスを変えるのが簡単になったり、場合によって実現クラスA、実現クラスBなどに切り替えることが可能になります。具体的に次回のサービスコンテナーとプロバイダーのところでまた詳しく見ようと思います。
HTTP Kernel
Laravelのアプリがコンテナーとして作られ、必要なバイディングをしてから、次のステップに入ります。
// public/index.php
// ...
$kernel = $app->make(Kernel::class);
$response = tap($kernel->handle(
$request = Request::capture()
))->send();
$kernel->terminate($request, $response);
前の節で説明した通り、ここはmake
メソッドで、httpリクエストを処理するkernelを取得しています。対象はバイディングのバリュー、App\Http\Kernel::class
となります。
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
HTTPリクエストを処理する時に、mvcで考えるとコントローラが主役だと考えがちですが、実際にコントローラに到達するまでに、様々なミドルウェアを通さないといけません。これを玉ねぎのように考えると、コントローラが真ん中にあり、外側にいくつかの皮に覆われています。
App\Http\Kernel.php
ファイルでは、これらのミドルウェアをリスティングしています。グローバルに通して欲しいミドルウェアもありながら、webかapiかによって通すか、または特定のルート(ルートのグループも含めて)のみ通すものがあります。
次のhandle
メソッドについて、HttpKernel
クラスにあります:
/**
* 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;
}
簡単に言えば、リクエストを受け取って、レスポンスを返します。うん。。それわかるけど、もう少し見てみると:
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
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());
}
ここでルーターが出てきましたが、どこから??と思ったりしますね。コンストラクターを見てみると:
/**
* Create a new HTTP kernel instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$this->syncMiddlewareToRouter();
}
アプリとルーターが依存注入(Dependency Injection)の形でkernelに入っていました。
またsendRequestThroughRouter
に戻ると、instance
メソッドで、$request
をコンテナー(アプリ)で共有できるようにします。Facade::clearResolvedInstance('request')
では、キャッシュされている古いrequest
をクリアします。
次にbootstrap
メソッドで次の配列のクラスを導入します:
/**
* The bootstrap classes for the application.
*
* @var string[]
*/
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,
];
//...
/**
* Bootstrap the application for HTTP requests.
*
* @return void
*/
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
bootstrapWith
メソッドまで少し深入りすると:
/**
* Run the given array of bootstrap classes.
*
* @param string[] $bootstrappers
* @return void
*/
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]);
}
}
イベントはさておき、真ん中のmake
メソッドは前述したように、該当bootstrapper
をキーに、アプリコンテナーからバリューを取り出し、それぞれ起動(bootstrap)します。
sendRequestThroughRouter
に戻りますが、最後にパイプラインを作り、リクエストをミドルウェアーを通してから、ルーターに送るようになっています。パイプラインについて少し複雑な部分があるので、またルーターの部分を読む時に深入りしようと思います。
これでようやく、レスポンスがリターンされ、最後にkernel
を終了(terminate)にし、それぞれのミドルウェアを止めて、最後にアプリインスタンスを止めると:
/**
* Call the terminate method on any terminable middleware.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
* @return void
*/
public function terminate($request, $response)
{
$this->terminateMiddleware($request, $response);
$this->app->terminate();
}
ここまでは主にHTTPリクエストが送られてきてから、Laravelアプリのライフサイクルについて見てきました。まだまだ深入りできるところがいっぱいですが、また今度のテーマにしようと思います。
では今日はこれで。
Discussion