paint-brush
Laravel 底层原理 - 什么是 Facades?经过@oussamamater
新歷史

Laravel 底层原理 - 什么是 Facades?

经过 Oussama Mater14m2024/06/29
Read on Terminal Reader

太長; 讀書

Laravel 附带了许多我们经常使用的 Facade。我们将讨论它们是什么、如何创建自己的 Facade,并了解实时 Facade。
featured image - Laravel 底层原理 - 什么是 Facades?
Oussama Mater HackerNoon profile picture

您刚刚安装了一个全新的 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实例进行操作。就是这样。


恭喜!您已经创建了属于自己的外观!

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 正在调用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 也会有所帮助。但是嘿,没有更多的黑魔法,感觉一定很好,至少这是我第一次的感觉!


请记住,下次静态调用方法时,情况可能就不一样了🪄