Laravel 底层原理 - Facade
你刚刚安装了一个全新的 Laravel 应用,启动了它,并得到了一个欢迎页面。和其他人一样,你尝试查看它是如何渲染的,所以转到 web.php
文件中,遇到以下代码
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
很明显,我们来到 welcome 视图,但你对 Laravel 的路由是如何工作的很好奇,所以你决定深入研究代码。最初的假设是:有一个 Route
类,我们在它上调用一个静态方法 get()
。但是,单击它时,那里没有 get()
方法。那么,发生了什么样的黑暗魔法呢?让我们解开这个谜!
常规 Facade
请注意,为了简单起见,我去掉了大部分 PHPDoc 并内联了类型,“…” 指的是更多的代码。
强烈建议你打开 IDE 并打开代码,以免出现混淆。
按照我们的示例,让我们探究 Route
类
<?php
namespace Illuminate\Support\Facades;
class Route extends Facade
{
// ...
protected static function getFacadeAccessor(): string
{
return 'router';
}
}
这里没有太多内容,只有返回字符串 router
的 getFacadeAccess()
方法。记住这一点,让我们转到父类
<?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
,我们知道使用 bootstrapWith()
解析时,其包含 bootstrap()
方法
<?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()
。它将在其 getFacadeAccess()
方法中注册的字符串解析为容器中绑定的任何字符串,并将我们的调用代理到检索到的实例。因此,我们最终得到了 (new HelloWorld())->method()
。这就是它的本质!你还是没明白?那么让我们创建 Facade。
创建 Facade
假设我们有这么一个类
<?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-word
字符串的 Facade。
<?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!
Laravel 实时 Facade
现在你已经对 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\Hellworld
究竟来自哪里呢?我知道这看起来像是妖术,不过一旦你掌握了,它其实很简单。
我们来仔细看看前述的 \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\\
前缀后,使用了未定义的类替换了伪内容。然后保存了这个更新后的 Facade。因此,当 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
类不需要使用任何参数构造。因此,容器调用并返回,所有的这些调用通过代理实现。
回顾实时 Facade:在类前使用前缀 Facades
。在应用启动区间,Laravel 注册 spl_autoload_register()
,其在调用未定义的类时触发。它最终导向 load()
方法。在 load()
中, 检测当前未定义类是否有 Facades
前缀。如果匹配,Laravel 尝试加载它。由于该 Facade 不存在,它从模板(stub)中创建,并引入该文件。就这样,你得到了一个普通的 Facade,不过该 Facade 是即时创建的。很酷吧?
总结
祝贺你走到这一步!我知道这可能有点让人不知所措。请随意返回并重读任何你不太明白的部分。通过 IDE 跟进也会有所帮助。不过,不再有黑魔法了,一定感觉很好,至少我第一次是这样感觉的!
记住,下次静态调用方法时,情况可能并非如此