在使用Laravel 框架的时候会看到很多 Cache::get() 这样的用法,称之为 Facade,门面。
但是代码中即没有看到使用 Cache 相关的命名空间,且在 Composer 自动加载中也没有相关的自动加载规则。那这是如何实现的呢?让我们从框架源码去发现。
Laravel 的入口文件是 public/index.php,此文件载入了 autoload.php, app.php 2个文件:
require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
顾名思义 autoload.php 实现了自动加载,app.php 和容器相关。
初始化容器的过程这里不详细解说,不是本文重点。
==============================重点来了================================
初始化容器后,执行了以下代码:
// 得到 App\Http\Kernel 实例对象
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// 执行对象handle 方法,此方法继承自 Illuminate\Foundation\Http\Kernel
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
让我们去看下 handle() 做了些什么:
public function handle($request)
{
//......省略......
$response = $this->sendRequestThroughRouter($request);
//......省略......
}
sendRequestThroughRouter 方法:
protected function sendRequestThroughRouter($request)
{
//......省略......
// 启动一些启动器,诸如异常处理,配置,日志,Facade,运行环境监测等
$this->bootstrap();
//......省略......
}
bootstrap 方法:
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
$this->bootstrappers() 中返回 $this->bootstrappers 保存的数据:
protected $bootstrappers = [
'Illuminate\Foundation\Bootstrap\DetectEnvironment',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
'Illuminate\Foundation\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\RegisterProviders',
'Illuminate\Foundation\Bootstrap\BootProviders',
];
可以看到 :
'Illuminate\Foundation\Bootstrap\RegisterFacades',
此类就是实现 Facade 的一部分,bootstrap 方法中 $this->app->bootstrapWith 会调用类的 bootstrap 方法:
class RegisterFacades
{
public function bootstrap(Application $app)
{
//......省略......
AliasLoader::getInstance($app->make('config')->get('app.aliases'))->register();
}
}
$app->make('config')->get('app.aliases') 返回的是 config/app.php 配置文件中 'aliases' 键对应的值,
我们继续往下看 AliasLoader::getInstance 方法:
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;
}
默认情况下 static::$instance 是 null,所以我们会得到 new static($aliases)。
new static 是一个自 5.3以后的新特性,参见:https://secure.php.net/manual/zh/language.oop5.late-static-bindings.php
回头再看
AliasLoader::getInstance($app->make('config')->get('app.aliases'))->register();
中调用了 AliasLoader->register 方法:
public function register()
{
if (!$this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
prependToLoaderStack 方法:
这里注册了当前对象中 load 方法为自动加载函数
protected function prependToLoaderStack()
{
spl_autoload_register([$this, 'load'], true, true);
}
load 方法:
public function load($alias)
{
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
这里的 $this->aliases 即是 AliasLoader:getInstance 中实例化一个对象: new static($aliases) 时构造函数中设置的:
private function __construct($aliases)
{
$this->aliases = $aliases;
}
这里 class_alias 是实现 Facade 的核心要点之一,该函数原型:
bool class_alias ( string $original, string $alias[, bool $autoload = TRUE ] )
第三个参数默认为 true,意味着如果原始类(string $original)没有加载,则自动加载。
更多该函数的解释请自行翻阅手册。
现在可以解答为什么 Cache 没有使用命名空间,composer 没有设置自动加载的情况可以立即使用了:
自动加载函数会尝试加载 Cache 这个别名对应的实际类所在的文件(加载这个类文件时会去调用SPL自动加载函数队列中其他函数,比如 composer 自动加载函数,并遵从规则)
接下来说一下 Cache::get() 中为什么可以像静态方法一样调用任何类的方法(哪怕不是静态方法)。
找到并打开 Cache 类文件:vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php
文件内容是这样的:
namespace Illuminate\Support\Facades;
/**
* @see \Illuminate\Cache\CacheManager
* @see \Illuminate\Cache\Repository
*/
class Cache extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cache';
}
}
看一下 父类 Illuminate\Support\Facades,发现父类中实现了魔术方法 __callStatic:
public static function __callStatic($method, $args)
{
$instance = static ::getFacadeRoot();
if (!$instance) {
throw new RuntimeException('A facade root has not been set.');
}
switch (count($args)) {
case 0:
return $instance->$method();
case 1:
return $instance->$method($args[0]);
case 2:
return $instance->$method($args[0], $args[1]);
case 3:
return $instance->$method($args[0], $args[1], $args[2]);
case 4:
return $instance->$method($args[0], $args[1], $args[2], $args[3]);
default:
return call_user_func_array([$instance, $method], $args);
}
}
谜底揭开了,原来是通过魔术方法去实现的。
简单的写了一段代码,用于实现类似 Facade 的调用方式:
namespace Illuminate\Support\Facades {
class Facades {
public function __call($name, $params) {
return call_user_func_array([$this, $name], $params);
}
public static function __callStatic($name, $params) {
return call_user_func_array([new static(), $name], $params);
}
}
class Cache extends Facades {
protected function fn($a, $b) {
echo "function parameters: ${a} and ${b}<br>";
}
protected function static_fn($a, $b) {
echo "static function parameters: ${a} and ${b}<br>";
}
}
}
namespace {
class Autoload {
public $aliases;
public function __construct($aliases = []) {
$this->aliases = $aliases;
}
public function register() {
spl_autoload_register([$this, 'load'], true, true);
return $this;
}
public function load($alias) {
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
}
$aliases = [
'Cache' => Illuminate\Support\Facades\Cache::class,
];
$autoloader = (new Autoload($aliases))->register();
Cache::fn(3,6);
Cache::static_fn(4,7);
}
======================分割线======================
其实 Facade 这个名字还有深层次的体现,不但可以理解为“门面”,也可以解释为“伪装”。
Cache 的背后不一定真的是 Cache,打开门面,撕开伪装才能看到真相。
在我对 laravel 5.1.31 LTS 源码注释中可以一窥究竟: