正文
今天我们将学习 Laravel 核心架构中的另一个主题「Facade(外观)」。
本文将从以下几个方面出发,全面讲解 Laravel 中
Facade
的运行原理,为了便于理解后续中所有
Facade
译作「外观」:
-
简单介绍「外观」设计模式;
-
Laravel「外观」的加载原理;
-
Laravel「外观」基本使用。
什么是「外观」设计模式
外观模式定义
为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互, 为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。 -
设计模式 Java 版
核心
就是在
客户端(使用者)
与
子系统(接口或服务)
之间引入一个「外观」角色。
将使用者与子系统从直接耦合,转变成由「外观」类提供统一的接口给使用者使用,以降低客户端与子系统之间的耦合度。
结构示意图:
关于「外观模式」可以阅读
设计模式 Java 版 - 外观模式
Laravel 外观组件
Laravel 中的「外观」组件实际上是服务容器中底层类的「静态代理」,它将 Laravel 内核中定义的「Contracts(在 Laravel 中又 称为服务、契约或者通常我们所说的接口)」,以静态可调用的方式封装到各个「外观」服务中供我们使用。
外观加载原理
在讲解如何使用外观组件之前,我们依旧先去深入分析「外观」组件是如何被 Laravel 加载到项目中的。这一步是 用好「外观」组件的前提。
外观组件配置
所有内置的外观组件的配置数据,同 Laravel 其它服务一样被定义在
config/app.php
文件中。让我们来浏览一下
aliases
节点的配置数据吧:
...
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
...
],
...
外观配置定义格式遵循
「别名」:「外观类」
的数据格式。当一个 HTTP 请求被接收时,将在处理请求阶段将这些「外观」组件加载到服务中。
接下来将深入分析外观服务的加载过程。
加载外观服务
「外观」服务的加载工作由定义在
Illuminate\Foundation\Http\Kernel
内核中的
\Illuminate\Foundation\Bootstrap\RegisterFacades::class
启动程序完成。
引导启动外观服务
如果你已经阅读我的另一篇文章
深入剖析 Laravel 服务提供者实现原理
,你应该对引导程序不会太陌生。
引导程序将在处理 HTTP 请求是完成引导启动
bootstrap()
。所以这里我们需要深入到
RegisterFacades
类的内部去了解更多细节上的处理。
<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\Facades\Facade;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Contracts\Foundation\Application;
class RegisterFacades
{
/**
* Bootstrap the given application. 引导启动服务
*/
public function bootstrap(Application $app)
{
// 清除已解析的「外观」服务实例
Facade::clearResolvedInstances();
// 将 Laravel 服务容器注入到「外观」服务
Facade::setFacadeApplication($app);
// 加载所有外观服务
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
}
加载外观服务有
AliasLoader
组件完成:
-
首先,会从配置文件
config/app.php
中读取所有的「外观」服务配置
aliases
;
-
再从清单文件中读取别名服务
$app->make(PackageManifest::class)->aliases()
;
-
将两个配置数组合并后注入到
AliasLoader
完成
注册(register)
。
注册外观服务
最后我们来瞧瞧
AliasLoader
加载器是如何将所有的「外观」服务加载到系统中的。
<?php
namespace Illuminate\Foundation;
class AliasLoader
{
/**
* Get or create the singleton alias loader instance. 获取或创建「别名加载器」单例实例。
*/
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;
}
/**
* Set the registered aliases. 设置需注册别名数据。
*/
public function setAliases(array $aliases)
{
$this->aliases = $aliases;
}
/**
* Register the loader on the auto-loader stack. 将加载器注册到自动加载中。
*/
public function register()
{
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
/**
* Prepend the load method to the auto-loader stack. 设置自动加载方法。
*/
protected function prependToLoaderStack()
{
// 将 AliasLoader 的 load 方法作为 __autoload 的实现
spl_autoload_register([$this, 'load'], true, true);
}
/**
* Load a class alias if it is registered.从注册过的服务中加载这个「外观」服务。
*/
public function load($alias)
{
if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
$this->loadFacade($alias);
return true;
}
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
}
注意
这里是知识点,在
AliasLoader->register()
完成「外服服务注册」涉及 PHP 两个知识的应用:
-
PHP 内置魔术方法
__autoload
的使用;
-
PHP 如何给类创建别名。
-
外观服务的动态引入
我们知道
__autoload
魔术方法的作用是尝试加载未经定义的类,这样当我们使用一个未经引入的类时,则会自动的给我们引入这个类。
更优的解决方案是通过
spl_autoload_register
函数,将自定义的类加载程序作为
__autoload
的实现,以替代默认
__autoload()
模式函数或方法的行为。
所有
prependToLoaderStack()
方法:
/**
* Prepend the load method to the auto-loader stack. 设置自动加载方法。
*/
protected function prependToLoaderStack()
{
// 将 AliasLoader 的 load 方法作为 __autoload 的实现
spl_autoload_register([$this, 'load'], true, true);
}
就是去完成这样的作用,将
AliasLoader->load()
方法作为自动加载程序的实现,在使用「外观」服务时动态引入这个类。
-
支持外观服务别名
我们已经了解到当「外观」服务被使用时,由
AliasLoader->load()
去自动加载这个类。
与此同时,load 方法通过
class_alias($original, $alias)
函数完成别名注册。
这样,当我们使用
App
类时实际上就是在使用
Illuminate\Support\Facades\App
类。
很完美么,我们的「狗蛋」终于与「世界上最好的语言」画上了等号。你就是我,我就是你。
到这里其实已经完成了「外观」服务工作原理分析工作的
70%
。
探秘 Facade
最后我们将揭开
Facade
的神秘面纱,研究一下 Laravel 是如何实现 Facade 设计模式的。
我们拿
Illuminate\Support\Facades\App
外观服务开刀,去解开类似
App::make()
静态方法使用的奥秘。
深入
Facades\App
:
<?php
namespace Illuminate\Support\Facades;
class App extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor()
{
return 'app';
}
}
我们看到它的实现内部仅仅定义了一个
getFacadeAccessor
方法,该方法的功能是获取已注册组件的名称
app
;除此之外,一无所有。
看来在这里我们得不到什么有用的信息了。继续调查基类
Illuminate\Support\Facades\Facade
。如果你有去通便浏览全部的源码。
<?php
namespace Illuminate\Support\Facades;
use Mockery;
use RuntimeException;
use Mockery\MockInterface;
abstract class Facade
{
/**
* Handle dynamic, static calls to the object.
*/
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
基类并没有定义类似
make
的方法,那么这里能够静态调用
App::make()
看来是需要从
__callStatic
着手才行。
不过在这里我们需要再次厘清一个事实:「外观」模式的功能是什么?
将使用者与子系统从直接耦合,转变成由「外观」类提供统一的接口给使用者使用,以降低客户端与子系统之间的耦合度。
这句话的意思就是我「外观」啥也不提供,就是一层对服务(或者说组件或接口)的封装,然后以统一的方式提供给你们外部调用。
好了现在我们来看看
Facade::__callStatic
是如何获取实际的服务并调用响应的方法的吧。