在 laravel 架構中,門面為應用服務容器中綁定的類提供了一個“靜态”接口。通常在項目開發中,我們為通過 ServiceProvider 注入容器的服務類建構一個門面,以便我們可以非常友善地調用這些類接口。并且由于門面使用了動态方法對服務容器中解析出來的對象方法調用進行了代理,我們也可以像測試注入類執行個體那樣測試門面。今天我們就來看一下 laravel 架構中 Facade 的源碼。
Facade 工作原理
在 Laravel 應用中,門面就是一個為容器中對象提供通路方式的類。該機制原理由 Facade 類實作。Laravel 自帶的門面,以及我們建立的自定義門面,都會繼承自 Illuminate\Support\Facades\Facade 基類。
門面類隻需要實作一個方法:
getFacadeAccessor
。正是
getFacadeAccessor
方法定義了從容器中解析什麼。然後 Facade 基類使用魔術方法
__callStatic()
代理門面上靜态方法的調用,并将其交給通過
getFacadeAccessor
方法定義的從容器中解析出來的服務類來執行。
Facade 核心源碼
接下來我們來看 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);
}
}
}
通過上面的方法,我們為門面類向程式中注冊了别名,這樣我們就可以使用别名在頂層命名空間下使用相應的門面類了。