前言

在使用 Laravel 框架进行开发的时候,Facades 是一个经常能用到的模块,比如在使用缓存(Cache)、日志(Log) 等组件的时候。

1
2
3
4
5
6
7
use Illuminate\Support\Facades\Cache;

$name = Cache::get('name');

use use Illuminate\Support\Facades\Log;

Log::info('this is log content');

Facades 的主要优点就是不需要记住各个组件所在目录对应的的命名空间,因为 Illuminate\Support\Facades\ 这一段都是固定的,变化的只是后面的组件名称。

早在刚接触 Laravel 的时候,就对 Facades 充满了疑惑,为什么要这样用,直接用组件真正的命名空间不行吗,代码追踪过去又没有组件实现代码,只有一个静态方法 getFacadeAccessor 返回了组件的名称(cache、log),但是在使用的时候又能调用到组件真正的方法。

1
2
3
4
5
6
7
8
9
10
11
12
class Cache extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cache';
}
}

那么真相只能在父类 Facade 里面了。

但是鉴于平时工作都在搬砖,做一名快乐的 CURD Boy,秉承着实用主义又不是不能用的理念,所以当时并没有去深究代码(实际上是因为太菜了看不懂)。

那为什么现在又开始研究怎么它实现的呢?是我变强了吗?不!是因为需要用上它了…(实用主义万岁)

当时在写一个基于 overtrue/socialite 实现的 her-cat/colourlife-oauth2 扩展包,为了兼容 Laravel 的 Facades 用法,不得不了解一下 Facades 的实现原理(其实仔细看两遍文档就能知道个大概了)。

实现原理

言归正传,就拿 Cache 来举例子,Facades\Cache 的文件内容就在上面,Cache 继承了抽象类 Facade 并实现了 getFacadeAccessor 静态方法,该方法返回了 Cache 组件(就是真正的缓存类)在 Laravel 容器中注册的名称,也就是 cache,这样我们就能从容器中取出 Cache 组件 的实例对象了,整个 Facades 实现也就是从容器中取出实例对象,让实例对象执行被调用的方法。

vendor/laravel/framework/src/Illuminate/Foundation/Application.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Register the core class aliases in the container.
*
* @return void
*/
public function registerCoreContainerAliases()
{
foreach ([
// ...
'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
// ...
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}

当我们运行 Cache::get('name') 的时候,会先触发父类 Facade 的魔术方法 __callStatic,因为 Facade 并没有 get 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 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);
}

$method 是被调用的方法的名称(这个时候的值是 get),$args 就是参数数组(array(1) { [0]=> string(4) "name" }),通过 getFacadeRoot 方法获取到 Cache 组件 的实例对象,然后以 $args 作为参数,调用实例的 get 方法,最后返回方法执行结果。

以上就是方法就是 Facade 的实现原理了,接下来再看看 getFacadeRoot 方法。

1
2
3
4
5
6
7
8
9
/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}

这个方法本身没有什么代码,就是调用了 resolveFacadeInstance 方法,通过组件名称从 Laravel 容器中取出对应的实例对象,这个组件名称就是通过 Facades\Cache 类的 getFacadeAccessor 方法获取的,这里的返回值就是 cache

接下来就到了最后一个方法:resolveFacadeInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Resolve the facade root instance from the container.
*
* @param object|string $name
* @return mixed
*/
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];
}
}

此时 $name 的值是 cache。如果 $name 是一个对象的话,就直接返回 $name$resolvedInstance 是一个用来保存已解析过的实例对象的数组,判断 $name 是否已经从 Laravel 容器中解析过,如果已经解析过就直接返回。

注意:这里是的判断是在这一次请求的生命周期内是否解析过,下次请求进来的时候还是会从 Laravel 容器中取出来。

如果 Laravel 容器如果不是空的话,就通过 $name 从 Laravel 容器中取出 Cache 组件 的实例对象,将 $name 作为 key 存入解析过的实例对象的数组中,并返回。

精简版实现及测试代码

好了,Facades 的实现代码到这里就完了,最后再附上精简版的 Facades 及测试代码。

Facade 抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
namespace App\Core;

abstract class Facade
{
/**
* 用于存放实例对象的数组(实际上在 Laravel 中并不是数组,而是 Application 对象)
* @var array
*/
protected static $app = [];

/**
* 用于保存解析过的实例对象的数组
*
* @var array
*/
protected static $resolvedInstance = [];

/**
* 获取 facade 绑定的实例对象
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}

/**
* 获取组件在容器中注册的名称
*
* @return string
*
* @throws \Exception
*/
protected static function getFacadeAccessor()
{
throw new \Exception('Facade does not implement getFacadeAccessor method.');
}

/**
* 从容器中获取组件的实例对象
*
* @param object|string $name
* @return mixed
*/
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];
}
}

/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \Exception
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();

if (! $instance) {
throw new \Exception('A facade root has not been set.');
}

return $instance->$method(...$args);
}

/**
* 设置组件实例对象(Laravel源码并无该方法,为了演示 Facade 加上的)
*
* @param $name
* @param $obj
*/
public static function setComponentInstance($name, $obj)
{
self::$app[$name] = $obj;
}
}

继承了 Facade 的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace App\Facades;

use App\Core\Facade;

/**
* @method static mixed get($key)
*/
class Cache extends Facade
{
public static function getFacadeAccessor()
{
return 'cache';
}
}

/**
* @method static mixed info($key)
*/
class Log extends Facade
{
public static function getFacadeAccessor()
{
// 这里与上面不一样,返回的是组件的实例
return new \App\Components\Log();
}
}

组件的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace App\Components;

class Cache
{
public function get($key)
{
return "key:{$key}, value: her-cat";
}
}

class Log
{
public function info($content)
{
return "记录的内容:{$content}";
}
}

测试代码:

1
2
3
4
5
6
7
8
9
namespace App\Test;

// 将 Cache 组件的实例对象存入 static::$app 中
\App\Core\Facade::setComponentInstance('cache', new \App\Components\Cache());

echo \App\Facades\Cache::get('name').PHP_EOL;

// Log 组件并未注册到 static::$app 中
echo \App\Facades\Log::info('哈哈哈哈').PHP_EOL;