您刚刚安装了一个全新的 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]; } }
它首先检查静态数组$resolvedInstance
是否具有使用给定$name
(再次说明为router
)设置的值。如果找到匹配项,则仅返回该值。这是 Laravel 缓存,用于稍微优化性能。此缓存发生在单个请求中。如果在同一请求中使用相同参数多次调用此方法,则它将使用缓存的值。我们假设这是初始调用并继续。
然后检查$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()); } } }
当请求通过时,它会被发送到路由器。在此之前,会调用bootstrap()
方法,该方法使用bootstrappers
数组来准备应用程序。如果您探索\Illuminate\Foundation\Application
类中的bootstrapWith()
方法,它会遍历这些 bootstrappers,并调用它们的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]; } }
我们确认在应用程序引导期间设置了$app
。下一步是通过验证$cached
来检查已解析的实例是否应该缓存,默认值为 true。最后,我们从应用程序容器中检索实例,在我们的例子中,这就像要求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
Facade;通常,你会发现一个 PHPDoc @see
提示这个 Facade 隐藏了什么,或者更准确地说,我们的方法调用将被代理到哪个类。
现在,回到我们的__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 返回一个绑定到容器的字符串。例如, hello-world
可能绑定到HelloWorld
类。当我们静态调用 Facade 上未定义的方法(例如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
实例进行操作。就是这样。
恭喜!您已经创建了属于自己的外观!
现在您已经对 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 正在调用spl_autoload_register()
函数,这是一个内置的 PHP 函数,在尝试访问未定义的类时触发。它定义了在这种情况下要执行的逻辑。在这种情况下,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。如果您检查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:我们在类上添加了Facades
前缀。在应用程序引导期间,Laravel 注册spl_autoload_register()
,当我们调用未定义的类时会触发它。它最终会导向load()
方法。在load()
内部,我们检查当前未定义的类是否以Facades
为前缀。如果匹配,Laravel 就会尝试加载它。
由于外观不存在,因此它会从存根创建外观,然后请求文件。瞧!您有一个常规外观,但这个外观是动态创建的。很酷,不是吗?
恭喜您走到这一步!我知道这可能有点让人不知所措。您可以随意返回并重新阅读任何您不太理解的部分。跟进您的 IDE 也会有所帮助。但是嘿,没有更多的黑魔法,感觉一定很好,至少这是我第一次的感觉!
请记住,下次静态调用方法时,情况可能就不一样了🪄