はじめに
普段Laravelでアプリケーションを実装するときは
コントローラでリクエストを受け取り、
コントローラでレスポンスを返却するまでの範囲しか
あまり意識していませんでした。
ですが、実際には
WEBサーバがHTTPリクエストを受け取ってから
コントローラに処理が渡されるまでの間や、
コントローラからレスポンスを返却してから
WEBサーバがHTTPレスポンスを送信するまでの間
にはいろいろと処理が行われているはずです。
・/public
ディレクトリに実際のHTMLファイルはないけどなぜルーティング処理されるんだ?
・どこの誰がどうやって処理対象のコントローラを特定してくれているんだ?
・あの便利なRequestインスタンスはいつ誰が作って渡してくれているんだ?
などなど
普段意識していなかった
コントローラより外の世界 が気になり、
Laravelのコードを追いかけてみましたので解説したいと思います。
今回はLaravelのソースコードの細かい分岐処理などは全て省略し、
メインフローのみピックアップして解説していくので
難しい複雑なコードはあまり登場しません。
Laravel6.2を元に解説を進めますが、
ほかのバージョンでも大した差はないと思います。
全体の流れ
全体の流れとしてはこのようになっています。
- WEBサーバがindex.phpにリダイレクト
- index.phpの中
- autoload読み込み
- Applicationインスタンス作成
- HttpKernelインスタンス作成
- Requestインスタンス作成
- HttpKernelがリクエストを処理してResponse取得
- レスポンス送信
- terminate()で後片付け
この1つ1つをもう少し詳しく解説していきます。
WEBサーバがindex.phpにリダイレクト
Laravelのドキュメントルートは/public/
です。
例えばhttps://example.com/test/
のURLにリクエストされたとして
通常であれば
/public/test/index.html
を取得することになります。
しかし、Laravelではすべての処理の始まりが/public/index.php
のファイルになるので、
WEBサーバの設定で
どのURLに対するリクエストでも、
全て/public/index.php
にリダイレクトするようになっています。1
WEBサーバがApacheの場合は.htaccessファイルに
この様にリダイレクト処理を書きます。
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
(この.htaccess設定はデフォルトで/public/.htaccess
に配置されています)
WEBサーバがNginxの場合はconfファイルに
この様にリダイレクト処理を書きます。
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
これらのWEBサーバ設定によって、
まずは全ての入り口となる/public/index.php
に入っていきます。
index.phpの中
実際のファイルがこちら。
require __DIR__.'/../vendor/autoload.php'; // 【1】autoload読み込み
$app = require_once __DIR__.'/../bootstrap/app.php'; // 【2】Applicationインスタンス作成
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); // 【3】HttpKernelインスタンス作成
$response = $kernel->handle( // 【5】HttpKernelがリクエストを処理してResponse取得
$request = Illuminate\Http\Request::capture() // 【4】Requestインスタンス作成
);
$response->send(); // 【6】レスポンス送信
$kernel->terminate($request, $response); // 【7】terminate()で後片付け
このindex.phpファイルが、Laravelの処理全体フローそのものになります。
【1】~【7】までをざっくりと追いかけていきます。
その中でも
【5】HttpKernelがリクエストを処理してResponse取得
は一番のメイン処理で、
僕らの知るコントローラにたどり着くまで
コードを深く深く見ていくことになります。
【1】autoload読み込み
まず初めに
require __DIR__.'/../vendor/autoload.php'; // 【1】autoload読み込み
でautoloadファイルを読み込みます。
PHPでは別ファイルのクラスを読み込む際は
require
を使ってファイルを読み込む必要がありますが、
このautoloadを最初に読み込むことで
いちいちrequireでファイルを読み込むことなく別ファイルのクラスを
利用できるようになります。
autoloadの詳しい仕様については割愛します。
参考:https://laraweb.net/surrounding/1642/
【2】Applicationインスタンス作成
次に、
$app = require_once __DIR__.'/../bootstrap/app.php'; // 【2】Applicationインスタンス作成
ここでApplicationインスタンスを作成しています。
/bootstrap/app.php
の中も見てみます。
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
return $app;
Illuminate\Foundation\Application
クラスをインスタンス化して返却しています。
このApplicationインスタンスが、通称「サービスコンテナ」と呼ばれるものです。
このサービスコンテナについては
こちらの記事で解説しているので
興味のある方はご覧ください。
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。
全体処理の流れをざっくり見る
という目的なので、今は気にせず次に進んでいいかと思います。
【3】HttpKernelインスタンス作成
次に、
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); // 【3】HttpKernelインスタンス作成
でHttpKernelクラスをインスタンス化しています。
この$app->make()
メソッドは、
単純にKernelクラスをnewしているだけと思って大丈夫です。
※詳しく知りたい方はこちらの記事でサービスコンテナについて解説しています
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。
kernel
とは「核」とか「核心」という意味で、
ソフトウェアの世界では
「司令塔となる中心の機能」
というイメージで僕はとらえています。
今回のHttpKernelの場合は
「Http処理全体の司令塔となるクラス」というイメージ。
【4】Requestインスタンス作成
次に、
$response = $kernel->handle( // 【5】HttpKernelがリクエストを処理してResponse取得
$request = Illuminate\Http\Request::capture() // 【4】Requestインスタンス作成
);
この$kernel->handle()
の引数として渡されている
$request = Illuminate\Http\Request::capture() // 【4】Requestインスタンス作成
ここの処理でRequestインスタンスを作成しています。
このcapture()
の中を見てみます。
public static function capture()
{
return static::createFromBase(SymfonyRequest::createFromGlobals());
}
さらにSymfonyRequest::createFromGlobals()
の中を見てみます。
※ここから先はSymfonyのコードになります2
public static function createFromGlobals()
{
$request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
return $request;
}
ここで、PHPのスーパーグローバル変数である
$_GET
、$_POST
、$_COOKIE
、$_SERVER
などを使っていますね。
素のPHPの機能で、
GET・POSTリクエスト内容や受け取ったCookie、リクエストのURLやHTTPメソッドなど
様々なHTTPリクエストの情報を取得することができます。
https://www.php.net/manual/ja/reserved.variables.server.php
さらに、
そのPHPのスーパーグローバル変数で取得した
リクエスト情報を渡しているself::createRequestFromFactory
の中を見てみます。
private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): self
{
return new static($query, $request, $attributes, $cookies, $files, $server, $content);
}
受け取った様々なリクエスト情報を引数に渡して、
ここでやっとRequestクラスをインスタンス化しました。
(この時点ではLaravelのRequestクラスではなくSymfonyのRequestクラスです)
そして、そのSymfonyRequestインスタンスを返却しています。
このSymfonyのRequestインスタンスが返却される途中、
先ほど見たこの処理のstatic::createFromBase
で
LaravelのRequestクラスインスタンスに変換されています。
public static function capture()
{
return static::createFromBase(SymfonyRequest::createFromGlobals());
}
これでようやく
この$request
にRequestクラスのインスタンスを入れるところまで完了しました。
$response = $kernel->handle( // 【5】HttpKernelがリクエストを処理してResponse取得
$request = Illuminate\Http\Request::capture() // 【4】Requestインスタンス作成
);
【5】HttpKernelがリクエストを処理してResponse取得
次は
$response = $kernel->handle( // 【5】HttpKernelがリクエストを処理してResponse取得
$request = Illuminate\Http\Request::capture() // 【4】Requestインスタンス作成
);
ここでHttpKernel
のhandle()
メソッドにRequestを渡して、
Responseを受け取っています。
このhandle()
の中を見てみます。
public function handle($request)
{
$response = $this->sendRequestThroughRouter($request);
return $response;
}
さらに$this->sendRequestThroughRouter()
の中を見てみます。
protected function sendRequestThroughRouter($request)
{
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->middleware)
->then($this->dispatchToRouter());
}
$this->bootstrap();
では実際のリクエストの処理の前に、もろもろ設定の読み込みなどを行っています。
具体的には、.envの環境変数の読み込みや、
各サービスプロバイダの実行などです。
(メイン処理の流れとは外れるため詳細は割愛)
こちらがメインのリクエスト処理フローです。
return (new Pipeline($this->app))
->send($request)
->through($this->middleware)
->then($this->dispatchToRouter());
このPipelineクラスの仕様解説は割愛しますが、
見たまんま、
->send($request)
(Requestインスタンスを渡して)
->through($this->middleware)
(ミドルウェアを通して)
->then($this->dispatchToRouter());
(ルーターの処理を実行)
という処理です。
この
->through($this->middleware)
(ミドルウェアを通して)
ではグローバルミドルウェアの処理が実行されます。
そしてメイン処理の続きは
->then($this->dispatchToRouter());
(ルーターの処理を実行)
ですね。
このdispatchToRouter()
の先では、
・対象のルーティングを取得する
・そのルーティングに紐づいたコントローラとコントローラアクションメソッドを判定する
・そのコントローラアクションメソッドを実行する
・コントローラから返されたレスポンスをResponseクラスのインスタンスに変換する
・Responseインスタンスを返却する
という感じで処理が進みます。
この先はさらにコードの深いところに入っていくので、
現時点で疲れていれば上記の概要だけ把握して
次の「【6】レスポンス送信」まで飛ばしちゃってもいいと思います。
ではdispatchToRouter()
の先を見ていきます。
protected function dispatchToRouter()
{
return $this->router->dispatch($request);
}
HttpKernelクラスから、Routerクラスに処理が渡されました。
さらにdispatch()
を見ていきます。
public function dispatch(Request $request)
{
return $this->dispatchToRoute($request);
}
さらにdispatchToRoute()
の中。
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
$this->findRoute($request)
で、/routes/web.php
に定義したルーティングリストの中から
今回のリクエストに合致するものを取得してきます。
そしてその特定したルートを渡して
runRoute()
で処理を続行。
protected function runRoute(Request $request, Route $route)
{
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
さらにrunRouteWithinStack()
の中。
protected function runRouteWithinStack(Route $route, Request $request)
{
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then($route->run());
}
先ほどの見覚えのある処理が出てきました。
特定したルート($route)をもとに、
->send($request)
(Requestインスタンスを渡して)
->through($middleware)
(ミドルウェアを通して)
->then($route->run())
(ルートの処理を実行)
となっています。
先ほどはグローバルミドルウェアの処理が実行されていましたが、
今回の
->through($middleware)
では
ルートミドルウェアを実行しています。
そして、
$route->run()
でRouterクラスからさらにRouteクラスに処理が渡されました。
$route->run()
の中。
public function run()
{
return $this->runController();
}
やっとコントローラにたどり着けそうな感じがしてきました。
runController()
の中。
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
$this->getController()
では、
/routes/web.php
に書かれていた
{コントローラクラス名}@{アクションメソッド名}
の文字列を取得して@で文字列分割し、
{コントローラクラス名}
でコントローラをインスタンス化しています。
(「UserController@index」とかっていつも書くやつですね)
public function getController()
{
$class = $this->parseControllerCallback()[0]; // 「@」で分割してコントローラクラス名を取得
$this->controller = $this->container->make(ltrim($class, '\\')); // サービスコンテナのmake()でインスタンス化
return $this->controller; // コントローラインスタンスを返却
}
「UserController@index」の例では、UserControllerクラスがインスタンス化されることになります。3
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
$this->controllerDispatcher()->dispatch()
に
先ほどとってきたコントローラクラス
とアクションメソッド名
を渡しています。
dispatch()
の中。
public function dispatch(Route $route, $controller, $method)
{
return $controller->{$method}(...array_values($parameters));
}
ここでついに・・・。
コントローラのアクションメソッドが実行されます。
「UserController@index」の例では、
$controller
にはUserControllerのインスタンスが、
$method
には「index」の文字列が入っていますので、
$controller->index()
の形でちゃんとメソッドが実行されていますね。
長かったですが、
WEBサーバでリダイレクトされて/public/index.php
に入ったところから、
僕らがいつも意識しているコントローラの中の処理に入るまでに
この様な処理をたどっているわけです。
コントローラから返却されたレスポンスは
今まで来た道を帰っていき、
だいぶ前にみた
protected function runRoute(Request $request, Route $route)
{
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
このprepareResponse()
の中で、
LaravelのResponseインスタンスに変換されます。
そして最終的に最初の/public/index.php
にある
$response = $kernel->handle( // 【5】HttpKernelがリクエストを処理してResponse取得
$request = Illuminate\Http\Request::capture() // 【4】Requestインスタンス作成
);
ここまで帰って$response
に格納されます。
【6】レスポンス送信
次に、
$response->send(); // 【6】レスポンス送信
ここで実際にレスポンスを送信します。
$response->send();
の中を見てみます。
public function send()
{
$this->sendHeaders();
$this->sendContent();
}
見た通り、
sendHeaders()
ではHttpレスポンスヘッダーを送信し、
sendContent()
ではレスポンス内容(HTMLなど)を送信しています。
sendHeaders()
の中。
public function sendHeaders()
{
// headers
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
$replace = 0 === strcasecmp($name, 'Content-Type');
foreach ($values as $value) {
header($name.': '.$value, $replace, $this->statusCode);
}
}
// cookies
foreach ($this->headers->getCookies() as $cookie) {
header('Set-Cookie: '.$cookie, false, $this->statusCode);
}
// status
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
}
いろいろと書いてありますが、
header()
とある3か所だけ見てみるといいです。
素のPHPのheader()
メソッドを使って、
Content-Type
やCookie
、HTTPステータス
などの
HTTPヘッダー情報を送信していますね。
続いてsendContent()
の中。
public function sendContent()
{
echo $this->content;
}
content
をechoしています。
このcontent
には表示する画面のHTMLがそのまま入っています。
これで実際にHttpレスポンスを送信完了しました。
【7】terminate()で後片付け
/public/index.php
の最後の処理です。
$kernel->terminate($request, $response); // 【7】terminate()で後片付け
ここでは、各ミドルウェアクラスに定義されたterminate()
処理などを実行しているだけです。
これで、ついにLaravelの処理がすべて終了しました!
おわりに
普段意識しない、
コントローラより前の世界、後の世界を見ていきました。
個人的には、
・まず最初に素のPHPの$_GET
や$_SEVER
などを使ってRequestクラスを作っている
・最後は素のPHPのheader()
とecho()
でレスポンスを送信している
というところを実際に見れたのが面白かったです。
すごく便利で魔法のようなフレームワークの機能たちに囲まれて開発していますが、
やっぱり一番最初と最後は僕らの知っている素のPHPの機能を使って処理をしているんだなと・・・。
今回は実際のLaravelソースコードから
重要処理のみを抜粋してできる限り読み進めやすいようにしています。
実際にはもっとたくさん分岐処理などがありますので、
自分で実物のコードを追いかけてみることをお勧めします。
Laravelのちょっと深いところを理解していきたいと思っている方、
これらの記事もおすすめですので是非読んでみてください。
(次に進む前に、LGTMしてもらえるとうれしいです)
魔法のようなLaravelも素のPHPに始まり素のPHPに終わる。Laravelのライフサイクルを簡単に解説。
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。
【Laravel】引数でタイプヒントしただけでインスタンスがもらえるのはなぜ?Laravelの魔法を解明してみる。
【Laravel】ファサードとは?何が便利か?どういう仕組みか?
参考
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。
【Laravel】引数でタイプヒントしただけでインスタンスがもらえるのはなぜ?Laravelの魔法を解明してみる。
https://readouble.com/laravel/6.x/ja/lifecycle.html
https://qiita.com/yamaji_daisuke/items/e661ffd63eefdfd178cc
https://speakerdeck.com/namizatork/laravelwakanne-kara-wan-quan-nili-jie-sita-madesutetupuatupu
-
実際は、CSSやJSなどpublicディレクトリ配下に実際に存在するファイルの場合はリダイレクトされない ↩
-
Laravelは、Symfonyという別のPHPフレームワークをベースに作られており、コードを深くたどるとSymfonyになっています
↩ -
このmake()で依存解決が行われます。依存解決とは、コンストラクタの引数で型宣言されているクラスをインスタンス化して渡してくれること(詳細はこちらの記事で解説)
$this->getControllerMethod()
の方では、
同じように
{コントローラクラス名}@{アクションメソッド名}
を「@」で分割して
{アクションメソッド名}
を文字列で返却しています。
「UserController@index」の例では、「index」の文字列が取得されることになります。
そして改めてrunController()
の中を見ると、 ↩