在 laravel 框架中,门面为应用服务容器中绑定的类提供了一个“静态”接口。通常在项目开发中,我们为通过 ServiceProvider 注入容器的服务类构建一个门面,以便我们可以非常方便地调用这些类接口。并且由于门面使用了动态方法对服务容器中解析出来的对象方法调用进行了代理,我们也可以像测试注入类实例那样测试门面。今天我们就来看一下 laravel 框架中 Facade 的源码。
在 Laravel 应用中,门面就是一个为容器中对象提供访问方式的类。该机制原理由 Facade 类实现。Laravel 自带的门面,以及我们创建的自定义门面,都会继承自 Illuminate\Support\Facades\Facade 基类。
门面类只需要实现一个方法:getFacadeAccessor
。正是 getFacadeAccessor
方法定义了从容器中解析什么。然后 Facade 基类使用魔术方法 __callStatic()
代理门面上静态方法的调用,并将其交给通过 getFacadeAccessor
方法定义的从容器中解析出来的服务类来执行。
接下来我们来看 Facade 类的主要源码
namespace Illuminate\Support\Facades;
abstract class Facade
{
/**
* The application instance being facaded.
* Application 对象的实例
* @var \Illuminate\Contracts\Foundation\Application
*/
protected static $app;
/**
* The resolved object instances.
* 解析出来的门面对应的服务类实例。索引数组,键:门面名称;值:服务类实例
* @var array
*/
protected static $resolvedInstance;
/**
* Hotswap the underlying instance behind the facade.
* 更换当前门面对应的对象实例
* @param mixed $instance
* @return void
*/
public static function swap($instance)
{
static::$resolvedInstance[static::getFacadeAccessor()] = $instance;
static::$app->instance(static::getFacadeAccessor(), $instance);
}
/**
* Get the root object behind the facade.
* 返回当前门面对应服务对象的实例
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
/**
* Get the registered name of the component.
* 返回此门面具体对应的服务类名,
* 一般为绑定在 Application 中的类对应的 $abstract,
* 或者带有完整命名空间的类名。由子类实现此方法
* @return string
* @throws \RuntimeException
*/
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
/**
* Resolve the facade root instance from the container.
* 创建并返回 $name 对应的对象实例
* @param string|object $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
//具体的创建操作由 Application 类对象的来进行,
//所以 $name 需要在 Application 对象中进行过绑定,
//或者 $name 是带有完整命名空间的类名
return static::$resolvedInstance[$name] = static::$app[$name];
}
/**
* Clear a resolved facade instance.
* 清除 $name 对应的服务对象实例
* @param string $name
* @return void
*/
public static function clearResolvedInstance($name)
{
unset(static::$resolvedInstance[$name]);
}
/**
* Clear all of the resolved instances.
* 清除门面里所有的服务对象
* @return void
*/
public static function clearResolvedInstances()
{
static::$resolvedInstance = [];
}
/**
* Handle dynamic, static calls to the object.
* 动态绑定,将门面的静态方法调用绑定到门面对应的服务对象实例来执行
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
}
至此我们看完了 Facade 的核心源码,其核心就是通过 getFacadeAccessor
标识门面对应的服务类,通过 __callStatic
来动态绑定门面上的静态方法调用,将其绑定到门面对应的服务对象上来调用。我们看到 Facade 类中还有 spy
、shouldReceive
、createFreshMockInstance
、createMockByName
、isMock
、getMockableClass
等方法,这些方法都是门面的模拟测试相关的方法,这里我们就不具体分析了。
我们看完了门面的类的实现,知道了门面如何通过静态调用的方法调用服务实例对象的方法。门面子类一般定义在不同的位置,位于不同的命名空间下,而我们在使用门面的时候,则是直接在顶层命名空间下调用,这又是怎么实现的呢?
我们在创建一个门面的时候,一般除了会创建一个 Facade 类的子类,是我们具体的门面类外,还会在 app.aliases
配置文件下添加一个配置,是门面类的别名与门面类的对应关系,这样我们就可以用顶层命名空间调用门面了。这又是如何实现的呢?我们接下来看门面的自动加载。
在框架启动过程中,会调用 Illuminate\Foundation\Bootstrap\RegisterFacades
类的 bootstrap
方法,我们来看这个类的源码实现:
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Support\Facades\Facade;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Contracts\Foundation\Application;
class RegisterFacades
{
/**
* Bootstrap the given application.
* 启动框架的门面服务
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance($app->make('config')->get('app.aliases', []))->register();
}
}
我们看到上面的代码是使用 AliasLoader
类注册 app.aliases
配置下的别名。我们来看相应代码:
namespace Illuminate\Foundation;
class AliasLoader{
/**
* The array of class aliases.
* @var array
*/
protected $aliases;
/**
* Get or create the singleton alias loader instance.
* 返回或穿创建针对 $aliases 的 AliasLoader 类
* @param array $aliases
* @return \Illuminate\Foundation\AliasLoader
*/
public static function getInstance(array $aliases = [])
{
if (is_null(static::$instance)) {
return static::$instance = new static($aliases);
}
$aliases = array_merge(static::$instance->getAliases(), $aliases);
static::$instance->setAliases($aliases);
return static::$instance;
}
/**
* Register the loader on the auto-loader stack.
* 注册当前的加载器到程序的自动加载栈
* @return void
*/
public function register()
{
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
/**
* Prepend the load method to the auto-loader stack.
* 注册当前对象的 load 方法到程序的自动加载栈
* @return void
*/
protected function prependToLoaderStack()
{
spl_autoload_register([$this, 'load'], true, true);
}
/**
* Load a class alias if it is registered.
* 当程序调用一个不存在的类的时候,调用这个方法
* @param string $alias
* @return bool|null
*/
public function load($alias)
{
//class_alias 为一个类创建别名
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
}
通过上面的方法,我们为门面类向程序中注册了别名,这样我们就可以使用别名在顶层命名空间下使用相应的门面类了。