文章简单介绍了 Facade 门面,总结了其工作原理,并介绍了 Facade 的使用方法。
介绍
Facade 为应用服务容器中绑定的类提供了一个“静态”接口。Laravel 内置了很多 Facade,几乎可以用来访问 Laravel 中所有的服务。Laravel 的 Facade 作为服务容器中底层类的“静态代理”,相比于传统静态方法,Facade 提供了简洁且丰富的语法同时,还带来了更好的可测试性和扩展性。Laravel 的所有 Facade 都定义在 “Illuminate\Support\Facades”命名空间下,我们也可以自己定义新的 Facade。
Facade 加载原理
为了更好的理解 Facade 的工作原理,我们从以下几个方面进行介绍:
- Facade 配置文件
- 加载 RegisterFacades 类
- 注册 Facade 服务
- 解析 Facade 服务
Facade配置文件
Facade 的配置文件保存在 config/app.php 中,主要用来保存所有的 Facade 别名,也即是把 Facade 和别名的对应关系存放在 config/app.php文件中的 aliases 数组里。aliases 数组的形式如下:
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
aliases 数组遵循(别名 => Facade 类)的数据格式,当接受一个 HTTP 请求时注册 aliases 数组,但是并没有实际的进行别名绑定,只是通过 spl_autoload_register() 函数进行了注册。只有在遇到尚未发现的别名时,才去真正绑定别名,因此,别名绑定是“懒惰”加载,并不影响程序的性能。
加载 RegisterFacades 类
Facade 服务的注册工作由“Illuminate\Foundation\Bootstrap\RegisterFacades”类完成,RegisterFacades 类是在启动应用程序的过程中加载的。因此,我们首先来看index.php文件
<?php
//public\index.php
/**
* 记录框架的启动时间
* microtime(get_as_float)函数返回当前Unix时间戳的微妙数
* get_as_float为true时返回浮点数,为false时返回字符串,默认为false
*/
define('LARAVEL_START', microtime(true));
/**
* Composer的自动加载文件
*/
require __DIR__.'/../vendor/autoload.php';
/**
* bootstrap文件里创建了一个Application实例
*/
$app = require_once __DIR__.'/../bootstrap/app.php';
/**
* 通过container服务容器获得一个kernel类的实例,(Illuminate\Contracts\Http\Kernel是一个接口,
* 真正生成的实例是App\Http\Kernel类)
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
/**
* laravel中所有功能服务的注册加载
* 通过调用kernel的handle方法,返回一个response
* Illuminate\Http\Request::capture():通过全局$_SERVER数组构造一个Http请求的语句
*/
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
//把response内容发到浏览器
$response->send();
//执行请求生命周期中的后续操作
$kernel->terminate($request, $response);
RegisterFacades 类的加载是在 $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ) 语句进行的。该语句主要用于 Laravel各个功能服务的注册启动。
$request = Illuminate\Http\Request::capture()
该语句是 Laravel 通过全局 $_SERVER 数组构造一个 HTTP 请求的语句,接下来会调用 HTTP 的内核函数 handle(),RegisterFacades 类的加载即是在内核函数 handle() 中进行。
//Illuminate\Foundation\Http\Kernel.php
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
//允许在表单中使用delete、put等类型的请求
$request->enableHttpMethodParameterOverride();
//将请求发给中间件、路由处理
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}
接下来,我们查看 sendRequestThroughRouter() 函数。
//Illuminate\Foundation\Http\Kernel.php
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
//设置request请求的对象实例
$this->app->instance('request', $request);
//清楚'request'对应的服务对象实例
Facade::clearResolvedInstance('request');
/**
* 依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数,做了几件准备事情:
* 1. 配置加载 LoadConfiguration
* 2.日志配置 ConfigureLogging
* 3.异常处理 HandleException
* 4.注册Facades RegisterFacades
* 5.注册Providers RegisterProviders
* 6.启动Providers BootProviders
*/
$this->bootstrap();
return (new Pipeline($this->app)) //请求的分发
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
函数首先在 Laravel 容器中设置了 request 请求的对象实例,并且清楚了 Facade 中 request 请求对应的服务对象实例。接下来我们来看 bootstrap() 函数
//Illuminate\Foundation\Http\Kernel.php
/**
* Bootstrap the application for HTTP requests.
* 启动引导(reauests驱动)
*
* @return void
*/
public function bootstrap()
{
//判断引导是否已经被启动
if (! $this->app->hasBeenBootstrapped()) {
/**
* 使用Application类的bootstrapWith()函数启动引导
* 参数:bootstrapers数组。
*/
$this->app->bootstrapWith($this->bootstrappers());
}
}
/**
* The bootstrap classes for the application.
* 引导类,起引导作用的类
*
* @var array
*/
protected $bootstrappers = [
//载入服务器环境变量(.env文件)
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
//载入配置信息(config目录)
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
//配置异常处理
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
//注册Facades
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
//注册Providers
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
//启动Providers
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
bootstrap() 函数首先判断引导类是否已经启动,如果没有启动,则启动引导类。引导类数组包含 RegisterFacades 类。启动引导类是调用“Illuminate\Foundation\Application”类中的 bootstrapWith() 函数。
//Illuminate\Foundation\Application
/**
* Run the given array of bootstrap classes.
*
* @param array $bootstrappers
* @return void
*/
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
/**
* 告知将要启动该bootstrapper
* $this['events']:对应绑定的类为 Dispatcher类(Illuminate\Events\Dispatcher)
* [$this]:数组,只有一个Application类元素
*/
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);
//解析每个 $bootstrapper,并调用他们自身的 bootstrap() 函数
$this->make($bootstrapper)->bootstrap($this);
$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}
bootstrapWith() 函数中的 $this->make($bootstrapper)->bootstrap($this) 语句即是解析出 $bootstrappers 数组中的每个类,并调用相应的 bootstrap() 函数。因此,RegisterFacades 类也即是在此时调用。
注册 Facade 服务
下面我们来看 RegisterFacades 类。
//Illuminate\Foundation\Bootstrap\RegisterFacades.php
<?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.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
//清除所有的门面对象服务实例
Facade::clearResolvedInstances();
//设置门面对象的Application实例
Facade::setFacadeApplication($app);
/**
* 默认的别名配置是从 app 配置文件下的 aliases 读取的,
* PackageManifest 是 laravel 5.5 新增的 包自动发现 规则
*/
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
}
可以看出,RegisterFacades 类只有一个 bootstrap() 函数,该函数主要完成以下功能:
- 清楚所有 Facade 对象服务实例
- 设置门面对象的 Application 实例
- 通过 AliasLoader 类为所有的 Facade 注册别名
接下来我们查看 AliasLoader 类如何注册别名。
//llluminate\Foundation\AliasLoader.php
/**
* Get or create the singleton alias loader instance.
* 获取或创建 AliasLoader 单例实例。
*
* @param array $aliases
* @return \Illuminate\Foundation\AliasLoader
*/
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;
}
/**
* Register the loader on the auto-loader stack.
* 将加载器注册到自动加载中
*
* @return void
*/
public function register()
{
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
/**
* Prepend the load method to the auto-loader stack.
* 设置自动加载方法
*
* @return void
*/
protected function prependToLoaderStack()
{
// 把AliasLoader::load()放入自动加载函数队列中,并置于队列头部
spl_autoload_register([$this, 'load'], true, true);
}
AliasLoader 类首先调用 getInstance() 函数获得 AliasLoader 的单例实例,然后调用 register() 函数将 AliasLoader 类中的 load() 函数注册到 SPL __autoload 函数队列的头部。
//llluminate\Foundation\AliasLoader.php
/**
* Load a class alias if it is registered.
*
* @param string $alias
* @return bool|null
*/
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);
}
}
load() 函数的上半部分是 laravel5.4 版本新出的功能,叫做实时门面服务。下半部分是 class_alias() 函数利用别名映射数组将别名映射到真正的门面类中去。例如,我们使用别名类 Cache 时,程序会通过 AliasLoader 类的 load() 函数为“Illuminate\Support\Facades\Cache::class”类创建一个别名 Cache,所以我们在程序里使用别名 Cache 就是使用“Illuminate\Support\Facades\Cache”类。
解析 Facade 服务
以 Cache 为例,我们讲一下 Facade 的使用。我们首先来看一下 Cache Facade 类。
<?php
//Illuminate\Support\Facades\Cache.php
class Cache extends Facade
{
/**
* Get the registered name of the component.
* 获取组件注册名称
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cache';
}
}
该类内部只有一个 getFacadeAccessor() 函数,该方法的功能是获取已经注册组件的名称。其实每个门面类只是重写了基类(Facade)的 getFacadeAccessor() 方法。
//Illuminate\Support\Facades\Facade.php
/**
* 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);
}
当运行 Cache::get() 函数时,门面类 Cache 类里并没有 get() 函数,此时 PHP 会自动调用魔术函数 __callStatic()。魔术函数 __callStatic() 首先获得 Facade 的服务对象实例,然后调用对象的 get() 函数。下面我们来看如何获得 Facade 的对象实例。
//Illuminate\Support\Facades\Facade.php
/**
* Get the root object behind the facade.
* 返回当前门面对应服务对象的实例
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
/**
* Get the registered name of the component.
* 获取组件注册名称,子类重写该函数
*
* @return string
*
* @throws \RuntimeException
*/
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
/**
* Resolve the facade root instance from the container.
* 创建并返回 $name 对应的对象实例
*
* @param string|object $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
//如果是对象,则直接返回
if (is_object($name)) {
return $name;
}
//$resolvedInstance数组中 $name 对应的对象实例是否存在,如果存在,直接返回
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
/**
* 具体的创建工作由Application类对象进行,所以$name需要在Application中进行过绑定
*/
return static::$resolvedInstance[$name] = static::$app[$name];
}
基类中的 getFacadeRoot() 函数首先调用了 getFacadeAccessor() 函数,也即是 Cache 类重写的 getFacadeAccessor() 函数,如果调用的是基类的 getFacadeAccessor() 函数则报错。然后调用 resolveFacadeInstance() 函数获得“cache”对应的服务实例。resolveFacadeInstance() 函数主要是从 Laravel 服务容器 static::$app[$name] 中解析出相应的服务。“cache”服务是应用程序初始化时,在类“Illuminate\Foundation\Bootstrap\RegisterProviders”中被注册到容器中的。
因为在程序启动时,将 Facade 的别名通过 RegisterFacades 类进行了加载注册,所以可以直接使用别名 Cache 代替“Illuminate\Support\Facades\Cache”。
其实,Cache::get('key') 的写法等价于以下写法。
$value = $app->make('cache')->get('key');
建立 Facade
建立自己的 Facade,需要做以下几方面的工作:
- 创建自定义类(Facade 的实现类)
- 创建 Facade 类
- Facade 类别名设置
- 创建 ServiceProvider
- 加载 ServiceProvider
创建自定义类(Facade 的实现类)
我们现在 app\Facades 目录下创建 PaymentGateway\Payment 类。
<?php
//app\Facades\\PaymentGateway\Payment.php
namespace App\Facades\PaymentGateway;
class Payment
{
public function get(){
return "Facade Test ...";
}
}
创建 Facade 类
创建自定义的 Facade 类,该类继承基类 Facade,并重写基类的 getFacadeAccessor() 函数。
<?php
//app\Facades\\PaymentGateway\Facade\Payment.php
namespace App\Facades\PaymentGateway\Facade;
use Illuminate\Support\Facades\Facade;
class Payment extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'payment';
}
}
Facade 类别名设置
我们可以为自定义的 Facade 类起个别名,只需要将别名和类的对应关系添加到 config/app.php 配置文件中的 aliases 数组即可。
//config/app.php
'aliases' => [
...
'Payment' => App\Facades\PaymentGateway\Facade\Payment::class,
...
],
创建 ServiceProvider
在 app\Providers 文件夹下创建 PaymentServiceProvider,提供“payment”服务。
<?php
//app\Providers\PaymentServiceProvider.php
namespace App\Providers;
use App\Facades\PaymentGateway\Payment;
use Illuminate\Support\ServiceProvider;
class PaymentServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
* $defer为true,表示延迟加载此类
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton('payment', function ($app) {
return new Payment();
});
}
/**
* 设置了延迟启动,需要重写 providers 函数
* 该方法返回provider中注册的服务容器绑定别名
*/
public function provides()
{
return ["payment"];
}
}
其中,$defer 设置为 true,是为了提供延迟加载功能,provides() 函数也是为了辅助延迟加载功能而重写的。register() 函数中的服务名“payment”应该与 Facade 类中 getFacadeAccessor() 函数返回值保持一致。
加载 ServiceProvider
为了能加载我们创建的 PaymentServiceProvider,需要在 config/app.php 配置文件中 providers 数组内添加 PaymentServiceProvider。
//config/app.php
'providers' => [
...
App\Providers\PaymentServiceProvider::class,
...
],
最后,使用如下代码即可以调用 Facade 类。
$value = Payment::get();
总结
之所以能够简单的使用 Cache::get() 操作,是因为程序启动时自动进行了别名注册,使用 Cache 等价于“Illuminate\Support\Facades\Cache”。另外,程序在基类 Facade 中自动调用魔术函数 __callStatic() 进行了动态绑定。
门面类列表
下面列出了每个 Facade 、对应的底层类以及服务容器绑定键。