您刚刚安装了一个全新的 Laravel 应用程序,启动它并获得了欢迎页面。像其他人一样,您尝试查看它是如何呈现的,因此您跳转到 文件并遇到以下代码: web.php <?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); 很明显,我们是如何获得欢迎视图的,但你对 Laravel 路由器的工作原理很好奇,所以你决定深入研究代码。最初的假设是:有一个 类,我们在其上调用静态方法 。但是,单击它时,那里没有 方法。那么,到底发生了什么黑魔法?让我们揭开它的神秘面纱! Route get() get() 常规立面 请注意,为了简单起见,我删除了大部分 PHPDocs 并内联了类型,“...”指的是更多代码。 我强烈建议您打开 IDE 并按照代码进行操作,以避免任何混淆。 按照我们的例子,让我们探索一下 类。 Route <?php namespace Illuminate\Support\Facades; class Route extends Facade { // ... protected static function getFacadeAccessor(): string { return 'router'; } } 这里没有太多内容,只有 方法返回字符串 。记住这一点,让我们转到父类。 getFacadeAccessor() router <?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() / 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 $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()); } } } 当请求通过时,它会被发送到路由器。在此之前,会调用 方法,该方法使用 数组来准备应用程序。如果您探索 类中的 方法,它会遍历这些 bootstrappers,并调用它们的 方法。 bootstrap() bootstrappers \Illuminate\Foundation\Application bootstrapWith() bootstrap() 为了简单起见,我们只关注 ,我们知道它包含一个 方法,该方法将在 中调用 \Illuminate\Foundation\Bootstrap\RegisterFacades bootstrap() bootstrapWith() <?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 { // ... } 因此, 确实返回了绑定到 字符串的实例,具体来说就是 。我怎么知道的?看一下 Facade;通常,你会发现一个 PHPDoc 提示这个 Facade 隐藏了什么,或者更准确地说,我们的方法调用将被代理到哪个类。 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); } } 我们有 ,它是 类的一个对象。我们测试它是否已设置(在我们的例子中,已确认),然后直接调用它的方法。所以,我们最终得到了。 $instance \Illuminate\Routing\Router // Facade.php return $instance->get('/', Closure()); 现在,您可以确认 存在于 类中。 get() \Illuminate\Routing\Router <?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); } } 就这样结束了!最后是不是很难?总结一下,facade 返回一个绑定到容器的字符串。例如, 可能绑定到 类。当我们静态调用 Facade 上未定义的方法(例如 )时, 就会介入。 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. <?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 实时 Facades 现在您已经对 Facade 有了很好的理解,还有一个魔术要揭晓。想象一下,使用 无需创建 Facade 即可调用 。 实时 Facade 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 让我们仔细看看我们之前检查过的 ,这个类负责设置 \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。如果您检查 ,您会发现以下内容: 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 ,但有一个条件,即该类不能有构造函数。否则,它就不知道如何为您构建它。在我们的例子中,我们的 类不需要任何参数来构造。因此,容器会解析它、返回它,并且所有调用都会代理到它。 即使容器是空的,它也会返回实例 HelloWorld 回顾实时 Facades:我们在类上添加了 前缀。在应用程序引导期间,Laravel 注册 ,当我们调用未定义的类时会触发它。它最终会导向 方法。在 内部,我们检查当前未定义的类是否以 为前缀。如果匹配,Laravel 就会尝试加载它。 Facades spl_autoload_register() load() load() Facades 由于外观不存在,因此它会从存根创建外观,然后请求文件。瞧!您有一个常规外观,但这个外观是动态创建的。很酷,不是吗? 结论 恭喜您走到这一步!我知道这可能有点让人不知所措。您可以随意返回并重新阅读任何您不太理解的部分。跟进您的 IDE 也会有所帮助。但是嘿,没有更多的黑魔法,感觉一定很好,至少这是我第一次的感觉! 请记住,下次静态调用方法时,情况可能就不一样了🪄