标题:【Laravel系列6.5】门面模式
门面模式
在之前我们的设计模式相关的系列文章中,已经学习过了门面模式。在设计模式中,门面模式的定义是:为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。当时我们也实现了自己的设计模式,不记得的小伙伴欢迎移步 PHP设计模式-门面模式 https://mp.weixin.qq.com/s/RzCoM96XnlT610q4AiuAVA 再复习复习。
Laravel 中的门面
虽然实现可能不太一样,但在 Laravel 中的门面总体上还是遵循着门面模式的基本思想的。Laravel 中的门面是为应用的服务容器提供一个【静态】接口,相当于是服务容器底层类中的一个【静态代表】,能够提供更加灵活、易于测试、优雅的语法。
对于 Laravel 中的门面来说,我们会经常使用到,比如说缓存。
Cache::get('key');
再比如我们之前经常用的数据库和 Redis 。
DB::connection('mysql2')->table('db_test')->get()->toArray();
Redis::connection('default')->client()->get('test')
发现没有,门面全是用的静态方法。但是你点过去,会发现这个门面类里面什么东西都没有呀!
class Cache extends Facade
{
protected static function getFacadeAccessor()
{
return 'cache';
}
}
神器吗?静态方法在哪里呀?怎么就有 get() 、set() 那些方法了?别急,我们去它的父类 vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php 中看看,玄机说不定就在这里哦。
在 Facade 类中,别的方法函数我们先不用看,直接拉到最底下,你会发现一个魔术方法,__callStatic() 。如果从最开始你就跟我一起学过 PHP中的那些魔术方法(一)https://mp.weixin.qq.com/s/QXCH0ZttxhuEBLQWrjB2_A 的话,那么这个方法你一定不会陌生。剩下的,还需要我多说吗?
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);
}
__callStatic() 的意思是通过静态调用时如果没有定义对应的方法,就进入到 __callStatic() 方法中,比如我们调用的 Cache::get() 这个方法,实际上当前的 Cache 门面类以及它的父类 Facade 都没有定义这个方法,那么就直接进入到了 __callStatic() 中。接着,它就通过 getFacadeRoot() 获取我们当前门面的实例对象,然后调用实例对象中的 get() 方法。
好了,到此为止,其实如果面试的时候有面试官问你 Laravel 中的门面模式是如何实现的时候,你就可以自信地说核心就是这个 __callStatic() 魔术方法了。那么这个具体的实例对象又是从哪里来的呢?我们继续往下看。
实例对象
接下来我们看看 Facade 中的具体实例对象是怎么获取的。这里我们又要回到服务容器中。不过还是先从门面入口来看看吧。
在 __callStatic() 方法中,我们会看到调用了一个 static::getFacadeRoot() 方法来获得具体的实例对象。
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
这个方法的内容很简单,就是调用了另外两个方法,注意 getFacadeAccessor() 是我们的各个门面子类中实现的,比如例子中就是在 Cache 这个类中实现的。它只是返回一个实例的别名,还记得这个别名是在哪里定义的吗?我们在服务容器中看到过,就是 vendor/laravel/framework/src/Illuminate/Foundation/Application.php 中 registerCoreContainerAliases() 方法里面定义的那些。
接下来,我们主要看的就是 static::resolveFacadeInstance() 这个方法。从名字我们可以出,它的意思是 解决门面实例 ,这货要是不返回一个实例对象那还真对不起它的名字了。
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
第一个判断,如果传递进来的是一个对象,直接返回。第二个判断,如果当前实例数组中已经有了,就不再创建了,类似于一个 享元模式 的效果。注意,静态的成员数组哦!什么意思呢?静态的全局共享的,也就是说,你这个实例对象创建之后,其他地方都可以使用,完全的单例状态。最后一个判断,app 也就是我们的服务容器存在的话,进行服务容器的操作。
我们先来看下这个 app 属性是什么时候赋值的。在讲服务提供者时,Kernel 中有一个 bootstrappers 属性数组,其中有一个 RegisterFacades 提供者。很明显,它是用于注册门面的一个服务提供者,在这个服务提供者中,我们会看到这样的代码。
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
其中的 Facade::setFacadeApplication() 就是将 服务容器 的 Application 对象注入到了门面类的静态成员变量 app 中。注意,同样是静态的,全局存在的。
然后我们继续回到 resolveFacadeInstance() 方法中。
protected static function resolveFacadeInstance($name)
{
// …………
// …………
if (static::$app) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
这里怎么回事,怎么就通过 static::$app[$name] 就能获得一个实例对象了呢?别激动,别着急,想想怎么让一个对象可以进行这样的数组操作?我们之前学过的哦!
好了,不卖关子了,如果你之前没有和我一起学习过,没有看过之前的文章视频的话,那么可以移步 PHP怎么遍历对象?https://mp.weixin.qq.com/s/cFMI0PZk2Zi4_O0FlZhdNg 以及 PHP的SPL扩展库(二)对象数组与数组迭代器https://mp.weixin.qq.com/s/T2dgXtDY8rVOImV3vutWmw 中复习一下。就是这个 ArrayAccess 接口,它必须实现的那几个方法可以让对象像数组一样去使用。
OK,知道原理了,我们来看看是不是这样,找到 Application 的父类 vendor/laravel/framework/src/Illuminate/Container/Container.php 。
class Container implements ArrayAccess, ContainerContract
{
// …………
// …………
public function offsetGet($key)
{
return $this->make($key);
}
// …………
// …………
}
真像大白了吧?不再需要我继续多解释了吧?关于 make() 方法在之前的服务容器中已经讲解过了哦。
好了,剩下的内容交给你了,请根据 vendor/laravel/framework/src/Illuminate/Foundation/Application.php 中 registerCoreContainerAliases() 方法中的别名找到 Cache 的具体实现类,然后分析它的 get()、set()、forget() 等方法的实现,看看它们是怎么根据我们的配置文件来使用不同的缓存存储方案的。
自定义门面
既然门面这么好用,那么能不能像服务容器和管道一样,我们自己来创建一个门面实现呢?都这么说了,当然是没问题的啦。而且门面的实现其实非常简单方便。
Route::get('facades/test', function(){
// 实时 Facades
Facades\App\Facades\ShowEmail::show();
// 直接实例
\App\Facades\ShowTel::show();
// 别名
\App\Facades\ShowWebSite::show();
});
在这里,我们使用了三种门面实现的方式。不过从上面的测试代码来看,你是看不出什么区别的,都是简单地调用了一个对象的静态方法。那么我们就来一条一条深入地看一下。
实时 Facades
第一个,注意它的命名空间。我们的 ShowEmail 类其实是定义在 app/Facades 这个目录下的,也就是说,它的命名空间是 \App\Facdes ,但是为什么我们给它的前面又加了一个 Facades 呢?其实这就是 Laravel 提供的 实时门面 的用法。
class ShowEmail
{
public function show(){
echo 'xxxx@xxx.com';
}
}
ShowEmail 就是一个普通的类,里面的 show() 方法也是一个普通的成员方法,但我们使用的时候,只需要给命名空间前面加上 Facades 前缀,框架就可以以门面的方式来调用这个类。是不是很方便。
继承门面
另外两个都是继承门面 Facade 基类的实现。
class ShowTel extends \Illuminate\Support\Facades\Facade
{
protected static function getFacadeAccessor()
{
return new ShowTelImplement();
}
}
class ShowWebSite extends \Illuminate\Support\Facades\Facade
{
protected static function getFacadeAccessor()
{
return 'showWebSite';
}
}
继承了 \Illuminate\Support\Facades\Facade 基类之后,我们只需要实现静态的 getFacadeAccessor() 方法,就可以实现门面的使用了。不过在这里要注意的是,ShowTel 类返回的是直接实例化之后的内容,而 ShowWebSite 则返回的是别名。
直接实例化的方式不用多说了,主要是这个别名,是在哪里定义的?vendor/laravel/framework/src/Illuminate/Foundation/Application.php 中 registerCoreContainerAliases() 方法是在框架底层的 Composer 源码中,修改它可不太好。不用着急,Laravel 早为我们考虑好了。去 config/app.php 中看看吧,除了 服务提供者 的那个数组之外,还有一个 aliases 数组,我们在这里定义就好了。
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
// …………
// …………
'showWebSite' => \App\Facades\ShowWebSiteImplement::class,
],
可以看到这里也定义了很多其它的系统默认的别名配置。反正你记得,以后自己添加的内容放在这里就可以了,和自定义的服务提供者一样。
总结
Laravel 中的门面是不是非常有意思?其实它还有一个重要的功能就是解决了静态类静态方法不好测试的问题,大家可以在官方文档契约相关的内容中查看详细的内容。在这里我们就不讲契约方面的内容了,其实本质上就是服务容器和门面要解决的依赖控制的问题。
至此,我们也就完成了 Laravel 核心内容的学习。服务容器、管道(中间件)、门面共同组成了这个复杂但优雅的框架。现在面试的时候相信你一定能够在 Laravel 框架的回答部分获得不错的成绩。同时,TP5 以后,Yii2 中也都有服务容器和中间件之类内容的存在,原理也都是类似的,你也一定可以举一反三。不过学习路途还远没有结束,后面我们还将学习到框架中的一些其他好玩的功能,像是事件、日志、测试之类的内容,加油吧,少年们!
参考文档:
https://learnku.com/docs/laravel/8.x/facades/9363