新しい 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); } } 親クラスには多くのメソッドがありますが、 メソッドはありません。しかし、 メソッドという興味深いメソッドがあります。これは メソッドで、このケースの のような未定義の静的メソッドが呼び出されるたびに呼び出されます。したがって、呼び出し を呼び出すときに渡したもの、つまりルート と welcome ビューを返す を表します。 get() __callStatic() マジック get() __callStatic('get', ['/', Closure()]) Route::get() / 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]; } } まず、静的配列 に、指定された (これも ) で設定された値があるかどうかを確認します。一致するものが見つかった場合は、その値を返します。これは、パフォーマンスを少し最適化するための Laravel キャッシュです。このキャッシュは、単一のリクエスト内で行われます。このメソッドが同じリクエスト内で同じ引数で複数回呼び出された場合は、キャッシュされた値が使用されます。これが最初の呼び出しであると仮定して、先に進みましょう。 $resolvedInstance $name router 次に、 が設定されているかどうか、 アプリケーションコンテナのインスタンスであるかどうかを確認します。 $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]; } } アプリケーションのブートストラップ中に が設定されていることを確認しました。次の手順では、デフォルトで true に設定されている を検証して、解決されたインスタンスをキャッシュする必要があるかどうかを確認します。最後に、アプリケーション コンテナーからインスタンスを取得します。この場合、これは に、文字列 にバインドされたクラスを提供するように要求するようなものです。 $app $cached static::$app['router'] router さて、 アプリケーション コンテナのインスタンス、つまり であるにもかかわらず、配列のようにアクセスするのはなぜかと疑問に思うかもしれません。その通りです。ただし、アプリケーション コンテナは と呼ばれる PHP インターフェイスを実装しており、配列のようなアクセスが可能です。この事実を確認するために、これを見てみましょう。 $app オブジェクト ArrayAccess <?php namespace Illuminate\Container; use ArrayAccess; // <- this guy use Illuminate\Contracts\Container\Container as ContainerContract; class Container implements ArrayAccess, ContainerContract { // ... } つまり、 文字列、具体的には にバインドされたインスタンスを実際に返します。どうしてわかったのでしょうか? ファサードを見てください。多くの場合、このファサードが何を隠しているか、より正確には、メソッド呼び出しがどのクラスにプロキシされるかを示す PHPDoc が見つかります。 resolveFacadeInstance() router \Illuminate\Routing\Router Route @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 おめでとうございます!独自のファサードを作成しました。 Laravel リアルタイム ファサード ファサードについて十分に理解できたところで、もう 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 関数である 関数を呼び出します。これは、このような状況で実行するロジックを定義します。この場合、Laravel は、未定義の呼び出しに遭遇したときに メソッドを呼び出すことを選択します。 spl_autoload_register() 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; } まず、 という名前のファイルが存在するかどうかを確認します。この場合、このファイルは存在しないため、次のステップである がトリガーされます。この関数はファイルを作成し、指定された に保存します。ファイルの内容は によって生成されます。名前から判断すると、これはスタブからファサードを作成します。facade.stub 調べると、次のようになります。 framework/cache/facade-'.sha1($alias).'.php' file_put_contents() $path formatFacadeStub() 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 リアルタイム ファサードの要約: クラスに というプレフィックスを付けました。アプリケーションのブートストラップ中に、Laravel は を登録します。これは、未定義のクラスを呼び出すとトリガーされます。最終的には メソッドに至ります。load 内では、現在の未定義のクラスに というプレフィックスが付いているかどうかを確認します。一致するため、Laravel はそれをロードしようとします。 Facades spl_autoload_register() load() load() Facades ファサードが存在しないため、スタブからファサードを作成し、ファイルを要求します。すると、出来上がりです。通常のファサードがありますが、これはオンザフライで作成されました。かなりクールですよね? 結論 ここまでお読みいただき、ありがとうございます。少し圧倒されるかもしれませんね。理解できなかったセクションは、戻ってもう一度読んでみてください。IDE でフォローするのも役立ちます。でも、黒魔術はもうやめて、気分がいいに違いありません。少なくとも、最初はそう感じました。 そして、次にメソッドを静的に呼び出すときには、そうではないかもしれないことを覚えておいてください🪄