新しい Laravel アプリケーションをインストールして起動し、ウェルカム ページが表示されました。他のユーザーと同様に、どのようにレンダリングされるかを確認しようとして、 web.php
ファイルに移動すると、次のコードが表示されます。
<?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); });
ウェルカム ビューを取得した方法は明らかですが、Laravel のルーターがどのように動作するのか興味があるので、コードを調べることにしました。最初の仮定は、静的メソッドget()
を呼び出すRoute
クラスがあるというものです。しかし、それをクリックしても、 get()
メソッドはありません。では、どのような黒魔術が起こっているのでしょうか。これを解明しましょう。
簡潔にするために、PHPDoc のほとんどを削除し、型をインライン化したことに注意してください。「...」は、追加のコードを指します。
混乱を避けるために、IDE を開いてコードに沿って進むことを強くお勧めします。
例に従って、 Route
クラスを調べてみましょう。
<?php namespace Illuminate\Support\Facades; class Route extends Facade { // ... protected static function getFacadeAccessor(): string { return 'router'; } }
ここには大したことはありません。文字列router
を返すgetFacadeAccessor()
メソッドがあるだけです。これを念頭に置いて、親クラスに移動しましょう。
<?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }
親クラスには多くのメソッドがありますが、 get()
メソッドはありません。しかし、 __callStatic()
メソッドという興味深いメソッドがあります。これはマジックメソッドで、このケースのget()
のような未定義の静的メソッドが呼び出されるたびに呼び出されます。したがって、呼び出し__callStatic('get', ['/', Closure()])
Route::get()
を呼び出すときに渡したもの、つまりルート/
と welcome ビューを返すClosure()
を表します。
__callStatic()
が呼び出されると、まずgetFacadeRoot()
を呼び出して変数$instance
を設定しようとします。 $instance
呼び出しを転送する実際のクラスが保持されます。詳しく見てみましょう。すぐに意味がわかります。
// Facade.php public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); }
ほら、これは子クラスRoute
のgetFacadeAccessor()
です。これは文字列router
を返すことがわかっています。次に、このrouter
文字列は、 resolveFacadeInstance()
に渡され、クラスへの解決が試みられます。これは、「この文字列はどのクラスを表しているか」を示す一種のマッピングです。見てみましょう。
// Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }
まず、静的配列$resolvedInstance
に、指定された$name
(これもrouter
) で設定された値があるかどうかを確認します。一致するものが見つかった場合は、その値を返します。これは、パフォーマンスを少し最適化するための Laravel キャッシュです。このキャッシュは、単一のリクエスト内で行われます。このメソッドが同じリクエスト内で同じ引数で複数回呼び出された場合は、キャッシュされた値が使用されます。これが最初の呼び出しであると仮定して、先に進みましょう。
次に、 $app
が設定されているかどうか、 $app
アプリケーションコンテナのインスタンスであるかどうかを確認します。
// Facade.php protected static \Illuminate\Contracts\Foundation\Application $app;
アプリケーション コンテナーがどのようなものか知りたい場合は、クラスが格納されているボックスと考えてください。クラスが必要な場合は、そのボックスに手を伸ばすだけです。このコンテナーは、時々ちょっとした魔法をかけてくれます。ボックスが空であっても、クラスを取ろうと手を伸ばすと、自動的に取得されます。これは別の記事で取り上げるトピックです。
ここで、「 $app
はいつ設定されるのか」と疑問に思うかもしれません。なぜなら、$app は設定される必要があるからです。そうしないと、 $instance
が取得されません。このアプリケーション コンテナーは、アプリケーションのブートストラップ プロセス中に設定されます。 \Illuminate\Foundation\Http\Kernel
クラスを簡単に見てみましょう。
<?php namespace Illuminate\Foundation\Http; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Facade; use Illuminate\Contracts\Http\Kernel as KernelContract; // ... class Kernel implements KernelContract { // ... protected $app; protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, // <- this guy \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; public function bootstrap(): void { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } }
リクエストが届くと、ルーターに送信されます。その直前に、 bootstrap()
メソッドが呼び出され、 bootstrappers
配列を使用してアプリケーションを準備します。 \Illuminate\Foundation\Application
クラスのbootstrapWith()
メソッドを調べると、これらのブートストラッパーを反復処理して、 bootstrap()
メソッドを呼び出していることがわかります。
簡単にするために、 bootstrapWith()
で呼び出されるbootstrap()
メソッドが含まれていることがわかっている\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 { // ... public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); // Interested here AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); } }
ここで、静的メソッドsetFacadeApplication().
を使用して、 Facade
クラスにアプリケーション コンテナーを設定します。
// RegisterFacades.php public static function setFacadeApplication($app) { static::$app = $app; }
ご覧のとおり、 resolveFacadeInstance()
内でテストしている$app
プロパティを割り当てています。これで質問の答えがわかりました。続けましょう。
// Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }
アプリケーションのブートストラップ中に$app
が設定されていることを確認しました。次の手順では、デフォルトで true に設定されている$cached
を検証して、解決されたインスタンスをキャッシュする必要があるかどうかを確認します。最後に、アプリケーション コンテナーからインスタンスを取得します。この場合、これはstatic::$app['router']
に、文字列router
にバインドされたクラスを提供するように要求するようなものです。
さて、 $app
アプリケーション コンテナのインスタンス、つまりオブジェクトであるにもかかわらず、配列のようにアクセスするのはなぜかと疑問に思うかもしれません。その通りです。ただし、アプリケーション コンテナはArrayAccess
と呼ばれる PHP インターフェイスを実装しており、配列のようなアクセスが可能です。この事実を確認するために、これを見てみましょう。
<?php namespace Illuminate\Container; use ArrayAccess; // <- this guy use Illuminate\Contracts\Container\Container as ContainerContract; class Container implements ArrayAccess, ContainerContract { // ... }
つまり、 resolveFacadeInstance()
router
文字列、具体的には\Illuminate\Routing\Router
にバインドされたインスタンスを実際に返します。どうしてわかったのでしょうか? Route
ファサードを見てください。多くの場合、このファサードが何を隠しているか、より正確には、メソッド呼び出しがどのクラスにプロキシされるかを示す PHPDoc @see
が見つかります。
さて、 __callStatic
メソッドに戻りましょう。
<?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }
\Illuminate\Routing\Router
クラスのオブジェクトである$instance
があります。これが設定されているかどうかをテストし (この場合は確認済み)、そのメソッドを直接呼び出します。つまり、次のようになります。
// Facade.php return $instance->get('/', Closure());
これで、 \Illuminate\Routing\Router
クラス内にget()
存在することを確認できます。
<?php namespace Illuminate\Routing; use Illuminate\Routing\Route; use Illuminate\Contracts\Routing\BindingRegistrar; use Illuminate\Contracts\Routing\Registrar as RegistrarContract; // ... class Router implements BindingRegistrar, RegistrarContract { // ... public function get(string $uri, array|string|callable|null $action = null): Route { return $this->addRoute(['GET', 'HEAD'], $uri, $action); } }
これで終わりです。結局、難しくなかったですか? まとめると、ファサードはコンテナーにバインドされた文字列を返します。たとえば、 hello-world
HelloWorld
クラスにバインドされている可能性があります。ファサード (たとえばHelloWorldFacade
) で未定義のメソッドを静的に呼び出すと、 __callStatic()
が介入します。
getFacadeAccessor()
メソッドに登録された文字列をコンテナ内でバインドされているものに解決し、取得したインスタンスへの呼び出しをプロキシします。したがって、 (new HelloWorld())->method()
になります。これが本質です。まだピンと来ませんか? それでは、ファサードを作成しましょう。
次のようなクラスがあるとします。
<?php namespace App\Http\Controllers; class HelloWorld { public function greet(): string { return "Hello, World!"; } }
目標はHelloWorld::greet()
を呼び出すことです。これを行うには、クラスをアプリケーション コンテナーにバインドします。まず、 AppServiceProvider
に移動します。
<?php namespace App\Providers; use App\Http\Controllers; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind('hello-world', function ($app) { return new HelloWorld; }); } // ... }
これで、アプリケーション コンテナー (または前述のボックス) からhello-world
を要求するたびに、 HelloWorld
のインスタンスが返されます。あとは、文字列hello-world
を返すファサードを作成するだけです。
<?php namespace App\Http\Facades; use Illuminate\Support\Facades\Facade; class HelloWorldFacade extends Facade { protected static function getFacadeAccessor() { return 'hello-world'; } }
これで、使用準備は完了です。web.php 内で呼び出してみましょうweb.php.
<?php use App\Http\Facades; use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorldFacade::greet(); // Hello, World! });
HelloWorldFacade
ファサードにgreet()
が存在しないことはわかっています。__ __callStatic()
がトリガーされます。これは、アプリケーション コンテナーから文字列 (この場合はhello-world
) で表されるクラスを取得します。また、このバインディングはAppServiceProvider
で既に作成されており、誰かがhello-world
をリクエストするたびにHelloWorld
のインスタンスを提供するように指示しています。その結果、 greet()
などの呼び出しは、取得したHelloWorld
のインスタンスに対して実行されます。これで完了です。
おめでとうございます!独自のファサードを作成しました。
ファサードについて十分に理解できたところで、もう 1 つ魔法のトリックを紹介します。リアルタイム ファサードを使用して、ファサードを作成せずにHelloWorld::greet()
を呼び出すことができると想像してください。
みてみましょう:
<?php use Facades\App\Http\Controllers; // Notice the prefix use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorld::greet(); // Hello, World! });
コントローラーの名前空間にFacades
というプレフィックスを付けることで、以前と同じ結果が得られます。しかし、 HelloWorld
コントローラーにはgreet()
という静的メソッドがないことは確かです。またFacades\App\Http\Controllers\HelloWorld
どこから来たのでしょうか。これは魔法のように思えるかもしれませんが、一度理解してしまえば、非常に簡単です。
先ほど確認した、$app の設定を担当するクラスである\Illuminate\Foundation\Bootstrap\RegisterFacades
を詳しく見てみましょう$app:
<?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 { public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); // Interested here } }
最後に、 register()
メソッドが呼び出されていることがわかります。内部を覗いてみましょう。
<?php namespace Illuminate\Foundation; class AliasLoader { // ... protected $registered = false; public function register(): void { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } }
$registered
変数は最初はfalse
に設定されています。したがって、 if
ステートメントに入り、 prependToLoaderStack()
メソッドを呼び出します。それでは、その実装を調べてみましょう。
// AliasLoader.php protected function prependToLoaderStack(): void { spl_autoload_register([$this, 'load'], true, true); }
ここで魔法が起こります。Laravel は、未定義のクラスにアクセスしようとしたときにトリガーされる組み込みの PHP 関数であるspl_autoload_register()
関数を呼び出します。これは、このような状況で実行するロジックを定義します。この場合、Laravel は、未定義の呼び出しに遭遇したときにload()
メソッドを呼び出すことを選択します。
さらに、 spl_autoload_register()
呼び出されるメソッドまたは関数に未定義のクラスの名前を自動的に渡します。
load()
メソッドを調べてみましょう。これが鍵となるはずです。
// AliasLoader.php public function load($alias) { if (static::$facadeNamespace && str_starts_with($alias, static::$facadeNamespace)) { $this->loadFacade($alias); return true; } if (isset($this->aliases[$alias])) { return class_alias($this->aliases[$alias], $alias); } }
$facadeNamespace
が設定されているかどうか、また渡されたクラス(この場合はFacades\App\Http\Controllers\HelloWorld
$facadeNamespace
に設定されている内容で始まるかどうかを確認します。
このロジックは、 $facadeNamespace
が設定されているかどうか、および渡されたクラス (この場合はFacades\App\Http\Controllers\HelloWorld
(未定義)) が$facadeNamespace.
// AliasLoader.php protected static $facadeNamespace = 'Facades\\';
コントローラの名前空間にFacades
をプレフィックスとして追加し、条件を満たしたので、 loadFacade()
に進みます。
// AliasLoader.php protected function loadFacade($alias) { require $this->ensureFacadeExists($alias); }
ここで、メソッドは、 ensureFacadeExists()
から返されるパスを必要とします。したがって、次のステップでは、その実装を詳しく調べます。
// AliasLoader.php protected function ensureFacadeExists($alias) { if (is_file($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) { return $path; } file_put_contents($path, $this->formatFacadeStub( $alias, file_get_contents(__DIR__.'/stubs/facade.stub') )); return $path; }
まず、 framework/cache/facade-'.sha1($alias).'.php'
という名前のファイルが存在するかどうかを確認します。この場合、このファイルは存在しないため、次のステップであるfile_put_contents()
がトリガーされます。この関数はファイルを作成し、指定された$path
に保存します。ファイルの内容はformatFacadeStub()
によって生成されます。名前から判断すると、これはスタブからファサードを作成します。facade.stub facade.stub
調べると、次のようになります。
<?php namespace DummyNamespace; use Illuminate\Support\Facades\Facade; /** * @see \DummyTarget */ class DummyClass extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'DummyTarget'; } }
見覚えがありますか? 基本的に、これは手動で行ったことです。ここで、 formatFacadeStub()
Facades\\
プレフィックスを削除した後、ダミーのコンテンツを未定義のクラスに置き換えます。その後、この更新されたファサードが保存されます。その結果、 loadFacade()
ファイルを必要とする場合、正しく要求され、次のファイルが必要になります。
<?php namespace Facades\App\Http\Controllers; use Illuminate\Support\Facades\Facade; /** * @see \App\Http\Controllers\HelloWorld */ class HelloWorld extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'App\Http\Controllers\HelloWorld'; } }
そして、通常のフローでは、アプリケーション コンテナーに、文字列App\Http\Controllers\HelloWorld
にバインドされたインスタンスを返すように要求します。この文字列を何にもバインドしていないし、 AppServiceProvider
にも触れていないのに、と不思議に思うかもしれません。しかし、冒頭でアプリケーション コンテナーについて述べたことを覚えていますか?
ボックスが空の場合でも、インスタンスが返されますが、条件が 1 つあります。クラスにはコンストラクターがあってはなりません。そうでない場合、クラスはそれをどのように構築すればよいかわかりません。この場合、 HelloWorld
クラスは構築するために引数を必要としません。そのため、コンテナーはそれを解決して返し、すべての呼び出しはそれにプロキシされます。
リアルタイム ファサードの要約: クラスにFacades
というプレフィックスを付けました。アプリケーションのブートストラップ中に、Laravel はspl_autoload_register()
を登録します。これは、未定義のクラスを呼び出すとトリガーされます。最終的にはload()
メソッドに至ります。load load()
内では、現在の未定義のクラスにFacades
というプレフィックスが付いているかどうかを確認します。一致するため、Laravel はそれをロードしようとします。
ファサードが存在しないため、スタブからファサードを作成し、ファイルを要求します。すると、出来上がりです。通常のファサードがありますが、これはオンザフライで作成されました。かなりクールですよね?
ここまでお読みいただき、ありがとうございます。少し圧倒されるかもしれませんね。理解できなかったセクションは、戻ってもう一度読んでみてください。IDE でフォローするのも役立ちます。でも、黒魔術はもうやめて、気分がいいに違いありません。少なくとも、最初はそう感じました。
そして、次にメソッドを静的に呼び出すときには、そうではないかもしれないことを覚えておいてください🪄