Read the Source (1) Laravelのライフサイクルを理解する

2021/10/19に公開

ソースコードを読もう!との趣旨で新しいシリーズを開始します。

最近の仕事では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];
            }
        }
    }

}

singletonメソッドはバイディングの一種です(特殊なbindメソッドとして見られる)。このメソッドでのバイディングでは、「常に一つのインスタンス」しか存在しないことを保証してくれます。もちろん、Laravelのアプリは一つしか作らないのでここはsingletonが必要になると。

さて、bootstrap/app.phpに戻りますが、ここで何が起こったかは理解できるようになるでしょう。

  • アプリのコンテナーを作ります
  • コンテナーにインターフェースをキーに、実現のクラスをヴァリューに、2種類のkernelExceptionHandlerをバイディングします
  • 最後はバイディングされたコンテナー、$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