まえがき
背景
エンジニア仲間でLaravelのソースコードを輪読することにしました。Laravelのソースコードとは言っても膨大な量があるので、まずはルーティング処理から読んでいきます。
1回目の先週はKernelやFacadeなどの概念をさらいながら App\Providers\RouteServiceProvider
クラスの mapWebRoutes()
メソッドを読むという発表がありました。2回目の今日はルーティング情報の設定がどのように行われるのかを厳密に読んでいきたいと思います。
ローカル開発環境
macOSです。
> sw_vers -productVersion
10.15.4
> php --version
PHP 7.2.30 (cli) (built: Apr 23 2020 02:40:39) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.30, Copyright (c) 1999-2018, by Zend Technologies
> composer --version
Composer version 1.10.5 2020-04-10 11:44:22
ソースコードを読むアプリケーションの作成方法
今回読むソースコードは Installation - Laravel - The PHP Framework For Web Artisans > Installing Laravel の Via Laravel Installer に書いてある手順に従ってローカルで作成したものです。
routes/web.php
を見てみよう
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
routes/web.php
は何ですか?
長部はこのように理解しています ↓
- 私たち アプリケーションの開発者が編集するファイル である。すなわち、フレームワークのファイルではなく、アプリケーションによって異なるファイルである。
- ブラウザからアプリケーションにアクセスされた時の ルーティング情報を記述するファイル である。ここで、 ルーティング情報 という用語を「これこれのHTTPメソッドでこれこれのURIをリクエストされたとき、これこれのControllerのこれこれのActionを実行する」もしくは「これこれのHTTPメソッドでこれこれのURIをリクエストされたとき、これこれのViewをレンダリングしてレスポンスを返す」といった情報の意味で使っている。また、 ブラウザ と言ったが、これは Webクライアント の方が正確である。
- HTTPリクエストのうち、 ユーザーが見るページのルーティング情報を記述するファイル であって、APIなどユーザーがレスポンスを直接は見ないリクエストを記述するファイルではない。
参考文献 ↓
- Routing - Laravel - The PHP Framework For Web Artisans > Basic Routing > The Default Route Files
次は何をしますか?
-
Route::get('/', function () { ... });
を実行した時に起きることを1段階深掘りします。 - 深掘りする過程で、必要なら
routes/web.php
が読み込まれるまでに起きることを理解します。
Illuminate\Support\Facades\Route
の定義を見てみよう
<?php
namespace Illuminate\Support\Facades;
/**
* @method static \Illuminate\Routing\Route fallback(array|string|callable|null $action = null)
* (中略)
* @method static \Illuminate\Routing\Route getCurrentRoute()
*
* @see \Illuminate\Routing\Router
*/
class Route extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'router';
}
}
Illuminate\Support\Facades\Route
クラスは Illuminate\Support\Facades\Facade
クラスを継承していることがわかったので、 Illuminate\Support\Facades\Facade
の定義も見てみましょう。
<?php
namespace Illuminate\Support\Facades;
use Closure;
use Mockery;
use Mockery\MockInterface;
use RuntimeException;
abstract class Facade
{
/**
* The application instance being facaded.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected static $app;
// 中略
/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
}
この2つのファイルを見ると、 Route::get('/', function () { ... });
を1段階深掘りするためには Illuminate\Support\Facades\Route
クラスか Illuminate\Support\Facades\Facade
クラスの定義を読めばいい ことがわかります。その理由は以下のようなものです ↓
-
Illuminate\Support\Facades\Route
クラスはIlluminate\Support\Facades\Facade
クラスを継承している。 -
Illuminate\Support\Facades\Facade
クラスはどっかのクラスを継承していない。 -
2つのクラスともどっかのインターフェースを実装していない。=> メソッドの実装を探すときにはインターフェースを見る必要がないことに気づいたのですが、PHPマニュアルへのリンクを残したいので打ち消し線を引いておきました。 - 2つのクラスともどっかのトレイトを追加していない。
Route::get()
メソッドの実装はどこにありますか?
結論はこうです ↓
- 1段階深掘りするなら
Route::get()
の実装はIlluminate\Support\Facades\Facade
クラスの__callStatic()
メソッド である。 - それ以上深掘りするとなんだかよくわからなくなる。
Illuminate\Support\Facades\Facade::__callStatic()
メソッドを読めばいいという結論に至る過程は以下のようなものです ↓
-
Illuminate\Support\Facades\Route
クラスにはget()
メソッドが定義されていない。 -
Illuminate\Support\Facades\Route
クラスの親クラスであるIlluminate\Support\Facades\Facade
クラスにもget()
メソッドが定義されていない。 - 2つのクラスともどっかのトレイトを追加しているわけではないため、
get()
メソッドの実装がどっか別のトレイトで定義されているわけでもない。 - 明示的な実装が定義されていないならマジックメソッド
__callStatic()
が定義されていないとおかしい。
Route::get()
を実行した時に何が起きますか?
↓ これを実行することは、
Route::get('/', function () {
return view('welcome');
});
↓ これを実行することと同じと考えてよいです。
Illuminate\Support\Facades\Route::__callStatic('get', [
'/',
function () {
return view('welcome');
},
]);
PHP: オーバーロード - Manual > メソッドのオーバーロード によれば、 __callStatic()
メソッドの第一引数には「コールしようとしたメソッドの名前」すなわち 'get'
が入り、第二引数には「メソッドに渡そうとしたパラメータが配列で」すなわち要素が function () { ... }
だけからなる配列 [function () { ... }]
が入ります。
__callStatic()
メソッドが定義されているクラスは Illuminate\Support\Facades\Facade
ですが、メソッドを呼び出し元のクラスは Illuminate\Support\Facades\Route
です。
__callStatic()
メソッドの中の static::
は今回の場合は Illuminate\Support\Facades\Route
クラスに解決されるので、まずは Illuminate\Support\Facades\Route::getFacadeRoot()
メソッドが実行されて返り値が $instance
に代入され、例外処理をした後に $instance
オブジェクトの get()
メソッドが function () { ... }
を引数として呼び出されます。
次は何をしますか?
-
$instance = static::getFacadeRoot()
を実行した時に起きることを1段階深掘りします。 - 深掘りする過程で、必要なら
routes/web.php
が読み込まれるまでに起きることを理解します。
Illuminate\Support\Facades\Facade::getFacadeRoot()
の定義を見てみよう
/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
getFacadeRoot()
メソッドの中の static::
は今回も Illuminate\Support\Facades\Route
クラスに解決されます。 resolveFacadeInstance()
メソッドは Illuminate\Support\Facades\Route
クラスには定義されておらず、 Illuminate\Support\Facades\Facade
クラスに定義されているのでそれを見てみましょう。
/**
* Resolve the facade root instance from the container.
*
* @param object|string $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
getFacadeAccessor()
メソッドは Illuminate\Support\Facades\Route
クラスに定義されているのでそれも見てみましょう。
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'router';
}
$instance = static::getFacadeRoot()
を実行した時に何が起きますか?
↓ これを実行することは、
$instance = static::getFacadeRoot();
↓ これを実行することと同じと考えてよいです。
$instance = Illuminate\Support\Facades\Facade::resolveFacadeInstance('router');
$name = 'router'
は文字列なので以下はスキップされます ↓
if (is_object($name)) {
return $name;
}
static::$resolvedInstance[$name]
が存在して NULL
でない時には static::$resolvedInstance[$name]
をそのまま返しているので、今回は static::$resolvedInstance[$name]
が存在しないか NULL
でない時のことを考えます。この場合は以下の処理が行われます ↓
-
static::$app[$name]
を取得してstatic::$resolvedInstance[$name]
に代入します。 -
static::resolveFacadeInstance()
はstatic::$resolvedInstance[$name]
を返します。 -
static::getFacadeRoot()
はそのままそれを返します。 -
Illuminate\Support\Facades\Facade::__callStatic()
の中の$instance
にはそれがそのまま代入されます。
以上のステップの static::
は全て Illuminate\Support\Facades\Route
クラスに解決されます。したがって Illuminate\Support\Facades\Route::$app['router']
の中身がわかれば return $instance->$method(...$args)
を実行した時に何が起きるかを1段階深掘りすることができます。
次は何をしますか?
-
Illuminate\Support\Facades\Route::$app['router']
の中身を1段階深掘りします。 - 深掘りする過程で、必要なら
routes/web.php
が読み込まれるまでに起きることを理解します。
Illuminate\Support\Facades\Route::$app には何が入っているのか調べよう
Illuminate\Support\Facades\Route
クラスには $app
というクラスプロパティは定義されていません。 Illuminate\Support\Facades\Route
クラスの親クラスである Illuminate\Support\Facades\Facade
クラスには $app
というクラスプロパティが定義されているため、 Illuminate\Support\Facades\Route::$app
はこちらを指しています。
/**
* The application instance being facaded.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected static $app;
結論から言ってしまうと Illuminate\Support\Facades\Facade::$app
には Illuminate\Foundation\Application
クラスのシングルトンが入っています。 Illuminate\Support\Facades\Facade::$app
に Illuminate\Foundation\Application
クラスのシングルトンが入るまでを厳密に追うのには時間がかかるので、気になった方が追いやすくなるように細部を省いてプロットしておきます。
public/index.php
bootstrap/app.php
を読み込んで実行し、返り値を $app
に代入しています。
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__.'/../bootstrap/app.php';
bootstrap/app.php
$app
には Illuminate\Foundation\Application
クラスのインスタンスが入ります。
/*
|--------------------------------------------------------------------------
| 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__)
);
↓ こんな設定をしています。
-
$app
が「Illuminate\Contract\Http\Kernel
インターフェースを実装したクラスのインスタンスを作れ」と言われたら、$app
はApp\Http\Kernel
クラスのインスタンスを作る。 -
App\Http\Kernel
クラスのインスタンスは1つしか作られない。すなわちシングルトンである。
/*
|--------------------------------------------------------------------------
| 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
);
public/index.php 再び
$kernel
には App\Http\Kernel
クラスのインスタンスが入ります。
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
App\Http\Kernel
クラスの handle()
メソッドを呼び出します。
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
Illuminate\Foundation\Http\Kernel
App\Http\Kernel
クラスは Illuminate\Foundation\Http\Kernel
クラスを継承しており、 handle()
メソッドはこちらのクラスに定義されています。
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Throwable $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
$request->enableHttpMethodParameterOverride();
はすっ飛ばして $response = $this->sendRequestThroughRouter($request)
を深掘りします。
/**
* 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());
}
いろいろすっ飛ばして $this->bootstrap();
を深掘りします。
/**
* Bootstrap the application for HTTP requests.
*
* @return void
*/
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
条件分岐はすっ飛ばして $this->app->bootstrapWith($this->bootstrappers());
を深掘りします。まずは Illuminate\Foundation\Http\Kernel
クラスの bootstrappers()
メソッドから読みます。
/**
* Get the bootstrap classes for the application.
*
* @return array
*/
protected function bootstrappers()
{
return $this->bootstrappers;
}
$this->bootstrappers
も Illuminate\Foundation\Http\Kernel
クラスに定義されています。
/**
* The bootstrap classes for the application.
*
* @var array
*/
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,
];
Illuminate\Foundation\Bootstrap\RegisterFacades
Illuminate\Foundation\Bootstrap\RegisterFacades
クラスは小さいので貼ってしまします。
<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Support\Facades\Facade;
class RegisterFacades
{
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
}
Illuminate\Support\Facades\Facade
Illuminate\Support\Facades\Facade
クラスの setFacadeApplication()
メソッドの定義も貼っておきます。
/**
* Set the application instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public static function setFacadeApplication($app)
{
static::$app = $app;
}
最後に Illuminate\Foundation\Application
クラスの bootstrapWith()
メソッドを読めば全部繋がります。
Illuminate\Foundation\Application
/**
* 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]);
}
}
次は何をしますか?
-
Illuminate\Support\Facades\Route::$app['router']
の中身をもう1段階深掘りします。 - 深掘りする過程で、必要なら
routes/web.php
が読み込まれるまでに起きることを理解します。
Illuminate\Support\Facades\Route::$app['router'] には何が入っているのか調べよう
$app
はオブジェクトなのに、配列のように $app['router']
という記述がされています。まずはこれがどうやって動くのかを調べます。
Illuminate\Foundation\Application
クラスは Illuminate\Container\Container
クラスを継承しており、 Illuminate\Container\Container
クラスは ArrayAccess
というインターフェースを実装しています。 ArrayAccess
は SPL(Standard PHP Library)で定義されているインターフェースの1つ で、次の4つのメソッドを定義してやることでオブジェクトに対して配列のようなアクセスをすることが可能になります。
- offsetExists()
- offsetGet()
- offsetSet()
- offsetUnset()
Illuminate\Container\Container クラスの offsetGet() メソッド
$app['router']
を取得しようとしたときは $app
に入っているオブジェクトの offsetGet()
メソッドが呼び出されます。これは Illuminate\Container\Container
クラスに定義されています。
/**
* Get the value at a given offset.
*
* @param string $key
* @return mixed
*/
public function offsetGet($key)
{
return $this->make($key);
}
Illuminate\Foundation\Application クラスの make() メソッド
offsetGet()
メソッドを呼び出したオブジェクト $app
は Illuminate\Foundation\Application
クラスのインスタンスなので、まずは Illuminate\Foundation\Application
クラスに定義されている make()
メソッドが呼ばれます。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
public function make($abstract, array $parameters = [])
{
$this->loadDeferredProviderIfNeeded($abstract = $this->getAlias($abstract));
return parent::make($abstract, $parameters);
}
Illuminate\Container\Container クラスの make() メソッド
返される値は parent::make($abstract, $parameters)
なので、 $this->loadDeferredProviderIfNeeded($abstract = $this->getAlias($abstract));
は飛ばして Illuminate\Container\Container
クラスの make()
メソッドを貼ります。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
Illuminate\Foundation\Application クラスの resolve() メソッド
offsetGet()
メソッドを呼び出したオブジェクト $app
は Illuminate\Foundation\Application
クラスのインスタンスなので、戻って Illuminate\Foundation\Application
クラスに定義されている resolve()
メソッドが呼ばれます。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$this->loadDeferredProviderIfNeeded($abstract = $this->getAlias($abstract));
return parent::resolve($abstract, $parameters, $raiseEvents);
}
Illuminate\Container\Container クラスの resolve() メソッド
返される値は parent::resolve($abstract, $parameters, $raiseEvents)
なので、 $this->loadDeferredProviderIfNeeded($abstract = $this->getAlias($abstract));
は飛ばして Illuminate\Container\Container
クラスの resolve()
メソッドを貼ります。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$concrete = $this->getContextualConcrete($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
if (is_null($concrete)) {
$concrete = $this->getConcrete($abstract);
}
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
ここで頭が爆発したのでレジュメはここまでです。輪講までに理解できたらその場で話すかもです。