Laravelが文字列Hello Worldを画面に表示するまでの仕組みを理解する
IT技術
概要
Laravelに入門するために、リクエストを受け取ってからレスポンスを返すまでの仕組みをたどってみます。
ゴール
Hello Worldを題材にソースコードをざっくり読んでみることで、Laravelがどのようにリクエストをもとにレスポンスを組み立てているのか、概要を掴むことを目指します。
つまり、Laravelがどうやって動いているのか、ぼんやりとでもイメージできるようになることが目標です。
環境
- PHP8.1
- Laravel10.0
目次
- 概要
-
- ゴール
- 環境
- 目次
- 用語
- 背景
- ざっくり要約
- 方針
- 全体像を知る
- ソースコード探検
-
- エントリーポイント(public/index.php)
- Service Containerの生成(app.php)
- Service Containerの仕組み
-
- Container::singleton()-オブジェクトのつくり方を登録
- Container::bind()
- Container::make()
- Container::resolve()
- オブジェクトのつくり方を読み出す-Container::getConcrete()
- オブジェクトをつくる-Container::build()
- 補足: Closureが呼ばれてから元の処理に合流するまで何が起こっているのか
- 補足: なぜこのような周りくどい仕組みが存在しているのか
- 今度こそオブジェクトをつくる-Container::build()
- 戻り値を返すまで-Container::resolve()
- Service Containerの概要ざっくりまとめ
- Service Containerのコンストラクタ
- Service Provider
- 復習-Laravelが動く前準備が整うまで
- Request::capture()
- Kernel::handle
- Kernel::sendRequestThroughRouter()
- Kernel::dispatchToRouter
- Route
- Router::findRoute()
- Router::runRoute()
- 振り返り
- まとめ
用語
※ 用語の詳細は記事で掘り下げていきます。
- リクエスト/レスポンス: HTTPリクエスト/HTTPレスポンスを指す
- オブジェクト: クラスのインスタンスと同義
- Service Container: いわゆるDIコンテナ Laravelではオブジェクトの作成・提供を責務に持つ
- Service Provider: どういうオブジェクトをどうやってつくるのか機能単位にまとめたもの
- Kernel: 核心などを意味する Laravelにおいてはアプリケーションを制御することを責務に持つ
- Bootstrapper: 設定ファイルを読み出すなど、Laravelが動作するのに必要な準備を表す処理をまとめたもの
背景
Webアプリケーションフレームワークを理解する上で、リクエストからレスポンスを組み立てるまでの流れを知ることはとても重要です。
公式でも書かれている通り、仕組みを理解すれば自信をもってLaravelを利用したコードが書けるようになるはずです。
仕組みを知り、自信をもつということは言い換えれば不具合や課題など、さまざまな種類の問題を解決する力を手にいれることを意味します。
Laravelともっと仲良くなるためにも、リクエストを扱う一連の流れを探っていきましょう。
ざっくり要約
public/index.phpが入り口。Laravelの核となるKernelが個々の処理を制御する。
まず、Laravelが動作するのに必要なオブジェクトをService Containerを介して生成。
具体的にどのオブジェクトをつくるのかは、Service Providerによって規定されている。
そして、リクエストを処理するのに必要なオブジェクトがそろったら、KernelオブジェクトがRouteオブジェクトを介して対応する処理を呼び出す。
出来上がったレスポンスを返却することで、処理が完了。
方針
Laravelがリクエストを処理するまでの道のりは、膨大な量のコードで構成されています。
すべてを読んでいると迷子になってしまうので、最初にやりたいことを決めておきます。
今回は見る範囲が広いので、2回に分けて全体の処理をたどっていきます。
具体的には、最初にLaravelがレスポンスを生成するために何をしているのか概要を見ていきます。先に全体像を押さえておくことで、詳細を見るときに何を目的とした処理なのか掴めるようになることを目指します。
続いて、具体的な仕組みを解き明かします。細部に入り込みすぎない程度に眺めていき、ざっくりどうやって実現しているのか主要なところを押さえていきます。
全体像・仕組みを押さえることができれば、Laravelがどうやって動いているのか多少なりとも見えてくるはずです。
範囲
Laravelのアプリケーションでは、データベースやBlade Templateなどを組み合わせることで、複雑な画面や機能も実装することができます。
とはいえ入門段階でこれらも含めた仕組みを解き明かそうと思うと、範囲が広すぎて途中で折れてしまいそうです。
よって、今回はいわゆるHello Worldに相当する処理がどのように実行されるのか、理解するところを目標とします。
より具体的には以下のような、レスポンスとして文字列を返却するようなコードを見ていきます。
1// routes/web.php
2Route::get('/hello', function (Request $request) {
3// Hello Laravel from hello
4return "Hello Laravel from {$request->path()}";
5});
それでは最初に全体像を知るために、リクエストからレスポンスをつくりだすときにLaravelが何をしているのか、簡単に見ていきます。
全体像を知る
文字列をレスポンスとして返すだけでも、Laravelではたくさんの処理を見ていくことになります。
ですので、いきなりコードから探り始めると、今見ている処理が何を目的としたものなのか見えてこず、理解が曖昧になってしまうかもしれません。
そこでまずは、Laravelがリクエストを扱うときに重要なオブジェクトの名前と役割をざっくり見ておきます。
どんな責務を持った処理が何をしているのか明らかにできれば、全体像も掴めてくるはずです。
必要な責務
細かな役割を見ていく前に、「リクエストからレスポンスを組み立てる」ためにどういう処理が必要か整理しておきましょう。
処理とは具体的に何か掘り下げておくことで、これから見ていくオブジェクトが処理のどの部分を担ってくれるのかイメージできるようにします。
具体的な流れは、以下の通りです。
- リクエストをWebサーバから受け取る
- リクエストを扱うのに必要なオブジェクトを用意
- リクエスト先のURLをもとに呼び出す処理を振り分け
- 該当する処理を呼び出し、リクエストをもとにレスポンスを生成
- 出来上がったレスポンスをWebサーバへ返却
Webアプリケーションフレームワークを触ったことがあれば、なんとなくイメージできるかと思います。
Laravelがこのような処理をどうやって実現しているのか、関係する主なオブジェクトを見てみます。
Service Container-オブジェクトを用意して提供
Laravelを構成するオブジェクトを用意・提供する処理は、Service Containerなるオブジェクトが担います。
これはいわゆるDIコンテナと呼ばれるものです。DIコンテナの役割やメリットはテストコードを題材とした方がわかりやすそうですが、今回は実装までを追うのが目標なので、割愛します。
先に概要を把握しておきたい方は、参考リンクの記事を読んでみてください。
実装の観点から見るService Containerの大きな役目は、リクエストを処理していくのに必要なオブジェクトを用意・提供することです。
これだけではイメージしづらいので、最初に見たHello Worldのコードを見てみましょう。
1// routes/web.php
2Route::get('/hello', function (Request $request) {
3// Hello Laravel from hello
4return "Hello Laravel from {$request->path()}";
5});
重要なのは、メソッドの引数で渡しているClosureです。この処理はHTTPリクエストを表わすRequestオブジェクトを受け取っています。
このように欲しいクラスを書いておくだけでインスタンスが得られるのは、Service Containerの恩恵によるものです。
※ ControllerのメソッドでRequestオブジェクトを受け取る場合も同様に考えられます。
少し話が抽象的になってきたので、ひとまずはService Containerがあることで、Laravelアプリケーションではオブジェクトを用意するのが楽になるんだな、ということを覚えておきましょう。
Service Provider-Laravelが動き出すための指示書
Service Containerのおかげでオブジェクトを用意できる仕組みは整っているようです。
しかし、簡単にオブジェクトが用意できるようになっても、Laravelで扱うオブジェクトの数は膨大です。これを1つ1つ設定して準備するのは大変そうです。
そこで、Laravelではセッション管理やデータベースなど、機能を実現するためのオブジェクトをどうやって組み立てるのか表現したクラスが用意されています。
これは、機能(Service)を提供するための役割を持つことから、Service Providerと呼ばれます。
具体例として、コードが比較的シンプルなHashServiceProviderクラスを見てみましょう。このクラスは、パスワードのハッシュ化など、ハッシュ化処理に関係する機能を用意するのが役割です。
1namespace Illuminate\Hashing;
2
3use Illuminate\Contracts\Support\DeferrableProvider;
4use Illuminate\Support\ServiceProvider;
5
6class HashServiceProvider extends ServiceProvider implements DeferrableProvider
7{
8 // register()にて、機能を実現するのに必要なオブジェクトを設定
9 /*
10 * Register the service provider.
11 *
12 * @return void
13 */
14 public function register()
15 {
16 // Service Containerに指示
17 // hashという名前のオブジェクトが要求されたら、HashManagerオブジェクトをつくって渡す
18 $this->app->singleton('hash', function ($app) {
19 return new HashManager($app);
20 });
21
22 $this->app->singleton('hash.driver', function ($app) {
23 return $app['hash']->driver();
24 });
25 }
26 // 中略...
27}
HashServiceProvider::register()の役目は、機能を実現するのに必要なオブジェクトを用意しておくことです。
オブジェクトをつくるとなるとスコープが気になるところですが、Service Containerがよしなに管理してくれます。
Service Containerが管理してくれていることで、必要に応じて必要な場所でオブジェクトを取り出せるようになります。
まとめると、Service Providerの責務はService Containerを介し、Laravelの各種機能を実現するためのオブジェクトを用意することです。
Service Containerと同様、普段の実装ではあまり意識することはありませんが、Laravelを支えるとても重要なオブジェクトです。
Kernel-Laravelの核
Kernelは、Laravelのドキュメントではあまり触れられていない機能です。
ですが、Kernelという英単語が物事の核心といった意味を持つように、LaravelにおけるKernelクラスはとても重要な役割を担っています。
Kernelの責務は、Laravelアプリケーションを動かすことです。
より具体的には、これまで紹介してきたService ContainerやService Provider・そして私達が実装するアプリケーションと協力し、HTTPレスポンスなどの出力をつくりだします。
どのように協力していくのかは、ソースコードを見るときにじっくり探っていきます。
よってここでは、KernelはLaravelアプリケーションを制御する司令塔のような役目を果たしているんだな、ということを押さえておきましょう。
全体像の振り返り
ざっくりとではありますが、Laravelがリクエストを捌く上で重要な要素を見てきました。忘れないよう、簡単に復習しておきましょう。
- Laravelの機能を表現するオブジェクトを生成して取り出す処理は、Service Containerが担当
- Service Containerを利用し、どんなオブジェクトをつくっていくかはService Providerが管理
- Kernelが司令塔となってそれぞれの処理を呼び出し、リクエストからレスポンスをつくっていく
Laravelが何をしているのかは、ぼんやりと見えてきました。
よってここからは、具体的にどうやってリクエストを処理しているのか、Laravelの仕組みを覗いてみます。
ソースコード探検
Laravelのソースコードを探検しながら、仕組みを解き明かしていきます。すべてを読むと迷子になってしまうので、雰囲気を掴める程度に見てみましょう。
エントリーポイント(public/index.php)
最初の一歩として、リクエストを扱う処理の入り口であるpublic/index.phpから始めます。
nginxなどのWebサーバを利用していれば、このファイルがエントリーポイントであるのもなんとなくイメージできるかと思います。
一方、開発するときによく実行するphp artisan serveコマンドとは結びついていないように見えます。このコマンドの仕組みもこの場で解明したいところですが、入り込むと少し長くなってしまうので、補足に譲ることにします。興味があったら見てみてください。
それではソースの中身を覗いてみましょう。
Auto Loader
最初に、Laravelアプリケーションに登場するPHPクラスファイルを読み込めるよう、Auto Loaderを読み出しておきます。
1// public/index.php
2/*
3|-----------------------------------------------------------------------
4| Register The Auto Loader
5|-----------------------------------------------------------------------
6|
7| Composer provides a convenient, automatically generated class loader for
8| this application. We just need to utilize it! We'll simply require it
9| into the script here so we don't need to manually load our classes.
10|
11*/
12require __DIR__.'/../vendor/autoload.php';
Applicationクラス
続いて、bootstrap/app.phpファイルを読み出しています。
1/*
2|-----------------------------------------------------------------------
3| Run The Application
4|-----------------------------------------------------------------------
5|
6| Once we have the application, we can handle the incoming request using
7| the application's HTTP kernel. Then, we will send the response back
8| to this client's browser, allowing them to enjoy our application.
9|
10*/
11$app = require_once __DIR__.'/../bootstrap/app.php';
以降ではapp.phpへ制御が移ります。
Service Containerの生成(app.php)
app.phpで最も重要な処理は、以下のコードです。
1// bootstrap/app.php
2
3/*
4|-----------------------------------------------------------------------
5| Create The Application
6|-----------------------------------------------------------------------
7|
8| The first thing we will do is create a new Laravel application instance
9| which serves as the "glue" for all the components of Laravel, and is
10| the IoC container for the system binding all of the various parts.
11|
12*/
13$app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) );
Applicationなるオブジェクトをつくっています。これは、Service Containerを表しています。
Service Containerの理解を深めるためにも、ざっくりと何をしているのか探ってみましょう。
Applicationクラス(Service Container)
まずは呼び出されていたコンストラクタを確認してみます。
1// Illuminate/Foundation/Application.php
2class Application extends Container implements ApplicationContract, CachesConfiguration, CachesRoutes, HttpKernelInterface
3{
4 use Macroable;
5
6 // 中略...
7
8 /*
9 * Create a new Illuminate application instance.
10 *
11 * @param string|null $basePath
12 * @return void
13 */
14 public function __construct($basePath = null)
15 {
16 // 中略...
17
18 $this->registerBaseBindings();
19 $this->registerBaseServiceProviders();
20 $this->registerCoreContainerAliases();
21 }
22 // 中略...
23}
色々重要そうな処理はありますが、いきなり細部に入り込むのはやめておきます。
ここで書かれた処理をイメージできるようになるには、Service Containerの大まかな仕組みを理解しておかなければなりません。
そうなると、Service Containerの理解を深めるために何から手をつければ良いか迷ってしまいそうです。しかし実は、後続のコードにService Containerの仕組みを読み解くのにうってつけなものが書かれています。
少し先を見ておきましょう。
1// bootstrap/app.php
2// これまで見てきたService Container生成処理
3$app = new Illuminate\Foundation\Application(
4 $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
5);
6
7// 中略...
8// Service Containerを呼び出しているっぽい処理
9$app->singleton(
10 Illuminate\Contracts\Http\Kernel::class,
11 App\Http\Kernel::class
12);
13
14// 中略...
15/*
16|-----------------------------------------------------------------------
17| Return The Application
18|-----------------------------------------------------------------------
19| | 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.
20|
21*/
22
23return $app;
1// public/index.php
2use Illuminate\Contracts\Http\Kernel;
3
4// 中略...
5$app = require_once __DIR__.'/../bootstrap/app.php';
6
7// Service Containerからオブジェクトを取り出しているっぽい処理
8$kernel = $app->make(Kernel::class);
復習になりますが、Service Containerの役割は、Laravelアプリケーションで必要なオブジェクトを用意・提供することです。
上で読んだ処理は、まさに役割を体現したようなことが書かれています。
見えやすくなるよう該当する処理だけ抜き出してみます。
1// オブジェクトをどうやって用意するのか規定
2$app->singleton(
3 Illuminate\Contracts\Http\Kernel::class,
4 App\Http\Kernel::class
5);
6// Service Containerからオブジェクトを取り出す
7// ここでのKernelは、Illuminate\Contracts\Http\Kernel Interfaceを指す
8$kernel = $app->make(Kernel::class);
Application::singleton()は、Kernelオブジェクトを要求されたとき、どのオブジェクトを用意するのか決めています。
そして、Application::make()は、上の対応づけをもとに、オブジェクトを生成して呼び出し元に返しています。
オブジェクトを用意・提供している処理がどうやって実現されているのか見えてくれば、Service Containerが何をしているかも明らかにできそうです。
ということで、Service Containerを理解する一歩目として、オブジェクトの登録・取得処理の仕組みを探検してみます。
Service Containerの仕組み
Service Containerがどのように動いているのか、ざっくりと読み解いていきます。
まずは全体像を掴むために、やっていることを簡単に言葉にしておきます。
Service Containerは、自身のプロパティにオブジェクトをつくる方法を保存しておきます。
そして、インスタンスを要求されると、プロパティを参照し、オブジェクトを組み立てて返却します。
文字で書き出すとなんだか難しそうです。何をやっているのかもう少し見えやすくなるよう、それっぽいコードで動きを書き出してみます。
1// なんちゃってService Container
2class ServiceContainer
3{
4 // オブジェクトのつくり方を格納
5 private array bindings[];
6
7 // オブジェクトのつくり方を決める
8 public function set(string $abstract, string $concrete)
9 {
10 // キーに抽象的なクラス名を設定しておくことで、あとからキーでオブジェクトが取り出せるようになる
11 $this->bindings[$abstract] = $concrete;
12 }
13
14 // つくり方に従い、オブジェクトを用意して返却
15 public function get(string $abstract)
16 {
17 return new $this->bindings[$abstract];
18 }
19}
20
21$app = new ServiceContainer();
22// KernelInterfaceという名前のオブジェクトを要求されたら、具体的なクラスからオブジェクトをつくって返すよう指示
23$app->set(KernelInteface::class, Kernel::class);
24// 要求をもとにオブジェクトを生成して返却
25$kernel = $app->get(KernelInterface::class);
26
27// 得られたオブジェクトでアプリケーションの処理を実行
28$kernel->handle();
オブジェクトのつくり方をあらかじめ決めておき、必要になったらつくって渡す。これがService Containerの主要な処理です。
それではLaravelでは具体的にどんなコードで実現しているのか、解き明かしていきましょう。
Container::singleton()-オブジェクトのつくり方を登録
app.phpでは、オブジェクトのつくり方を登録する処理としてContainer::singleton()を呼び出しています。
この処理がどうやって動作しているのか追ってみたいと思います。
1// bootstrap/app.php
2$app->singleton(
3 Illuminate\Contracts\Http\Kernel::class,
4 App\Http\Kernel::class
5);
まずはメソッド定義を入り口とします。
※ Service Containerとしての機能は、Applicationクラスの継承元であるIlluminate\Container\Containerクラスに書かれています。よって、以降ではContainerクラスを中心に見ていきます。
1// Illuminate/Container/Container.php
2
3/*
4 * Register a shared binding in the container.
5 *
6 * @param string $abstract
7 * @param \Closure|string|null $concrete
8 * @return void
9 */
10public function singleton($abstract, $concrete = null)
11{
12 $this->bind($abstract, $concrete, true);
13}
Container::bind()へと続きます。
今回の場合、変数abstractはInterface名・concreteはオブジェクトをつくる対象の完全修飾クラス名・そして第3引数はオブジェクトをシングルトンとして共有することを表現しています。
Container::bind()
オブジェクトの生成方法を決定するメソッドを見てみます。
1/*
2 * Register a binding with the container.
3 *
4 * @param string $abstract
5 * @param \Closure|string|null $concrete
6 * @param bool $shared
7 * @return void
8 *
9 * @throws \TypeError
10 */
11public function bind($abstract, $concrete = null, $shared = false)
12{
13 // 中略...
14
15 // オブジェクトのつくり方が決まっていない場合、デフォルトの生成方法を取得
16 // If the factory is not a Closure, it means it is just a class name which is
17 // bound into this container to the abstract type and we will just wrap it
18 // up inside its own Closure to give us more convenience when extending.
19 if (! $concrete instanceof Closure) {
20 if (! is_string($concrete)) {
21 throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
22 }
23
24 $concrete = $this->getClosure($abstract, $concrete);
25 }
26
27 // オブジェクトの生成方法をbindingsプロパティへ保存しておく
28 $this->bindings[$abstract] = compact('concrete', 'shared');
29
30 // 中略...
31}
今回はクラス名の文字列だけを渡しているので、オブジェクトの生成方法はService Containerが規定しているデフォルトが設定されます。
デフォルトの設定方法はContainer::getClosure()に定義されているので、中身を確認しておきます。
1/*
2 * Get the Closure to be used when building a type.
3 *
4 * @param string $abstract
5 * @param string $concrete
6 * @return \Closure
7 */
8protected function getClosure($abstract, $concrete)
9{
10 // Container::resolve()を呼び出していると理解しておけばOK
11 return function ($container, $parameters = []) use ($abstract, $concrete) {
12 if ($abstract == $concrete) {
13 return $container->build($concrete);
14 }
15
16 return $container->resolve(
17 $concrete, $parameters, $raiseEvents = false
18 );
19 };
20}
関数を返す関数でぱっと見難しそうですが、ひとまずはオブジェクトをつくるときにContainer::resolve()を呼び出しているんだな、ということだけを覚えておきましょう。
1// オブジェクトの生成方法をbindingsプロパティへ保存しておく
2$this->bindings[$abstract] = compact('concrete', 'shared');
compact関数は変数名から配列を生成するためのものです。
参考
ですので、bindingsプロパティは以下のような構造となります。
1$bindings = [
2 'Illuminate\Contracts\Http\Kernel::class' => [
3 'concrete' => 'Container::resolve()を呼び出すClosure',
4 'shared' => true
5 ]
6];
つまり、Container::bind()が呼ばれると、オブジェクトをどうやってつくるのかが決定されます。
Container::make()
app.phpでContainer::singleton()を呼び出すことで、オブジェクトのつくり方が定まりました。
そして、呼び出し元のindex.phpで早速オブジェクトが必要になったので、Container::make()でオブジェクトを取り出しています。
1// public/index.php
2$kernel = $app->make(Kernel::class);
つくり方をもとに、Service Containerがどうやってオブジェクトを生成しているのか、探ってみましょう。
まずは例のごとくメソッド定義へ移動するところから始めます。
1// Illuminate/Container/Container.php
2
3/*
4 * Resolve the given type from the container.
5 *
6 * @param string|callable $abstract
7 * @param array $parameters
8 * @return mixed
9 *
10 * @throws \Illuminate\Contracts\Container\BindingResolutionException
11 */
12public function make($abstract, array $parameters = [])
13{
14 return $this->resolve($abstract, $parameters);
15}
引数abstractには、クラスの完全修飾名を表す文字列Illuminate\Contracts\Http\Kernelが指定されています。
オブジェクトをつくるときの引数parametersは今回は空なので、気にしないでおきます。
さて、実際にオブジェクトを組み立てているContainer::resolve()を読んでいきます。
Container::resolve()
1/*
2 * Resolve the given type from the container.
3 *
4 * @param string|callable $abstract
5 * @param array $parameters
6 * @param bool $raiseEvents
7 * @return mixed
8 *
9 * @throws \Illuminate\Contracts\Container\BindingResolutionException
10 * @throws \Illuminate\Contracts\Container\CircularDependencyException
11 */
12protected function resolve($abstract, $parameters = [], $raiseEvents = true)
13{
14 // オブジェクトのつくり方を表すconcrete変数を設定
15 // 中略...
16 $concrete = $this->getContextualConcrete($abstract);
17 // 中略...
18 if (is_null($concrete)) {
19 $concrete = $this->getConcrete($abstract);
20 }
21
22 // concrete変数をもとにオブジェクトを生成
23 // We're ready to instantiate an instance of the concrete type registered for
24 // the binding. This will instantiate the types, as well as resolve any of
25 // its "nested" dependencies recursively until all have gotten resolved.
26 if ($this->isBuildable($concrete, $abstract)) {
27 $object = $this->build($concrete);
28 } else {
29 $object = $this->make($concrete);
30 }
31
32 // Container::singleton()経由でオブジェクトをつくった場合、1つのオブジェクトを全体で共有できるよう設定
33 // If the requested type is registered as a singleton we'll want to cache off
34 // the instances in "memory" so we can return it later without creating an
35 // entirely new instance of an object on each subsequent request for it.
36 if ($this->isShared($abstract) && ! $needsContextualBuild) {
37 $this->instances[$abstract] = $object;
38 }
39 // 中略
40
41 return $object;
42}
少し長めの処理なので、段階的に見ていきます。
まず、Container::resolve()の目的は、指定された情報(今回はクラス名)をもとにオブジェクトをつくって返すことです。
これを実現するために、binding、つまりつくり方を探し出し、得られたものに従ってオブジェクトを組み立てています。
それぞれの動作を簡単に見てみます。
オブジェクトのつくり方を読み出す-Container::getConcrete()
※ contextualConcreteなるものは、参照元に応じて返すオブジェクトを切り替えるための仕組みです。今回は関係しないことから割愛します。
concrete変数はnullと評価されるので、Container::getConcrete()が呼ばれます。
このメソッドでやっていることは非常にシンプルで、Container::bind()で登録したClosureを読み出しています。登録されたClosureはオブジェクトのつくり方を表しています。
1/*
2 * Get the concrete type for a given abstract.
3 *
4 * @param string|callable $abstract
5 * @return mixed
6 */
7protected function getConcrete($abstract)
8{
9 // If we don't have a registered resolver or concrete for the type, we'll just
10 // assume each type is a concrete name and will attempt to resolve it as is
11 // since the container should be able to resolve concretes automatically.
12 if (isset($this->bindings[$abstract])) {
13 return $this->bindings[$abstract]['concrete'];
14 }
15
16 return $abstract;
17}
オブジェクトをつくる-Container::build()
つくり方を表すレシピが手元に揃ったので、いよいよオブジェクトをつくる処理を探ります。
ここから少し難しくなるので、流れを1つ1つじっくりと見ていきます。
1// concreteはオブジェクトを生成するためのClosure
2// abstractはつくりたいオブジェクトと対応するクラス名を表す
3
4// We're ready to instantiate an instance of the concrete type registered for
5// the binding. This will instantiate the types, as well as resolve any of
6// its "nested" dependencies recursively until all have gotten resolved.
7if ($this->isBuildable($concrete, $abstract)) {
8 $object = $this->build($concrete);
9} else {
10 $object = $this->make($concrete);
11}
1/*
2 * Determine if the given concrete is buildable.
3 *
4 * @param mixed $concrete
5 * @param string $abstract
6 * @return bool
7 */
8protected function isBuildable($concrete, $abstract)
9{
10 return $concrete === $abstract || $concrete instanceof Closure;
11}
Container::isBuildable()はconcrete(getConcreteメソッドで得られたもの)がClosureであることからtrueが返ります。
よって、インスタンスをつくる処理は、Container::build()が主要な役割を担っています。
ということで、Container::build()が何をしているか、明らかにしていきます。
1/*
2 * Instantiate a concrete instance of the given type.
3 *
4 * @param \Closure|string $concrete
5 * @return mixed
6 *
7 * @throws \Illuminate\Contracts\Container\BindingResolutionException
8 * @throws \Illuminate\Contracts\Container\CircularDependencyException
9 */
10public function build($concrete)
11{
12 // If the concrete type is actually a Closure, we will just execute it and
13 // hand back the results of the functions, which allows functions to be
14 // used as resolvers for more fine-tuned resolution of these objects.
15 if ($concrete instanceof Closure) {
16 return $concrete($this, $this->getLastParameterOverride());
17 }
18 // 中略
19}
どうやら引数がClosureであった場合、呼び出してすぐに値を返してしまうようです。
この処理を詳細に読み解こうとすると、関数の中で呼ばれる関数の戻り値の戻り値を考えるようになり、非常に難しくなってしまいます。
よって、ここでは、最終的にどうなるのか結果だけを書いておきます。なぜこのような構造を持ち、何が起こっているのかは補足に譲ることにします。
一連の処理は最終的に、具体的なクラス名App\Console\Kernelを引数にContainer::build()を呼び出すことで元の処理に合流します。
補足: Closureが呼ばれてから元の処理に合流するまで何が起こっているのか
しっかりと理解しておきたい方のために、簡単に処理の流れを記しておきます。
まず、そもそも呼び出されるClosureがどのようなものであったか復習するところから始めます。
1// abstractはIlluminate\Contracts\Http\Kernel::class
2// concreteはApp\Http\Kernel::class
3protected function getClosure($abstract, $concrete)
4{
5 return function ($container, $parameters = []) use ($abstract, $concrete) {
6 // 中略
7 // ここでのconcreteは、クラス名App\Http\Kernel
8 return $container->resolve(
9 $concrete, $parameters, $raiseEvents = false
10 );
11 };
12}
なんと、Container::resolve()の中でさらにContainer::resolve()が呼ばれています。
なんだか無限ループしそうに見えますが、実際には呼ばれるときの引数が異なります。
Closureで呼び出すContainer::resolve()は、具象クラス名App\Console\Kernelを引数とします。
一方、Container::make()から呼ばれるときは、Interface名Illuminate\Contracts\Console\Kernelが渡されます。
この違いは、Container::build()へ渡す引数である、変数concreteを組み立てる処理に表れます。
変数concreteを生成しているメソッドContainer::getConcrete()を見てみましょう。
1protected function getConcrete($abstract)
2{
3 // If we don't have a registered resolver or concrete for the type, we'll just
4 // assume each type is a concrete name and will attempt to resolve it as is
5 // since the container should be able to resolve concretes automatically.
6 if (isset($this->bindings[$abstract])) {
7 return $this->bindings[$abstract]['concrete'];
8 }
9
10 return $abstract;
11}
引数である具象クラス名App\Console\Kernelはbindingsプロパティに登録されていないことから、そのまま戻り値となります。
すると、そのまま後続へと処理が進んでいき、Container::build()がClosureではなく、具象クラス名App\Console\Kernelを引数に呼ばれるようになります。
1// Container::resolve()の一部
2
3// concreteは文字列App\Console\Kernelとなる
4if (is_null($concrete)) {
5 $concrete = $this->getConcrete($abstract);
6}
7
8// Container::isBuildable()は、concreteとabstractが同一でもtrueを返す
9// よって、文字列App\Console\Kernelを引数にContainer::build()が呼ばれる
10// We're ready to instantiate an instance of the concrete type registered for
11// the binding. This will instantiate the types, as well as resolve any of
12// its "nested" dependencies recursively until all have gotten resolved.
13if ($this->isBuildable($concrete, $abstract)) {
14 $object = $this->build($concrete);
15} else {
16 $object = $this->make($concrete);
17}
補足: なぜこのような周りくどい仕組みが存在しているのか
Container::build()を見ていると、Container::resolve()が2回呼ばれていることが分かりました。
なぜこのような周りくどい処理をしているのでしょうか。
単純に考えれば、以下のコードのようにClosureの中でContainer::build()を呼べば済む話のように思えます。
1// abstractはIlluminate\Contracts\Http\Kernel::class
2// concreteはApp\Http\Kernel::class
3protected function getClosure($abstract, $concrete)
4{
5 return function ($container, $parameters = []) use ($abstract, $concrete) {
6 // 中略
7 // ここでのconcreteは、クラス名App\Http\Kernel
8 return $container->build($concrete);
9 };
10}
実は、Laravelのコードのコメントに経緯は書かれていました。該当箇所を見てみます。
1// Container::bind()を抜粋
2
3// If the factory is not a Closure, it means it is just a class name which is
4// bound into this container to the abstract type and we will just wrap it
5// up inside its own Closure to give us more convenience when extending.
6
7// 変数concreteがClosureでないなら、それは抽象型に対する具象型である。
8// 「extending」にて利便性を確保するために、Closureで包んでおく
9if (! $concrete instanceof Closure) {
10 if (! is_string($concrete)) {
11 throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
12 }
13
14 $concrete = $this->getClosure($abstract, $concrete);
15}
どうやら、「extending」なる処理の都合でこのような複雑な構造が必要であるようです。
詳細は参考リンクに譲りますが、既にできあがったオブジェクトを利用し、さらに処理を拡張したオブジェクトをつくることをextendingと呼ぶようです。
このとき、既存のオブジェクトを参照する上で、Container::resolve()が既存のオブジェクトと対応するクラス名でも呼ばれるようになっていると、シンプルに扱えるようになります。
具体的には、以下のコードにて既存のオブジェクトが参照されます。
1// Container::resolve()を抜粋
2
3// 既存のオブジェクトを得る
4// We're ready to instantiate an instance of the concrete type registered for
5// the binding. This will instantiate the types, as well as resolve any of
6// its "nested" dependencies recursively until all have gotten resolved.
7if ($this->isBuildable($concrete, $abstract)) {
8 $object = $this->build($concrete);
9} else {
10 $object = $this->make($concrete);
11}
12
13// 既存のオブジェクトを引数に、拡張したオブジェクト生成処理(Closure)を呼び出す
14// If we defined any extenders for this type, we'll need to spin through them
15// and apply them to the object being built. This allows for the extension
16// of services, such as changing configuration or decorating the object.
17foreach ($this->getExtenders($abstract) as $extender) {
18 $object = $extender($object, $this);
19}
今度こそオブジェクトをつくる-Container::build()
さて、Container::build()は呼び出す処理が少々複雑であることから回り道をしてしまいました。
ようやく本筋の処理に戻って来たので、改めて見るべきことを整理しておきます。
ここからは、App\Console\Kernelオブジェクトがどうやってつくられるのか、たどっていきます。
まずはコードから流れを掴んでおきます。
1/*
2 * Instantiate a concrete instance of the given type.
3 *
4 * @param \Closure|string $concrete
5 * @return mixed
6 *
7 * @throws \Illuminate\Contracts\Container\BindingResolutionException
8 * @throws \Illuminate\Contracts\Container\CircularDependencyException
9 */
10public function build($concrete)
11{
12 // concreteは文字列「App\Console\Kernel」
13 // 文字列で動的にクラスを操作するReflectionと呼ばれる仕組みを使えるようにする
14 try {
15 $reflector = new ReflectionClass($concrete);
16 } catch (ReflectionException $e) {
17 throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
18 }
19
20 // 対象のクラスのコンストラクタを取得
21 $constructor = $reflector->getConstructor();
22
23 // コンストラクタが無いのであれば、シンプルにオブジェクトをつくって返却
24 // If there are no constructors, that means there are no dependencies then
25 // we can just resolve the instances of the objects right away, without
26 // resolving any other types or dependencies out of these containers.
27 if (is_null($constructor)) {
28 array_pop($this->buildStack);
29
30 return new $concrete;
31 }
32
33 // コンストラクタの引数を解決
34 // 引数がクラスであれば再帰的に解決
35 $dependencies = $constructor->getParameters();
36
37 // Once we have all the constructor's parameters we can create each of the
38 // dependency instances and then use the reflection instances to make a
39 // new instance of this class, injecting the created dependencies in.
40 try {
41 $instances = $this->resolveDependencies($dependencies);
42 } catch (BindingResolutionException $e) {
43 array_pop($this->buildStack);
44
45 throw $e;
46 }
47
48 // 最終的に、コンストラクタを引数つきで呼び出し、得られたオブジェクトを返却
49 return $reflector->newInstanceArgs($instances);
50}
Container::build()の動作は一言でまとめると、動的にオブジェクトを生成することです。
動的にクラスを操作するために、Reflectionと呼ばれる仕組みを呼び出しています。
対象のクラスがコンストラクタで更にクラスを参照していると、少し処理が複雑になりますが、ここではひとまずクラス名をもとにオブジェクトをつくっているんだな、ということを押さえておきましょう。
戻り値を返すまで-Container::resolve()
Container::build()にて、欲しかったオブジェクトを手にいれることができました。
あとは呼び出し元に返すまでの流れを簡単に見ておきましょう。
1// Container::resolve()抜粋
2
3// concrete変数をもとにオブジェクトを生成
4// 変数$objectにつくられたオブジェクトが格納される
5// We're ready to instantiate an instance of the concrete type registered for
6// the binding. This will instantiate the types, as well as resolve any of
7// its "nested" dependencies recursively until all have gotten resolved.
8if ($this->isBuildable($concrete, $abstract)) {
9 $object = $this->build($concrete);
10} else {
11 $object = $this->make($concrete);
12}
13
14// Container::singleton()経由でオブジェクトをつくった場合、1つのオブジェクトを全体で共有できるよう設定
15// If the requested type is registered as a singleton we'll want to cache off
16// the instances in "memory" so we can return it later without creating an
17// entirely new instance of an object on each subsequent request for it.
18if ($this->isShared($abstract) && ! $needsContextualBuild) {
19 $this->instances[$abstract] = $object;
20}
21// 中略
22
23return $object;
注目すべきは、Container::singleton()経由でオブジェクトを登録していた場合、Containerクラスのinstancesプロパティへオブジェクトを保存しているところです。
このことから、Service Containerはオブジェクトを取り出すとき、シングルトンである場合は一度つくったものを共有し、通常は新しくオブジェクトをつくって返していることが分かります。
Service Containerの概要ざっくりまとめ
とても長い道のりでしたが、ようやくService Containerの全体像が見えてきました。
忘れないよう理解したことを簡単にまとめておきましょう。
- Container::singleton()やContainer::bind()でオブジェクトのつくり方をService Containerに保存しておく
- Container::make()が呼ばれると、つくり方に基づいてオブジェクトを生成
つくり方をあらかじめ決めておいて、必要になったらつくる。言葉にするとシンプルですね。
Service Containerのコンストラクタ
さて、Service Containerの仕組みを見てきたのは、コンストラクタで何をしているのか理解するためでした。
ここまでの道のりを思い出すために、見ていた処理をもう一度振り返ります。
1// エントリーポイントであるpublic/index.phpにてapp.phpを読込
2$app = require_once __DIR__.'/../bootstrap/app.php';
3
4// app.phpにて、Service Containerを表すクラスのインスタンスを生成
5$app = new Illuminate\Foundation\Application(
6 $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
7);
これまでは、Service Containerの仕組みを理解するために、その先の処理であるApp\Http\Kernelオブジェクトをつくるところを見てきました。
仕組みが見えてくればApplicationクラスのコンストラクタも読めるようになっているはずです。
早速中身を覗いてみましょう。
1// Illuminate/Foundation/Application.php
2// 先ほどまで見ていたContainerクラスを継承
3class Application extends Container implements ApplicationContract, CachesConfiguration, CachesRoutes, HttpKernelInterface
4{
5 use Macroable;
6
7 /*
8 * Create a new Illuminate application instance.
9 *
10 * @param string|null $basePath
11 * @return void
12 */
13 public function __construct($basePath = null)
14 {
15 if ($basePath) {
16 $this->setBasePath($basePath);
17 }
18
19 $this->registerBaseBindings();
20 $this->registerBaseServiceProviders();
21 $this->registerCoreContainerAliases();
22 }
コンストラクタで肝となるのは、registerから始まる3つのメソッドです。
それぞれが何をしているか把握できれば、Service Containerへ入門できたと思って良さそうです。
概要を探っていきます。
Application::registerBaseBinding()
ここでのbindingsは、オブジェクトまたはオブジェクトのつくり方を表します。汎用的な機能を表すオブジェクトをService Containerへ登録しておくのがメソッドの役目です。
1/*
2 * Register the basic bindings into the container.
3 *
4 * @return void
5 */
6protected function registerBaseBindings()
7{
8 static::setInstance($this);
9
10 // Container::instance()は、既存のインスタンスをinstancesプロパティへ設定し、Service Containerから参照できるようにする
11 $this->instance('app', $this);
12
13 $this->instance(Container::class, $this);
14 $this->singleton(Mix::class);
15
16 $this->singleton(PackageManifest::class, fn () => new PackageManifest(
17 new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
18 ));
19}
$this->singleton(Mix::class);のように引数が1つしかない場合、Service Containerのデフォルトの挙動にて、そのままオブジェクトがつくられます。
一方、PackageManifestクラスに関する設定は少し複雑そうです。何をやっているのか言葉にして整理しておきましょう。
第2引数がClosureであることから、bindings(インスタンスのつくり方)へそのまま登録されます。
$app->make(PackageManifest::class)のようにオブジェクトを取り出すタイミングにて、Closureが実行されます。
より具体的には、Closureに記述された方法でPackageManifestオブジェクトがつくられ、そのまま呼び出し元に返されます。
さまざまな呼び出し方を見ることで、Service Containerの仕組みも少しずつ見えてきたのではないでしょうか。
Application::registerCoreContainerAliases
※ Service Provider周りの話は長くなるので、先にこちらを見ておきます。
ここでの処理は、Service Containerに登録されたオブジェクトに別名をつけています。
1/*
2 * Register the core class aliases in the container.
3 *
4 * @return void
5 */
6public function registerCoreContainerAliases()
7{
8 // Interfaceや継承元クラス名でもオブジェクトを参照できるようエイリアス(別名)を登録しておく
9 foreach ([
10 'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
11 // 中略...
12 'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
13 'session' => [\Illuminate\Session\SessionManager::class],
14 'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
15 'url' => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
16 'validator' => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
17 'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
18 ] as $key => $aliases) {
19 foreach ($aliases as $alias) {
20 $this->alias($key, $alias);
21 }
22 }
23}
具体例で見てみましょう。
先ほど見ていたApplication::registerBaseBinding()にて、appという名前でApplicationクラスのインスタンスを登録していました。
今回の処理と合わさると、以下のコードのように動作します。
1// $appはService Containerを指す
2$container = $app->make(\Illuminate\Contracts\Container\Container::class);
3$containerInterface = $app->make(\Psr\Container\ContainerInterface::class);
4
5// いずれも同一のApplicationオブジェクトが返却される
別名をつけておくことで、Interface名など、ほかの識別子でもオブジェクトにアクセスできるようになります。
Application::registerBaseServiceProviders()
このメソッドは、Application::register()を呼び出して各種Service Providerを設定しているようです。
1/*
2 * Register all of the base service providers.
3 *
4 * @return void
5 */
6protected function registerBaseServiceProviders()
7{
8 $this->register(new EventServiceProvider($this));
9 $this->register(new LogServiceProvider($this));
10 $this->register(new RoutingServiceProvider($this));
11}
Service Providerは、Service Containerが掴めていればサクッと理解できるはずです。
ということで、ここからはService Providerについて詳しく見ていきます。
Service Provider
Service ContainerとService Providerが連携している処理を見ていくことで、Service Providerの仕組みを解き明かしていきます。
上で見たコードでは3つのService Providerが登録されているようですが、今回はリクエストを扱う処理と関係が深そうなRoutingServiceProviderに絞って見ていきます。
復習-Service Providerとは
Service Providerの仕組みを見ていく前に、役割を簡単に復習しておきます。
Service Providerは、アプリケーションが動作するのに必要な機能を用意するのが役目です。具体的に何をしているのか、それっぽいコードから動きを確認しておきます。
1// なんちゃってService Provider
2class ServiceProvider
3{
4 // Service Containerを参照できるようにしておく
5 private Application $app;
6 public function __construct(Application $app) {
7 $this->app = $app;
8 }
9
10 public function register()
11 {
12 // 必要なオブジェクトのつくり方をService Containerへ登録
13 $this->app->singleton(RouterInterface::class, Router::class);
14 }
15}
16
17$serviceProvider = new ServiceProvider($app);
18// ServiceProvider::register()を呼ぶことで、アプリケーションが動作するのに必要な機能がServiceContainerへ用意される
19$serviceProvider->register();
Service Providerを通して、機能を表現するオブジェクトをService Containerへ登録しています。
こうしておくことで、Laravelアプリケーションが動作するための準備を整えることができます。
Application::register()
Service Providerの全体像が見えてきたところで、仕組みへと入り込んでいきます。
まずはService Providerを参照しているApplication::register()を見てみます。
1// Illuminate/Foundation/Application.php
2
3/*
4 * Register a service provider with the application.
5 *
6 * @param \Illuminate\Support\ServiceProvider|string $provider
7 * @param bool $force
8 * @return \Illuminate\Support\ServiceProvider
9 */
10public function register($provider, $force = false)
11{
12 // 中略...
13
14 // Service Providerオブジェクトのregisterメソッドを呼び出す
15 $provider->register();
16
17 // 中略...
18
19 // Service Containerにて、後からboot処理(後述)を呼び出せるようService Providerを保持しておく
20 $this->markAsRegistered($provider);
21
22 // If the application has already booted, we will call this boot method on
23 // the provider class so it has an opportunity to do its boot logic and
24 // will be ready for any usage by this developer's application logic.
25 if ($this->isBooted()) {
26 $this->bootProvider($provider);
27 }
28
29 return $provider;
30}
Application::register()の処理は、大きく2つに分かれています。
1つはbootと呼ばれる処理なのですが、入門段階ではあまり触れる機会が無さそうなので、補足へ譲ります。
重要なのは、もう1つの処理である、ServiceProvider::register()を呼び出しているところです。
ServiceProvider::register()
ここでは、ServiceProvider::register()が何をしているのか改めて見ていきます。具体例として、RoutingServiceProviderを対象とします。
1// Illuminate/Routing/RoutingServiceProvider.php
2
3class RoutingServiceProvider extends ServiceProvider
4{
5 /*
6 * Register the service provider.
7 *
8 * @return void
9 */
10 public function register()
11 {
12 $this->registerRouter();
13 $this->registerUrlGenerator();
14 $this->registerRedirector();
15 // 中略...
16 }
17 // 中略...
18}
RoutingServiceProvider::register()は、たくさんのregisterから始まるメソッドを呼び出しています。
今回は代表例として、一番上のメソッドだけを覗いておきます。
1/*
2 * Register the router instance.
3 *
4 * @return void
5 */
6protected function registerRouter()
7{
8 $this->app->singleton('router', function ($app) {
9 return new Router($app['events'], $app);
10 });
11}
Service Providerの役割の通り、Service ContainerへLaravelアプリケーションの機能を表現するオブジェクトのつくり方を設定しています。
繰り返しになりますが、Service Providerはひとまずは、Laravelアプリケーションの機能を表現するオブジェクトのつくり方を登録していくのが責務である。ということを頭に入れておきましょう。
補足: ServiceProvider::boot()について
Service Providerではもう1つ、bootなるメソッドが呼ばれるようになっています。
これは、Laravelの設定ファイルなどが読まれた後、つまりLaravelアプリケーションが動き出す準備が整ったときに呼び出されます。
あまり使う機会は多くありませんが、例えばほかのService Providerで登録されたオブジェクトで何か処理をしたいなど、自分自身以外と連携するときに有用です。
更に、今回見ていく処理で具体例を挙げると、routes/web.phpに書いたRoute::get()などで設定した情報を読み出すときにboot処理が関わっています。
ほかの機能と連携するための仕組みが用意されているんだな、ということだけまずは意識しておきましょう。
復習-Laravelが動く前準備が整うまで
さてさて、Laravelアプリケーションの準備段階で既に途方もない旅路となってしまいました。
ここからいよいよ実際にレスポンスを組み立てるところに移れるのですが、その前に来た道を振り返っておきましょう。
具体的には、Laravelアプリケーションのエントリーポイントから前準備が終わるまでを通しで見ていきます。
Service Container・Service Providerの役割が頭に入っていれば、きっと流れが掴めるはずです。
1// public/index.php
2
3use Illuminate\Contracts\Http\Kernel;
4
5require __DIR__.'/../vendor/autoload.php';
6
7$app = require_once __DIR__.'/../bootstrap/app.php';
8
9// Service Containerからオブジェクトを取り出す
10$kernel = $app->make(Kernel::class);
11
12// これから見ていく処理
13$response = $kernel->handle(
14 $request = Request::capture()
15)->send();
1// bootstrap/app.php
2
3// Service Containerを用意
4// このときに基本的な機能を表すオブジェクトがService Providerなどを通じてService Containerへ登録される
5$app = new Illuminate\Foundation\Application(
6 $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
7);
8
9// Kernel Interfaceを実装したオブジェクトのつくり方を規定
10$app->singleton(
11 Illuminate\Contracts\Http\Kernel::class,
12 App\Http\Kernel::class
13);
14
15// Service Containerを読み込み元へ返却
16/*
17|-----------------------------------------------------------------------
18| Return The Application
19|-----------------------------------------------------------------------
20|
21| This script returns the application instance. The instance is given to
22| the calling script so we can separate the building of the instances
23| from the actual running of the application and sending responses.
24|
25*/
26return $app;
コードに書かれていることを箇条書きでも整理しておきましょう。
- Auto LoaderでPHPクラスを読み込む準備をしておく
- Service Containerを用意
- Laravelの基本的な機能をService Providerなどを介してService Containerへ登録
- Laravelアプリケーションの核となるKernelオブジェクトをService Containerから取り出せるよう設定
Laravelアプリケーションの準備段階の処理を見てきたことで、残すところ理解したいコードは以下の3行だけです。
1$response = $kernel->handle(
2 $request = Request::capture()
3)->send();
当然、内部ではたくさんの処理が呼ばれているので、要点をかいつまんで少しずつ読み解いていきましょう。
Request::capture()
まずは、HTTPリクエストを表すオブジェクトをつくっている処理です。LaravelではどうやってHTTPリクエストを扱おうとしているのか、理解することを目指します。
見ていく処理は変わりましたが読み方は変わらず、メソッド定義へ移動するところから始めます。
1// Illuminate/Http/Request.php
2
3/*
4 * Create a new Illuminate HTTP request from server variables.
5 *
6 * @return static
7 */
8public static function capture()
9{
10 static::enableHttpMethodParameterOverride();
11
12 return static::createFromBase(SymfonyRequest::createFromGlobals());
13}
重要なのは、PHPDocに書かれている文です。
LaravelにおけるHTTPリクエストオブジェクトは、server variablesなるものからつくられているようです。
ここでのserver variablesは、$_GETや$_POSTのようなWebサーバから受け取ったものを元に構築されているものを指します。
つまり、いわゆるPHPにおけるSuperglobalsから生のHTTPリクエストの内容を参照しています。
Symfonyとは
1SymfonyRequest::createFromGlobals()
先ほどのコードにもあった通り、リクエストオブジェクトは、SymfonyRequestと名付けられたオブジェクトをもとにしているようです。
ゆえに、LaravelのHTTPリクエストオブジェクトがどのようなものであるか理解するには、Symfonyが何か明確にしておかなければなりません。
Symfonyそのものは、Laravelと同じくWebアプリケーションフレームワークです。
フレームワークの中で別のフレームワークをもとにしていると考えると、なんだか混乱してきそうです。もう少し詳しく見てみます。
LaravelとSymfonyの関係
Symfonyは、HTTPリクエストやRouterなど、Webアプリケーションに共通する要素を独立して扱えるよう切り出した、Symfony Componentsを提供しています。
参考
そしてLaravelでは、一部の機能をSymfony Componentsを拡張することで実現しています。
参考
まとめると、LaravelはHTTPリクエストオブジェクトを、Symfony Componentsとして提供されているものを拡張することで実装しているようです。
LaravelはSymfony Componentsを利用することで、いわゆる車輪の再発明を経ることなく、Webアプリケーションフレームワークとして必要な機能を実現しています。
SymfonyRequest::createFromGlobals()
Symfonyの正体が見えてきたので、呼ばれる処理を順に追いながら、リクエストオブジェクトがつくられる仕組みを見ていきます。
まずはSymfonyがどうやってHTTPリクエストオブジェクトをつくっているのか、俯瞰しておきます。
1// Symfony/Component/HttpFoundation/Request.php
2// ※ SymfonyRequestという名前は、Laravel側でSymfonyのRequestクラスを参照するときのエイリアス名
3/*
4 * Creates a new request with values from PHP's super globals.
5 */
6public static function createFromGlobals(): static
7{
8 $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
9
10 // 中略...
11
12 return $request;
13}
Webサーバから渡される情報をもとにPHPが構築する変数Superglobalsから、リクエストオブジェクトを作成しています。
ここでは、リクエストオブジェクトがあることで、クエリ文字列やPOSTボディを扱いやすくなっているんだな、ということを押さえておきましょう。
Request::createFromBase()
Symfonyのリクエストオブジェクトは出来上がったので、続いてはLaravelが扱うリクエストオブジェクトへどうやって変換されていくのかを見ていきます。
流れを思い出すために、もう一度Request::capture()を見直しておきます。
1public static function capture()
2{
3 static::enableHttpMethodParameterOverride();
4
5 // Symfony RequestオブジェクトをもとにRequest::createFromBase()を呼び出す
6 return static::createFromBase(SymfonyRequest::createFromGlobals());
7}
どうやらSymfonyが提供しているHTTPリクエストオブジェクトをベースに、Laravelが扱うHTTPリクエストオブジェクトをつくり出しているようです。
実際の処理の流れも確認しておきましょう。
1 /*
2 * Create an Illuminate request from a Symfony instance.
3 *
4 * @param \Symfony\Component\HttpFoundation\Request $request
5 * @return static
6 */
7 public static function createFromBase(SymfonyRequest $request)
8 {
9 // Symfonyのリクエストオブジェクトをもとに、Illuminate\Http\Requestオブジェクトを作成
10 $newRequest = (new static)->duplicate(
11 $request->query->all(), $request->request->all(), $request->attributes->all(),
12 $request->cookies->all(), $request->files->all(), $request->server->all()
13 );
14
15 // 中略...
16
17 return $newRequest;
18 }
クエリ文字列やクッキーといったHTTPリクエストに関する情報をSymfonyのリクエストオブジェクトから、Laravelのリクエストオブジェクトへ詰め替えています。
こうすることで、Symfony Componentsであるリクエストオブジェクトを拡張した、Laravelが扱うHTTPリクエストオブジェクトを手にいれることができます。
Kernel::handle
ここまでの処理で、アプリケーションを制御するKernelオブジェクトと、HTTPリクエストを表すリクエストオブジェクトがそろいました。
つまり、ようやくHTTPリクエストをもとにHTTPレスポンスを組み立てる処理を見ることができます。
1// public/index.php
2$response = $kernel->handle(
3 $request = Request::capture()
4)->send();
ということで、Kernel::handle()へと足を踏み入れていきましょう。
1// Illuminate/Foundation/Http/Kernel.php
2
3class Kernel implements KernelContract
4{
5 use InteractsWithTime;
6
7 // これらのプロパティはService Containerにて設定される
8 // Service Container
9 /*
10 * The application implementation.
11 *
12 * @var \Illuminate\Contracts\Foundation\Application
13 */
14 protected $app;
15
16 // URLと紐づく処理を探索するオブジェクト
17 /**
18 * The router instance.
19 *
20 * @var \Illuminate\Routing\Router
21 */
22 protected $router;
23
24 /**
25 * Handle an incoming HTTP request.
26 *
27 * @param \Illuminate\Http\Request $request
28 * @return \Illuminate\Http\Response
29 */
30 public function handle($request)
31 {
32 try {
33 $response = $this->sendRequestThroughRouter($request);
34 } catch (Throwable $e) {
35 $this->reportException($e);
36
37 $response = $this->renderException($request, $e);
38 }
39
40 // 中略...
41
42 return $response;
43 }
Kernel::handle()は、HTTPレスポンスオブジェクトをつくって返すのが責務のようです。レスポンス自体は、Kernel::sendRequestThroughRouter()でRouterと呼ばれるオブジェクトを介して生成されています。
Routerがどんな役割を持つのか簡単に理解しておくために、今回アプリケーションコードとして書いたものを再度見てみます。
1// routes/web.php
2Route::get('/hello', function (Request $request) {
3// Hello Laravel from hello
4return "Hello Laravel from {$request->path()}";
5});
これは、http://localhost:8000/helloのようなURLへリクエストが送られると呼ばれる処理です。
Kernel::sendRequestThroughRouter()はリクエストのパスから呼び出すべき処理を決定し、実際に呼び出した上で得られたレスポンスを出力します。
上の例であれば、Routerから得られた戻り値である文字列Hello Laravel from helloをよしなに加工してレスポンスオブジェクトへと変換しています。
Laravelの処理がどうやってアプリケーションのコードと合流してレスポンスを構築していくのか、仕組みを探っていきます。
Kernel::sendRequestThroughRouter()
Kernel::handle()からKernel::sendRequestThroughRouter()へ移ります。
ここから更に難しくなってきますが、要所を押さえて読み解いていきましょう。まずはメソッドを読んでみます。
1/*
2 * Send the given request through the middleware / router.
3 *
4 * @param \Illuminate\Http\Request $request
5 * @return \Illuminate\Http\Response
6 */
7protected function sendRequestThroughRouter($request)
8{
9 // リクエストオブジェクトをService Containerへ登録しておく
10 $this->app->instance('request', $request);
11 // 中略...
12 $this->bootstrap();
13
14 return (new Pipeline($this->app))
15 ->send($request)
16 ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
17 ->then($this->dispatchToRouter());
18}
大きく2つの処理に分かれているので、分解して見ていきましょう。
Kernel::bootstrap()
まずはbootstrap処理です。単語自体は自動実行するということを意味しますが、Laravelでは起動するぐらいの意味で捉えておくとよいかもしれません。
Laravelにおけるbootstrapとは、Kernelクラスのbootstrappersプロパティに設定されたクラスのbootstrapメソッドを呼び出すことです。
1// Kernelクラスのプロパティ
2/*
3 * The bootstrap classes for the application.
4 *
5 * @var string[]
6 */
7protected $bootstrappers = [
8 \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
9 \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
10 \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
11 \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
12 \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
13 \Illuminate\Foundation\Bootstrap\BootProviders::class,
14];
プロパティに設定されている完全修飾クラス名と対応するオブジェクトは、Service Containerを介してつくられています。
ここで、bootstrappersとして記述されたクラスは、.envファイルを読み込んだり、configディレクトリ以下の設定値を読み出したりと、複雑なことをしています。
内部の仕組みが複雑であること・Hello World相当の処理では関わりが薄いことから、大半の処理は割愛します。
それぞれがざっくり何をしているかは、補足へ譲ることにします。
BootProviders
ここでは、以降の処理と関わりが深いBootProvidersだけを見ておきます。
とてもシンプルな処理なので、クラス全体を示します。
1// Illuminate/Foundation/Bootstrap/BootProviders.php
2
3namespace Illuminate\Foundation\Bootstrap;
4
5use Illuminate\Contracts\Foundation\Application;
6
7class BootProviders
8{
9 /*
10 * Bootstrap the given application.
11 *
12 * @param \Illuminate\Contracts\Foundation\Application $app
13 * @return void
14 */
15 public function bootstrap(Application $app)
16 {
17 $app->boot();
18 }
19}
Service Containerのbootメソッドを呼び出しています。具体的に何をしているのか、Service Containerを覗いてみます。
1// Illuminate/Foundation/Application.php
2/*
3 * Boot the application's service providers.
4 *
5 * @return void
6 */
7public function boot()
8{
9 // 中略...
10 array_walk($this->serviceProviders, function ($p) {
11 $this->bootProvider($p);
12 });
13
14 $this->booted = true;
15 // 中略...
16}
17/**
18 * Boot the given service provider.
19 *
20 * @param \Illuminate\Support\ServiceProvider $provider
21 * @return void
22 */
23protected function bootProvider(ServiceProvider $provider)
24{
25 // 中略...
26
27 if (method_exists($provider, 'boot')) {
28 // Container::call()は非常に複雑なので割愛
29 // Service Providerのbootメソッドをよしなに呼び出していると解釈してOK
30 $this->call([$provider, 'boot']);
31 }
32}
どうやらLaravelが起動(boot)するときには、Service Providerのbootメソッドを一斉に呼び出しているようです。
リクエストを処理する直前なので、Laravelを起動させてリクエストを扱う準備を整えるには丁度いいタイミングだと言えそうです。
補足: Kernel::bootstrap()では何が起きているのか
ここでは、BootProviders以外のbootstrapperが何をしているのか、概略を記しておきます。
Laravelのどういう処理がどこで動いているのか知っておくだけでも後々役に立つはずなので、ざっくりと押さえておきましょう。
LoadEnvironmentVariables
.envファイルを読み出すのが役割です。
最終的にはfile_get_contents()で.envファイル読み出して、対応するオブジェクトから参照できるようにしています。
LoadConfiguration
configディレクトリ以下の*.phpファイルをrequire命令で読み出しています。該当するファイルは、キーを設定名・値を設定値とする配列を返却しているので、この命令により設定を表す配列が得られます。
参考
Laravelアプリケーションで設定を参照するときは、上の処理で得られた配列から値を取り出しています。
HandleExceptions
Laravelアプリケーションで例外が起きたとき、どのクラスに任せるかを登録しています。対象は、App\Exceptions\Handler.phpです。
このように設定しておくことで、Laravelアプリケーションで起きた例外を一律に制御できるようになります。
RegisterFacades
Route::get()のような記法にて、Service Containerに登録されたオブジェクトを参照するための仕組みを実現するFacadeクラスを準備します。Facadeの概要は後ほど触れていきます。
ひとまずは、Facadeクラスのクラス変数へService Containerを登録しているんだな、ぐらいのことを覚えておきましょう。
RegisterProviders
config/app.phpに書かれたServiceProviderを中心に、各種ServiceProviderを読み出しています。
読み出されるService Providerは、キャッシュやセッション・バリデーション処理など、リクエスト扱う上で重要な責務を担っています。
Pipelineとは
bootstrap処理でLaravelが動き出す準備が完全に整いました。
そうなると、残りの処理はリクエストからレスポンスを組み立てるのに大きく関わっているはずです。早速中身を見ていきたいところなのですが、なにやら難しそうな処理が書かれています。もう一度コードを見ておきましょう。
1return (new Pipeline($this->app))
2 ->send($request)
3 ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
4 ->then($this->dispatchToRouter());
Pipelineと呼ばれる仕組みで何かたくさんの処理が動いているようです。Pipelineとは一体どういうもので、何をしているのでしょうか。
pipelineという単語は、ガスなどを輸送するパイプを意味しています。Google画像検索なんかで見てみるとイメージしやすいかもしれません。
LaravelにおけるPipelineは、処理を繋げる役目を持っています。
Pipelineは何をしているのか
本来であればPipelineクラスのコンストラクタから始めて各メソッドを読んでいきたいところです。
しかし、Pipelineクラスは、おそろしいほど複雑な処理が書かれており、仕組みを解き明かすだけでも大変な労力を要します。
よってここでは、LaravelにおけるPipelineオブジェクトが何をしているのか、動作を簡単に記しておくに留めておきます。
一言で表すと、Pipelineオブジェクトは、複数のオブジェクトのメソッドを共通の引数でどんどん呼び出すように動きます。これだけではイメージしづらいので、上で見た処理を簡易版のクラスで再現してみます。
1// なんちゃってPipelineクラス
2// ※ 実際の仕組みをかなり簡略化しているので、雰囲気を掴む程度に眺めてください
3class Pipeline
4{
5 private mixed $passable;
6
7 // send()で共通の引数を設定
8 public function send(mixed $passable)
9 {
10 $this->passable = $passable;
11 return $this;
12 }
13 // 共通の引数で一連のメソッドを呼び出す
14 public function through(array $pipes)
15 {
16 foreach($pipes as $pipe) {
17 $pipe->handle($this->passable);
18 }
19 return $this;
20 }
21 // 後処理として、渡されたClosureを呼び出す
22 public function then(Closure $destination)
23 {
24 $destination($this->passable):
25 }
26}
27
28$pipeline = new Pipeline($this->app);
29$pipeline = $pipeline->send($request);
30$pipeline = $pipeline->through($this->middleware)
31$pipeline->then($this->dispatchToRouter());
リクエストオブジェクトを引数に、ミドルウェア(後述)なるオブジェクトのhandleメソッドをどんどん呼び出します。そして、後処理として、Kernel::dispatchToRouter()が呼ばれています。
ざっくり抜き出してみると、やっていることはシンプルですね。
このように、一定の入力を複数のハンドラで処理していく仕組みは、デザインパターンにてChain of Responsibilityパターンと命名されています。
もう少し詳しく知りたい方は参考リンクを読んでみてください。
参考
補足: ミドルウェアについて
ミドルウェアはHello Worldの段階ではあまり意識することはありませんが、せっかくなので少しだけ触れておきます。
参照されるミドルウェアは、App\Http\Kernel.phpに書かれています。
1namespace App\Http;
2
3use Illuminate\Foundation\Http\Kernel as HttpKernel;
4
5class Kernel extends HttpKernel
6{
7 /*
8 * The application's global HTTP middleware stack.
9 *
10 * These middleware are run during every request to your application.
11 *
12 * @var array
13 */
14 protected $middleware = [
15 \App\Http\Middleware\TrustHosts::class,
16 \App\Http\Middleware\TrustProxies::class,
17 \Illuminate\Http\Middleware\HandleCors::class,
18 \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
19 \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
20 \App\Http\Middleware\TrimStrings::class,
21 \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
22 ];
23 // 中略...
24}
これらのクラスの完全修飾名は、Service Containerを介してオブジェクトへと変換されます。
そして、ここに書かれているミドルウェアと名付けられたオブジェクトは、ミドル(中間)という名の通り、リクエストの前処理を担っています。
例えば、ValidatePostSizeクラスは、POSTリクエストのボディのサイズを事前に検証しておき、極端に大きなリクエストを扱わないようガードする役割を担っています。
具体的な実装もさらっと見ておきましょう。
1// ミドルウェア
2class ValidatePostSize
3{
4 /*
5 * Handle an incoming request.
6 *
7 * @param \Illuminate\Http\Request $request
8 * @param \Closure $next
9 * @return mixed
10 *
11 * @throws \Illuminate\Http\Exceptions\PostTooLargeException
12 */
13 public function handle($request, Closure $next)
14 {
15 // POSTリクエストのボディの最大サイズ
16 $max = $this->getPostMaxSize();
17
18 // POSTリクエストのボディが最大サイズを超過していたら処理を打ち止め
19 if ($max > 0 && $request->server('CONTENT_LENGTH') > $max) {
20 throw new PostTooLargeException;
21 }
22
23 // ※ 厳密にはPipelineにおける後続の処理は、オブジェクトのメソッドから呼び出されている
24 return $next($request);
25 }
26}
入門段階では、ミドルウェアはリクエストオブジェクトを受け取って前処理をよしなにやっているんだな、ということを押さえておきましょう。
Kernel::dispatchToRouter
色々と回り道をしてきましたが、ようやくリクエストからレスポンスを組み立てる処理へたどり着きました。
本命の処理が何をしているのか、読んでいきましょう。
1// Illuminate/Foundation/Http/Kernel.php
2/*
3 * Get the route dispatcher callback.
4 *
5 * @return \Closure
6 */
7protected function dispatchToRouter()
8{
9 return function ($request) {
10 return $this->router->dispatch($request);
11 };
12}
プロパティrouterは、Service Containerを介して渡されたIlluminate\Routing\Routerオブジェクトです。
Routerはその名の通り、リクエストのURLをもとに対応する処理へと遷移先を振り分けるのが役割です。どうやって振り分けているのか、Router::dispatch()から見てみます。
Router::dispatch
Kernelクラスから離れて、Routerクラスを探っていきます。
まずはdispatchメソッドの定義から始めていきます。
1// Illuminate/Routing/Router.php
2
3/*
4 * Dispatch the request to the application.
5 *
6 * @param \Illuminate\Http\Request $request
7 * @return \Symfony\Component\HttpFoundation\Response
8 */
9public function dispatch(Request $request)
10{
11 $this->currentRequest = $request;
12
13 return $this->dispatchToRoute($request);
14}
15
16// 中略...
17
18/**
19 * Dispatch the request to a route and return the response.
20 *
21 * @param \Illuminate\Http\Request $request
22 * @return \Symfony\Component\HttpFoundation\Response
23 */
24public function dispatchToRoute(Request $request)
25{
26 return $this->runRoute($request, $this->findRoute($request));
27}
戻り値がHTTPレスポンスっぽいオブジェクトであることから、Router::findRoute()で呼び出すべき処理を決め、Router::runRoute()でHTTPレスポンスを生成しているように見えます。
それぞれの仕組みを見ていけば、HTTPレスポンスがどうやってつくられるのか明らかになりそうです。
順に整理していきます。
Router::findRoute()
最初に、呼び出す処理を決めているメソッドを読んでいきます。
1/*
2 * Find the route matching a given request.
3 *
4 * @param \Illuminate\Http\Request $request
5 * @return \Illuminate\Routing\Route
6 */
7protected function findRoute($request)
8{
9 // 中略...
10 $this->current = $route = $this->routes->match($request);
11
12 return $route;
13}
リクエストのURLと合致するものとして、Routeなるオブジェクトを探し出しているようです。
ここで、Routeについて少し掘り下げていこうと思います。
Route
Routeという文字を見ると、Hello Worldアプリケーションとして実装したコードが思い浮かぶかもしれません。
復習がてら、アプリケーションの実装コードとして書いたものを見てみましょう。
1// routes/web.php
2Route::get('/hello', function (Request $request) {
3// Hello Laravel from hello
4return "Hello Laravel from {$request->path()}";
5});
Routeクラスのgetメソッドなる静的メソッドを呼び出しているっぽい処理が書かれています。これはRouteオブジェクトと関係が深そうです。
ここで、先ほどまで見ていたRouterオブジェクトとの関係を考えると、いくつか疑問が浮かんできます。
- routes/web.phpで書かれたRouteとは何か
- いつroutes/web.phpは呼ばれたのか
- Route::get()が呼ばれると何が起こっているのか
Routerが何をしているのか理解するためには、これらの疑問を解消しておいた方が良さそうです。1つ1つざっくりと考えてみましょう。
RouteクラスとFacadeクラス
routes/web.phpで書かれたRouteクラスは、完全修飾名がIlluminate\Support\Facades\Routeとなっています。
Facadeなる概念が新しく登場しました。Facadeも掘り下げていくと深みにハマってしまうので、ここでは概要だけ触れておきます。
Facadeはデザインパターンの1つで、フレームワークなどの機能をシンプルに扱うためのインタフェースを提供します。
参考
これが何を表すかは、Facadeクラスの仕組みを知ることで見えてくるはずです。
深入りしすぎない程度にたどっていきましょう。
Facade
LaravelにおけるFacadeの仕組みを明らかにしていきます。
入り口はFacadeの1つであるRouteクラスとします。どのような構造を持つのか見てみましょう。
1// Illuminate/Support/Facades/Route.php
2class Route extends Facade
3{
4 /*
5 * Get the registered name of the component.
6 *
7 * @return string
8 */
9 protected static function getFacadeAccessor()
10 {
11 return 'router';
12 }
13}
Route::get()が呼ばれているはずなのに、getメソッドが定義されていません。親クラスにも定義は無さそうです。
どういう仕組みで何を呼び出しているのでしょうか。
実は、継承元であるFacadeクラスにて、__callStatic()なるメソッドが定義されています。
これはPHPが特定のタイミングにて呼び出すメソッドで、Route::get()のように静的メソッドを呼び出そうとして対象が見つからなかったときに発火します。
1/*
2 * Handle dynamic, static calls to the object.
3 *
4 * @param string $method
5 * @param array $args
6 * @return mixed
7 *
8 * @throws \RuntimeException
9 */
10public static function __callStatic($method, $args)
11{
12 $instance = static::getFacadeRoot();
13 // 中略...
14 return $instance->$method(...$args);
15}
ということで、__callStatic()から呼ばれている処理を見てみます。
1/*
2 * Get the root object behind the facade.
3 *
4 * @return mixed
5 */
6public static function getFacadeRoot()
7{
8 // getFacadeAccessorは子クラスに定義されている
9 // 今回の場合は、文字列routerへと評価される
10 return static::resolveFacadeInstance(static::getFacadeAccessor());
11}
12
13/**
14 * Resolve the facade root instance from the container.
15 *
16 * @param string $name
17 * @return mixed
18 */
19protected static function resolveFacadeInstance($name)
20{
21 // 中略...
22 // クラス変数appは、bootstrapperにてService Containerが設定されている
23 if (static::$app) {
24 // 中略...
25
26 return static::$app[$name];
27 }
28}
中身を読んでみると、どうやらService Containerへrouterというキーで配列アクセスをしているようです。
Service ContainerであるContainerクラスは、ArrayAccess Interfaceを実装しているので、Container::offsetGet()が呼ばれます。
参考
Container::offsetGet()は、オブジェクトを取り出す処理であるContainer::make()を呼んでいます。
1// Illuminate\Container\Container.php
2/*
3 * Get the value at a given offset.
4 *
5 * @param string $key
6 * @return mixed
7 */
8public function offsetGet($key): mixed
9{
10 return $this->make($key);
11}
つまりここまでの処理は、Service Containerからrouterというキーのオブジェクトを取り出すのが目的のようです。
routerというキーでオブジェクトを登録する処理は、Application::registerBaseServiceProviders()から呼ばれるRoutingServiceProviderにて定義されています。
1// Illuminate\Routing\RoutingServiceProvider.php
2class RoutingServiceProvider extends ServiceProvider
3{
4 // 中略...
5 /*
6 * Register the router instance.
7 *
8 * @return void
9 */
10 protected function registerRouter()
11 {
12 $this->app->singleton('router', function ($app) {
13 return new Router($app['events'], $app);
14 });
15 }
16}
ここでのRouterは、先ほどまで見ていたIlluminate\Routing\Routerクラスを指しています。
少し長くなってしまいましたが、LaravelにおけるFacadeクラスは、デザインパターンで定義されている通り、Service Containerへ簡潔にアクセスするためのインタフェースを提供しているようです。
具体的には、Route::get()のように書くだけで、Service Containerに登録されたRouterオブジェクトのメソッドが呼び出せるようになります。
routes/web.phpはいつ読み出されるのか
Facadeの仕組みを見ることで、routes/web.phpがRouterと関わりが深いことが分かりました。
ですが、これまで見てきた処理でこのファイルは読み出されていませんでした。
重要な処理である以上どこかで読み出されてはいそうですが、一体どこで参照されたのでしょうか。
実は、Service Providerの1つである、App\Providers\RouteServiceProviderクラスが入り口となっています。
※ このService Providerはconfig/app.phpに定義されているので、Kernel::bootstrap()にて呼び出されています。
1// App/Providers/RouteServiceProvider.php
2
3class RouteServiceProvider extends ServiceProvider
4{
5
6 /*
7 * Define your route model bindings, pattern filters, and other route configuration.
8 */
9 public function boot(): void
10 {
11 $this->configureRateLimiting();
12
13 $this->routes(function () {
14 Route::middleware('api')
15 ->prefix('api')
16 ->group(base_path('routes/api.php'));
17
18 Route::middleware('web')
19 ->group(base_path('routes/web.php'));
20 });
21 }
22}
ここの処理は非常に複雑なので、とりあえずroutes/web.phpはService Providerを介して読み出され、実行されている。ということを頭に入れておきましょう。
Route::get()は何をしているのか
たくさん寄り道をしてしまったので、本来の目的を整理しておきます。
今回の目的は、Routerから参照されているRouteなるオブジェクトがどのようにつくられているのか明らかにすることです。
ということで、関わりが深そうなRoute::get()へと戻ります。
1// routes/web.php
2Route::get('/greeting', function () {
3 return 'Hello World';
4});
ここでのRouteの実体はRouterオブジェクトであるため、実際にはRouter::get()が呼び出されます。
何が起きているのか掘り下げていきましょう。
1// Illuminate/Routing/Router.php
2
3/*
4 * Register a new GET route with the router.
5 *
6 * @param string $uri
7 * @param array|string|callable|null $action
8 * @return \Illuminate\Routing\Route
9 */
10public function get($uri, $action = null)
11{
12 return $this->addRoute(['GET', 'HEAD'], $uri, $action);
13}
14
15/**
16 * Add a route to the underlying route collection.
17 *
18 * @param array|string $methods
19 * @param string $uri
20 * @param array|string|callable|null $action
21 * @return \Illuminate\Routing\Route
22 */
23public function addRoute($methods, $uri, $action)
24{
25 return $this->routes->add($this->createRoute($methods, $uri, $action));
26}
Router::addRoute()に注目してみます。
Routerが参照しているRouteオブジェクトをつくっている処理にたどり着けました。
ここを見ていけば、Routeオブジェクトが何者であるか理解できそうです。
先に進んでみましょう。
1/*
2 * Create a new route instance.
3 *
4 * @param array|string $methods
5 * @param string $uri
6 * @param mixed $action
7 * @return \Illuminate\Routing\Route
8 */
9protected function createRoute($methods, $uri, $action)
10{
11 // 中略...
12
13 $route = $this->newRoute(
14 $methods, $this->prefix($uri), $action
15 );
16 // 中略...
17 return $route;
18}
19
20/**
21 * Create a new Route object.
22 *
23 * @param array|string $methods
24 * @param string $uri
25 * @param mixed $action
26 * @return \Illuminate\Routing\Route
27 */
28public function newRoute($methods, $uri, $action)
29{
30 return (new Route($methods, $uri, $action))
31 ->setRouter($this)
32 ->setContainer($this->container);
33}
ここで、methodsはGETまたはHEAD・uriはRoute::get()に渡したパス・actionはRoute::get()に渡したClosureを表しています。
Routeクラスもさわりだけ見ておきます。
1// Illuminate/Routing/Route.php
2/*
3 * Create a new Route instance.
4 *
5 * @param array|string $methods
6 * @param string $uri
7 * @param \Closure|array $action
8 * @return void
9 */
10public function __construct($methods, $uri, $action)
11{
12 $this->uri = $uri;
13 $this->methods = (array) $methods;
14 $this->action = Arr::except($this->parseAction($action), ['prefix']);
15 // 中略...
16}
ひとまずはプロパティに引数で渡したものを保存しているぐらいに理解しておきましょう。
さて、ようやくRouteオブジェクトが何者であるか理解できました。
理解したことを整理しておくと、Routeオブジェクトは、routes/web.phpに書かれたRoute::get()のようなメソッドを介してつくられます。
そして、Routeオブジェクトはパスと実行する処理(Closure)を保存しています。
Router::findRoute()
Routeオブジェクトが見えてくると、以降の処理で何をするかも明確になります。
リクエストのパスと対応するRouteオブジェクトさえ手に入れば何を実行するべきかも定まるので、レスポンスまで一気に繋げることができます。
ということで、Router::findRoute()でどうやってRouteオブジェクトを探し当てるのか、解き明かしていきましょう。
復習のためにメソッド定義をもう一度見ておきます。
1// Illuminate/Routing/Router.php
2
3/*
4 * Find the route matching a given request.
5 *
6 * @param \Illuminate\Http\Request $request
7 * @return \Illuminate\Routing\Route
8 */
9protected function findRoute($request)
10{
11 // 中略...
12 // routesプロパティは、Illuminate\Routing\RouteCollectionオブジェクトを指す
13 $this->current = $route = $this->routes->match($request);
14
15 return $route;
16}
17
18// Service Containerを介して呼ばれるコンストラクタ
19// routesプロパティを設定している
20/**
21 * Create a new Router instance.
22 *
23 * @param \Illuminate\Contracts\Events\Dispatcher $events
24 * @param \Illuminate\Container\Container|null $container
25 * @return void
26 */
27public function __construct(Dispatcher $events, Container $container = null)
28{
29 $this->events = $events;
30 $this->routes = new RouteCollection;
31 $this->container = $container ?: new Container;
32}
該当するRouteオブジェクトを探す処理は、RouteCollectionオブジェクトが担っているようです。
※ 各Routeオブジェクトは、Route::get()にてRouteCollectionオブジェクトへ詰め込まれています。
RouteCollection::match()
RouteCollectionオブジェクトがどうやってパスと対応するRouteオブジェクトを探しているのか、メソッドから仕組みを見てみます。
1// Illuminate/Routing/RouteCollection.php
2
3/*
4 * Find the first route matching a given request.
5 *
6 * @param \Illuminate\Http\Request $request
7 * @return \Illuminate\Routing\Route
8 *
9 * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
10 * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
11 */
12public function match(Request $request)
13{
14 // Routeオブジェクトのうち、HTTPメソッドがリクエストと一致しているもので絞り込む
15 $routes = $this->get($request->getMethod());
16
17 // パスと対応するRouteオブジェクトを探索
18 // First, we will see if we can find a matching route for this current request
19 // method. If we can, great, we can just return it so that it can be called
20 // by the consumer. Otherwise we will check for routes with another verb.
21 $route = $this->matchAgainstRoutes($routes, $request);
22
23 // 「/user/{id}」のようなRouteパラメータをRouteオブジェクトで解釈しておく
24 // 今回はRouteパラメータを扱わないので割愛
25 return $this->handleMatchedRoute($request, $route);
26}
27
28/**
29 * Determine if a route in the array matches the request.
30 *
31 * @param \Illuminate\Routing\Route[] $routes
32 * @param \Illuminate\Http\Request $request
33 * @param bool $includingMethod
34 * @return \Illuminate\Routing\Route|null
35 */
36protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
37{
38 // Routeオブジェクトのmatchesメソッドを呼び出し、最初にtrueを返したものを返却
39 [$fallbacks, $routes] = collect($routes)->partition(function ($route) {
40 return $route->isFallback;
41 });
42 return $routes->merge($fallbacks)->first(
43 fn (Route $route) => $route->matches($request, $includingMethod)
44 );
45}
重要なのは、Route::matches()がtrueを返したRouteオブジェクトを返却していることです。
つまり、Routeオブジェクトでどのように判定しているかが分かれば、パスからどうやって呼び出すべき処理を決定しているのかが見えてきます。
ということで、Route::matches()が何をやっているのか探っていきましょう。
Route::matches()
1// Illuminate/Routing/Route.php
2
3/*
4 * Determine if the route matches a given request.
5 *
6 * @param \Illuminate\Http\Request $request
7 * @param bool $includingMethod
8 * @return bool
9 */
10public function matches(Request $request, $includingMethod = true)
11{
12 // 中略...
13 foreach (self::getValidators() as $validator) {
14
15 if (! $validator->matches($this, $request)) {
16 return false;
17 }
18 }
19
20 return true;
21}
Route::getValiators()は、validatorと呼ばれるオブジェクトを取得する処理です。
validatorはbool値を返すmatchesというメソッドを実装しているので、すべてのvalidatorが有効だと判定したRouteオブジェクトが返されるようです。
ここでは、パスと関わりが深いUriValidatorクラスを見てみます。
1// Illuminate/Routing/Matching/UriValidator.php
2
3class UriValidator implements ValidatorInterface
4{
5 /*
6 * Validate a given rule against a route and request.
7 *
8 * @param \Illuminate\Routing\Route $route
9 * @param \Illuminate\Http\Request $request
10 * @return bool
11 */
12 public function matches(Route $route, Request $request)
13 {
14 $path = rtrim($request->getPathInfo(), '/') ?: '/';
15
16 return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));
17 }
18}
何やら難しそうな処理が書かれていますが、ここではリクエストのパスとRouteオブジェクトに登録されたパスをよしなに比べているんだな、と思っておきましょう。
Router::findRoute()
ざっくり探っていくことで、何が起きているのか見えてきました。
理解を整理するために、Router::findRoute()の流れをまとめておきましょう。
1// Illuminate/Routing/Router.php
2
3/*
4 * Find the route matching a given request.
5 *
6 * @param \Illuminate\Http\Request $request
7 * @return \Illuminate\Routing\Route
8 */
9protected function findRoute($request)
10{
11 // 中略...
12 $this->current = $route = $this->routes->match($request);
13 return $route;
14}
処理の流れが見えやすいよう、箇条書きで簡単に振り返っておきます。
- routes/web.php`などで`Route::get()のような処理を呼び出す
- パスと実行する処理を表すRouteオブジェクトを登録
- Routeオブジェクトは、RouteCollectionオブジェクトによって保持される
- RouteCollectionオブジェクトは、HTTPリクエストのメソッドやパスの情報をもとにRouteオブジェクトのmatchesメソッドを呼び出す
- それぞれのRouteオブジェクトは、パスなどの情報をもとに、自身でリクエストを処理するべきか判定
- 合致したものが返され、Route::findRoute()の戻り値となる
ということでまとめると、パスが合致するRouteオブジェクトを探し出すのがRouter::findRoute()の責務だったようです。
Router::runRoute()
Router::findRoute()により、実行するべき処理に対応するRouteオブジェクトが得られました。
あとはリクエストを扱う処理を呼び出せばレスポンスが手に入るはずです。流れを見返すために、Router::findRoute()を呼び出しているところを見直しておきましょう。
1// Illuminate/Routing/Router.php
2
3/*
4 * Dispatch the request to a route and return the response.
5 *
6 * @param \Illuminate\Http\Request $request
7 * @return \Symfony\Component\HttpFoundation\Response
8 */
9public function dispatchToRoute(Request $request)
10{
11 return $this->runRoute($request, $this->findRoute($request));
12}
リクエストオブジェクトと、Router::findRoute()`で得られたRouteオブジェクトを引数に、`Router::runRoute()を呼び出しているようです。
Router::runRoute()の仕組みが明確になれば、リクエストからレスポンスを組み立てるまでの処理の流れが繋がるはずです。
最後の正念場となりそうですが、じっくりと見ていきましょう。
1/*
2 * Return the response for the given route.
3 *
4 * @param \Illuminate\Http\Request $request
5 * @param \Illuminate\Routing\Route $route
6 * @return \Symfony\Component\HttpFoundation\Response
7 */
8protected function runRoute(Request $request, Route $route)
9{
10 // 中略...
11 return $this->prepareResponse($request,
12 $this->runRouteWithinStack($route, $request)
13 );
14}
再び処理が2つに分かれているようです。メソッド名を見るに、Router::runRouteWithinStack()`でレスポンスの元になるオブジェクトを組み立て、`Router::prepareResponse()でHTTPレスポンスを表すオブジェクトへと変換しているように思えます。
ということなので、まずはレスポンスの元をつくっていそうなRouter::runRouteWithinStack()を見ていきます。
Router::runRouteWithinStack()
長そうなメソッド名ですが、ひとまず定義から概要を俯瞰しておきます。
1/*
2 * Run the given route within a Stack "onion" instance.
3 *
4 * @param \Illuminate\Routing\Route $route
5 * @param \Illuminate\Http\Request $request
6 * @return mixed
7 */
8protected function runRouteWithinStack(Route $route, Request $request)
9{
10 // 中略...
11 $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
12
13 return (new Pipeline($this->container))
14 ->send($request)
15 ->through($middleware)
16 ->then(fn ($request) => $this->prepareResponse(
17 // 今回見るべき処理は、Route::run()のみ
18 $request, $route->run()
19 ));
20}
ここでのミドルウェアは、Routeオブジェクトに挟まるものなのですが、入門段階ではあまり触れる機会も無いので割愛します。
Pipelineで書かれた処理は、ミドルウェアの処理を実行した上で後処理を動かすためのものです。
今回見るべきは、Routeオブジェクトと結びつく処理を呼び出していると思われるRoute::run()です。
Routeオブジェクトと結びつく処理は、routes/web.phpで定義したものです。肝となる重要な処理なので、再掲しておきます。
1// routes/web.php
2// 呼び出されるのは、Route::get()の第二引数のClosure
3Route::get('/hello', function (Request $request) {
4// Hello Laravel from hello
5return "Hello Laravel from {$request->path()}";
6});
このClosureがどうやって呼び出されるのか理解することを目指します。
Route::run()
ということで、Routeオブジェクトへと移動します。
Route::run()が何をしているのか見ていきましょう。
1// Illuminate/Routing/Route.php
2
3/*
4 * Run the route action and return the response.
5 *
6 * @return mixed
7 */
8public function run()
9{
10 // 中略...
11 try {
12 return $this->runCallable();
13 } catch (HttpResponseException $e) {
14 return $e->getResponse();
15 }
16}
Route::runCallable()へと進みます。
1/*
2 * Run the route action and return the response.
3 *
4 * @return mixed
5 */
6protected function runCallable()
7{
8 // Route::get()で渡したClosureと等価
9 $callable = $this->action['uses'];
10
11 // 中略...
12
13 // Service Containerからキー「Illuminate\Routing\Contracts\CallableDispatcher」でオブジェクトを取り出す
14 // 取り出したオブジェクトのdispatchメソッドを、Routeオブジェクト、呼び出すClosureを引数に実行
15 return $this->container[CallableDispatcher::class]->dispatch($this, $callable);
16}
ここからはじっくり整理していく必要がありそうです。
1つ1つの処理が何をしているのかゆっくり解き明かしていきましょう。
CallableDispatcher
Service Containerから取り出そうとしていたCallableDispatcherオブジェクトは、Service Containerの初期処理で実行されるRoutingServiceProviderにて用意されています。
1// Illuminate/Routing/RoutingServiceProvider.php
2
3/*
4 * Register the callable dispatcher.
5 *
6 * @return void
7 */
8protected function registerCallableDispatcher()
9{
10 // CallableDispatcherContractは、CallableDispatcher Interfaceのエイリアス
11 $this->app->singleton(CallableDispatcherContract::class, function ($app) {
12 return new CallableDispatcher($app);
13 });
14}
ここで生成されるオブジェクトは、Illuminate\Routing\CallableDispatcherオブジェクトです。
ということで、Service Containerから何を取り出しているのか見えてきました。
続いて、取り出されたオブジェクトのdispatchメソッドで何が起きているのか見ていきます。
CallableDispatcher::dispatch()
1// Illuminate/Routing/CallableDispatcher.php
2
3/*
4 * Dispatch a request to a given callable.
5 *
6 * @param \Illuminate\Routing\Route $route
7 * @param callable $callable
8 * @return mixed
9 */
10public function dispatch(Route $route, $callable)
11{
12 // 変数callableはRoute::get()で渡したClosure
13 return $callable(...array_values($this->resolveParameters($route, $callable)));
14}
ついにRoute::get()で渡した処理を呼び出している箇所へたどり着きました。
最後に明らかにしなければならないのは、どうやって引数を組み立てているかです。ここで渡されるClosureは、リクエストオブジェクトを引数として受け取っています。
よって、CallableDispatcher::resolveParameters()にて、なんらかの仕組みで引数であるオブジェクトを構築しているはずです。
ここは難解な処理が書かれているので、全体像を掴むぐらいを目指して見ていきます。
CallableDispatcher::resolveParameters()
まずはメソッド定義を見てみます。
1行の処理ですがたくさんのことが詰まっているので、じっくり解きほぐしていきましょう。
1/*
2 * Resolve the parameters for the callable.
3 *
4 * @param \Illuminate\Routing\Route $route
5 * @param callable $callable
6 * @return array
7 */
8protected function resolveParameters(Route $route, $callable)
9{
10 return $this->resolveMethodDependencies($route->parametersWithoutNulls(), new ReflectionFunction($callable));
11}
まず、Route::parametersWithoutNulls()`は、`/user/{id}のようなパスから組み立てられた引数を抜き出す処理です。
参考
このようなパラメータは今回は扱わないので割愛します。
続いて、ReflectionFunctionオブジェクトをつくっている処理は、関数が受け取る引数を読み出すためのものです。
Route::get()に渡した処理を例に考えると、Request型のオブジェクトを引数として受け取ることが読み出されます。
これらの前提を踏まえた上で、Closureへ渡す引数を組み立てる処理CallableDispatcher::resolveMethodDependencies()を読んでいきましょう。
CallableDispatcher::resolveMethodDependencies()
1/*
2 * Resolve the given method's type-hinted dependencies.
3 *
4 * @param array $parameters
5 * @param \ReflectionFunctionAbstract $reflector
6 * @return array
7 */
8public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
9{
10 // 中略...
11
12 // Closureが受け取る引数を1つ1つ走査
13 // keyはインデックス
14 // parameterは引数そのものを表すReflectionParameterオブジェクトとなる
15 foreach ($reflector->getParameters() as $key => $parameter) {
16 // 引数の情報からインスタンスをつくっていそうな処理
17 $instance = $this->transformDependency($parameter, $parameters, $skippableValue);
18
19 // 中略...
20 $this->spliceIntoParameters($parameters, $key, $instance);
21 }
22
23 return $parameters;
24}
難しそうな処理が並んでいます。一気に理解しようと思うと迷子になってしまうので、まずは何をやっているのかざっくり掴むことを目指します。
そこで、処理が何をしているのか簡易版のコードで眺めておきます。
1// 呼び出すClosure
2function resolveRequest(Request $request)
3{
4 return "Hello Laravel from {$request->path()}";
5}
6
7// transformDependency()でClosureを呼び出すための引数となるオブジェクトを取得
8$request = transformDependency(Request::class);
9// 得られたオブジェクトを引数にClosureを呼び出す
10$response = resolveRequest($request);
Reflectionの仕組みが関わることで難しく見えますが、ざっくりやっていることは、関数定義をもとに引数を組み立てているのみです。
よって、どうやって引数となるオブジェクトを組み立てているのか、流れを押さえておきましょう。
CallableDispatcher::transformDependency()
オブジェクトをつくっていそうな処理を見てみます。
1/*
2 * Attempt to transform the given parameter into a class instance.
3 *
4 * @param \ReflectionParameter $parameter
5 * @param array $parameters
6 * @param object $skippableValue
7 * @return mixed
8 */
9protected function transformDependency(ReflectionParameter $parameter, $parameters, $skippableValue)
10{
11 // オブジェクトが必要なクラス名を取得
12 $className = Reflector::getParameterClassName($parameter);
13
14 // If the parameter has a type-hinted class, we will check to see if it is already in
15 // the list of parameters. If it is we will just skip it as it is probably a model
16 // binding and we do not want to mess with those; otherwise, we resolve it here.
17 if ($className && ! $this->alreadyInParameters($className, $parameters)) {
18
19 // 中略...
20 // Service Containerからクラス名をキーにオブジェクトを取得
21 return $this->container->make($className);
22 }
23 // 中略...
24}
思いのほかシンプルに動いていそうです。型ヒントで欲しいオブジェクトの型を指定していたことで、Service Containerからクラス名をもとにオブジェクトを取り出すことができます。
リクエストオブジェクトはService Containerに詰め込まれていることから、問題なく取り出されます。
Closure呼び出し
流れが見えてきたので、呼び出し元をもう一度見てみます。
1public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
2{
3 // 中略
4 // Closureが受け取る引数を1つ1つ走査
5 // keyはインデックス
6 // parameterは引数そのものを表すReflectionParameterオブジェクトとなる
7 foreach ($reflector->getParameters() as $key => $parameter) {
8 // 引数の情報からオブジェクトを生成
9 $instance = $this->transformDependency($parameter, $parameters, $skippableValue);
10
11 // 中略...
12 // パスから組み立てられる引数を後ろに配置するための処理
13 // 基本的には引数を表す配列にオブジェクトを詰め込んでいると理解してOK
14 $this->spliceIntoParameters($parameters, $key, $instance);
15 }
16
17 return $parameters;
18}
難しそうな処理が並んでいましたが、流れを整理してみると理解できそうです。簡単にまとめておきましょう。
- Reflectionの仕組みを利用して、Closureの引数情報を読み出す
- 引数を1つ1つループで走査し、型ヒント情報からService Containerを介してオブジェクトを生成
- 得られたオブジェクトを引数リストを表す配列へ詰め込んで返却
更に戻って、Closureを呼び出しているところへと移ります。
1/*
2 * Dispatch a request to a given callable.
3 *
4 * @param \Illuminate\Routing\Route $route
5 * @param callable $callable
6 * @return mixed
7 */
8public function dispatch(Route $route, $callable)
9{
10 // ...は配列を展開して個別に関数の引数として渡す
11 return $callable(...array_values($this->resolveParameters($route, $callable)));
12}
13
14/**
15 * Resolve the parameters for the callable.
16 *
17 * @param \Illuminate\Routing\Route $route
18 * @param callable $callable
19 * @return array
20 */
21protected function resolveParameters(Route $route, $callable)
22{
23 return $this->resolveMethodDependencies($route->parametersWithoutNulls(), new ReflectionFunction($callable));
24}
得られたオブジェクトを引数にClosureを呼び出しています。
ということで非常に長くなりましたが、パスと対応する処理を呼び出すまでの流れをようやく掴めました。
Router::runRoute()
ここまでの処理を経ることで、文字列Hello Laravel from helloがレスポンスとして得られました。
このままではHTTPレスポンスオブジェクトとして返せないので、よしなに変換している処理を見ていきたいです。
ということで、レスポンスを整形している処理がありそうな呼び出し元へと戻りましょう。
1// Illuminate/Routing/Router.php
2
3/*
4 * Return the response for the given route.
5 *
6 * @param \Illuminate\Http\Request $request
7 * @param \Illuminate\Routing\Route $route
8 * @return \Symfony\Component\HttpFoundation\Response
9 */
10protected function runRoute(Request $request, Route $route)
11{
12 // 中略...
13 return $this->prepareResponse($request,
14 $this->runRouteWithinStack($route, $request)
15 );
16}
Router::prepareResponse()でどうやってHTTPレスポンスへと変換されているのか、整理していきます。
Router::prepareResponse()
まずはメソッド定義から全体像を見ておきます。
1/*
2 * Create a response instance from the given value.
3 *
4 * @param \Symfony\Component\HttpFoundation\Request $request
5 * @param mixed $response
6 * @return \Symfony\Component\HttpFoundation\Response
7 */
8public function prepareResponse($request, $response)
9{
10 return static::toResponse($request, $response);
11}
12
13/**
14 * Static version of prepareResponse.
15 *
16 * @param \Symfony\Component\HttpFoundation\Request $request
17 * @param mixed $response
18 * @return \Symfony\Component\HttpFoundation\Response
19 */
20public static function toResponse($request, $response)
21{
22 // 中略...
23
24 if ($response instanceof PsrResponseInterface) {
25 $response = (new HttpFoundationFactory)->createResponse($response);
26 // 中略...
27
28 // 文字列のレスポンスはこの分岐を通る
29 } elseif (! $response instanceof SymfonyResponse) {
30 $response = new Response($response, 200, ['Content-Type' => 'text/html']);
31 }
32 // 中略...
33
34 // Response::prepare()はHTTPレスポンスのヘッダを整形
35 // 単に文字列を返すだけであれば関わりは薄いので割愛
36 return $response->prepare($request);
37}
JSONやレスポンスっぽいオブジェクトなど、さまざまなオブジェクトをもとにレスポンスが組み立てられるよう、たくさんの分岐が書かれています。
今回のレスポンスは単なる文字列なので、最後の分岐を通ります。
文字列をもとに、Symfonyが提供するResponseクラスを拡張した、Illuminate\Http\Responseオブジェクトを生成しています。
1// Illuminate/Http/Response.php
2
3class Response extends SymfonyResponse
4{
5
6 /*
7 * Create a new HTTP response.
8 *
9 * @param mixed $content
10 * @param int $status
11 * @param array $headers
12 * @return void
13 *
14 * @throws \InvalidArgumentException
15 */
16 public function __construct($content = '', $status = 200, array $headers = [])
17 {
18 $this->headers = new ResponseHeaderBag($headers);
19
20 // 今回の場合、contentに文字列・statusCodeに200(正常)が設定される
21 $this->setContent($content);
22 $this->setStatusCode($status);
23 $this->setProtocolVersion('1.0');
24 }
25 // 中略...
26}
Response::send()
あとは呼び出し元にレスポンスオブジェクトを返すだけなので、エントリーポイントまで戻ります。
1// public/index.php
2$response = $kernel->handle(
3 $request = Request::capture()
4)->send();
最後にResponse::send()を呼び出して終わりのようです。
何をしているのか見ていきましょう。
1//symfony/http-foundation/Response.php
2
3/*
4 * Sends HTTP headers and content.
5 *
6 * @return $this
7 */
8public function send(): static
9{
10 $this->sendHeaders();
11 $this->sendContent();
12
13 // 中略...
14
15 return $this;
16}
17
18/**
19 * Sends content for the current web response.
20 *
21 * @return $this
22 */
23public function sendContent(): static
24{
25 // $this->contentの中身は、文字列「Hello Laravel from hello」
26 echo $this->content;
27
28 return $this;
29}
最終的な仕組みはおどろくほどシンプルでした。
PHP標準のecho命令を呼び出すことで、WebサーバへHTTPレスポンスを出力しています。
あとはWebサーバが受け取ったレスポンスをよしなに処理していけば、ブラウザへ文字列が表示され、無事LaravelのHello Worldが達成されます。
振り返り
Hello Worldのように単に画面に文字列を表示させるだけの処理でしたが、とてつもなく長い道のりでした。
歩んできた道を忘れないよう、箇条書きで流れをまとめておきましょう。
- public/index.phpがエントリーポイント
- autoloaderでクラスを読み出せるようにしておく
- オブジェクトを用意・提供するService Containerを初期化
- あわせて、どういうオブジェクトをつくるか規定したService Providerと、Service Containerを連携させながらLaravelが動作するのに必要なオブジェクトを用意
- アプリケーションの制御を担うKernelオブジェクトをService Containerを介して生成
- Kernelオブジェクトのhandleメソッドを呼び出す
- routes/web.phpに書いたHello World処理をもとに、パスと実行する処理を表すRouteオブジェクトを作成
- リクエストのパスと合致するRouteオブジェクトを探し、対応する処理が呼ばれる
- Router・Routeオブジェクトを介してつくられたレスポンスをPHPのecho命令で出力することで、Hello Worldに相当する文字列が画面に表示される
これまでたどってきた処理、とくにService Container・Service ProviderはLaravelでアプリケーションをつくる上でとても重要です。
普段あまり触る機会が多くなくても、どういう仕組みで動いているのか、ざっくりとでも頭に入れておきましょう。
以降では、ちょっとした補足を残しておきます。
補足: artisan serveコマンドはいかにして動作しているのか
これまでは、public/index.phpをエントリーポイントとした処理を見てきました。
しかし、開発するときにはphp artisan serveコマンドで開発サーバを動かす機会も多いはずです。このコマンドはどのように動いているのか、ものすごくざっくりとですが整理しておきます。
まずは全体像を描くために、やっていることを簡単に書き出しておきます。
- artisan serveコマンドによりPHPをビルトインサーバモードで起動
- router scriptとしてservrer.phpを指定
- server.phpによりpublic/index.phpが読み込まれる
- ※ cwdはProcessクラスにてpublic以下が指定される
- あとはnginxなどと同じような流れでリクエストを処理
エントリーポイントは、.env`などが置かれているディレクトリにある`artisanファイルです。実体はPHPファイルなので、PHPスクリプトとして実行されます。
中身のソースコードも馴染みがあるはずです。中を見ていきましょう。
1// artisan
2require __DIR__.'/vendor/autoload.php';
3
4// Service Containerを用意
5$app = require_once __DIR__.'/bootstrap/app.php';
6
7// Consoleアプリケーション用のKernelオブジェクトを生成
8$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
9
10$status = $kernel->handle(
11 $input = new Symfony\Component\Console\Input\ArgvInput,
12 new Symfony\Component\Console\Output\ConsoleOutput
13);
Service ContainerやService Providerで準備を整え、Kernelで処理を実行。といったようにpublic/index.phpで見たものと似通っています。
ここからの処理を詳細に見ていくと補足に収まり切らなくなってしまうので、重要な処理まで移動します。
Console KernelはService Providerなどを介して、実行する処理を表現するCommandオブジェクトを読み出します。
そして、コマンドライン引数(今回はserve)と対応するオブジェクトを実行するべき処理として呼び出します。
一気にまとめてしまうと、ServeCommandが今回実行する対象となります。
1// Illuminate/Foundation/Console/ServeCommand.php
2
3class ServeCommand extends Command
4{
5 /*
6 * The console command name.
7 *
8 * @var string
9 */
10 protected $name = 'serve';
11
12 /**
13 * The console command description.
14 *
15 * @var string
16 */
17 protected $description = 'Serve the application on the PHP development server';
18}
コマンド名を表すプロパティを見ても、コマンドライン引数serveと対応していることが分かります。
ServeCommandオブジェクトもたくさんの処理を担っているのですが、処理の流れに関わるところだけ見ておきます。
1/*
2 * Get the full server command.
3 *
4 * @return array
5 */
6protected function serverCommand()
7{
8 $server = file_exists(base_path('server.php'))
9 ? base_path('server.php')
10 : __DIR__.'/../resources/server.php';
11
12 return [
13 (new PhpExecutableFinder)->find(false),
14 '-S',
15 $this->host().':'.$this->port(),
16 $server,
17 ];
18}
難しそうな処理が並んでいますが、やっていることを抜き出すと、PHPを開発サーバとして起動しています。
そして、パスに応じてリクエストを振り分ける処理として、server.phpなるファイルを指定しています。
このファイルにartisan serveコマンドの正体が詰まっているので、中身を読み解きます。
1// Illuminate/Foundation/resources/server.php;
2
3// PHPを開発サーバとして起動するときに、publicディレクトリ以下を指定している
4$publicPath = getcwd();
5
6// 中略...
7
8// public/index.phpが読み込まれる
9require_once $publicPath.'/index.php';
今まで見てきたpublic/index.phpへ合流していることが分かりました。
Laravelがコマンドを実行する仕組み・PHPの開発サーバとしての機能・そしてリクエストを扱う処理(public/index.php)を組み合わせることで、簡易な開発サーバを実現しているようでした。
補足: namespaceに指定されているIlluminateやFoundationは何を意味しているのか
これまで読み解いてきたLaravelのクラスは、IlluminateやFoundationといった名前空間に属していました。
これらが何を意味しているのか簡単に整理しておきましょう。名前空間の意味が見えてくれば、どこに何があるのかも探しやすくなるはずです。
Illuminateとは
IlluminateはLaravel4のコードネームを指しています。
LaravelがPHPでの開発体験を照らすことを願って名付けられたようです。
ですので、Laravelのクラスの大半は名前空間Illuminateに属しています。
Contractとは
名前空間Contractには、Interfaceが属しています。contractという単語が契約を意味することから、実装するべき処理を契約として表したのではないかと思われます。
参考
Foundationとは
英単語foundationが基盤を表す通り、Laravelにおける基盤となる処理が置かれています。
今回見てきた処理では、Kernelクラスやbootstrapper、そしてPipelineクラスなど、重要なものが関わっています。
明確に言及している文書は無さそうだったので、FoundationはLaravelで基盤になる処理が置かれているんだな、ぐらいに捉えておきましょう。
まとめ
Laravelへ入門するためにリクエストからレスポンスが組み立てられるまでの流れを見てきました。
興味がわいてきたらModelやViewなど、今回扱い切れなかった機能がどんな仕組みで実現されているのか、探ってみてください。
仕組みを理解していけば、Laravelも少しずつ手に馴染んでくるはずです。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
強くなりたい。