今天我们将学习 Laravel 核心架构中的另一个主题Facade(外观).
本文将从以下几个方面出发, 全面讲解 Laravel 中 Facade 的运行原理, 为了便于理解后续中所有 Facade 译作外观:
简单介绍外观设计模式;
Laravel外观的加载原理;
Laravel外观基本使用.
什么是外观设计模式
外观模式定义
为子系统中的一组接口提供一个统一的入口. 外观模式定义了一个高层接口, 这个接口使得这一子系统更加容易使用.
外观模式是一种使用频率非常高的结构型设计模式, 它通过引入一个外观角色来简化客户端与子系统之间的交互, 为复杂的子系统调用提供一个统一的入口, 降低子系统与客户端的耦合度, 且客户端调用非常方便. - 设计模式 Java 版 https://quanke.gitbooks.io/design-pattern-java/
核心 就是在 客户端(使用者) 与 子系统(接口或服务) 之间引入一个外观角色.
将使用者与子系统从直接耦合, 转变成由外观类提供统一的接口给使用者使用, 以降低客户端与子系统之间的耦合度.
结构示意图:
关于外观模式可以阅读 设计模式 Java 版 - 外观模式 https://quanke.gitbooks.io/design-pattern-java/外观模式-Facade Pattern.html
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 服务提供者实现原理 https://laravel-china.org/articles/12986/in-depth-analysis-of-the-principle-of-laravel-service-provider-implementation , 你应该对引导程序不会太陌生.
引导程序将在处理 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 http://php.net/manual/zh/function.autoload.php 魔术方法的作用是尝试加载未经定义的类, 这样当我们使用一个未经引入的类时, 则会自动的给我们引入这个类.
更优的解决方案是通过 spl_autoload_register http://php.net/manual/zh/function.spl-autoload-register.php 函数, 将自定义的类加载程序作为 __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 是如何获取实际的服务并调用响应的方法的吧.
首先, 通过 getFacadeRoot 静态方法获取实际服务的实例对象;
然后, 调用实例对象的相关方法并返回处理结果.
- <?php
- namespace Illuminate\Support\Facades;
- use Mockery;
- use RuntimeException;
- use Mockery\MockInterface;
- abstract class Facade
- {
- /**
- * Get the root object behind the facade. 从 facade 中解析出真实服务的对象
- */
- public static function getFacadeRoot()
- {
- return static::resolveFacadeInstance(static::getFacadeAccessor());
- }
- /**
- * Resolve the facade root instance from the container.me 从 Laravel 服务容器中解析出真实服务的对象
- */
- protected static function resolveFacadeInstance($name)
- {
- if (is_object($name)) {
- return $name;
- }
- if (isset(static::$resolvedInstance[$name])) {
- return static::$resolvedInstance[$name];
- }
- return static::$resolvedInstance[$name] = static::$app[$name];
- }
- }
从 getFacadeRoot 解析对象的功能中我们可以看到: 它会调用实现外观的 getFacadeAccessor 方法获取到组件 (服务或者说接口) 的名称; 然后从 Laravel 服务容器 static::$app[$name](app 是在 RegisterFacades 中注册到外观中) 中解析出相关服务.
到这里, 我们就将外观服务的基本工作原理给分析透彻了.
另外有关外观组件的一些细枝末节, 如:
在文档Facades Vs. 辅助函数 https://laravel-china.org/docs/laravel/5.6/facades/1361#d45649 一节提到的测试验证是如何实现的 Cache::shouldReceive('get');
什么是实时 Facades https://laravel-china.org/docs/laravel/5.6/facades/1361#7e43fe .
还是需要你自行深入到 Facade 基类去一探究竟.
扫盲 ArrayAccess 接口
另外补充一个知识点就是关于 static::$app[$name] 这一句代码. 你不经要问, 这有啥好补充的呢, 不就是一个简单获取数据么.
获取数据不假, 简单也不假.
不过你仔细看一下, 你会发现 static::$app 静态成员变量难道不是一个 \Illuminate\Contracts\Foundation\Application 实现实例么, 怎么可以从对象中以数组的方式获取值呢?
这是因为我们的服务容器 Illuminate\Container\Container https://github.com/laravel/framework/blob/5.6/src/Illuminate/Container/Container.php 实现了 ArrayAccess http://php.net/manual/zh/class.arrayaccess.php 接口.
该接口的功能是提供像访问数组一样访问对象的能力的接口, 这样就可以像数组一样访问对象访问成员.
- /**
- *@link https://github.com/laravel/framework/blob/5.6/src/Illuminate/Container/Container.php
- */
- class Container implements ArrayAccess, ContainerContract
- {
- /**
- * Get the value at a given offset. 获取一个偏移位置的值, 实际上从容器中解析出服务.
- */
- public function offsetGet($key)
- {
- return $this->make($key);
- }
- }
Laravel外观基本使用
外观服务的一个典型使用场景是在定义路由时使用 Route::get('/', ...). 这样一看似乎Laravel 别名服务也就不这么神秘了.
来源: https://juejin.im/entry/5b3c419a5188251b3c3b029a