思考并回答以下问题:
简介
门面为应用服务容器中的绑定类提供了一个「静态」接口。Laravel内置了很多门面,你可能在不知道的情况下正在使用它们。Laravel的门面作为服务容器中底层类的「静态代理」,相比于传统静态方法,在维护时能够提供更加易于测试、更加灵活、简明优雅的语法。
Laravel的所有门面都定义在Illuminate\Support\Facades命名空间下,所以我们可以轻松访问到门面:1
2
3
4
5use Illuminate\Support\Facades\Cache;
Route::get('/cache', function () {
return Cache::get('key');
});
在整个Laravel文档中,很多例子使用了门面来演示框架的各种功能特性。
何时使用门面
门面有诸多优点,其提供了简单、易记的语法,让我们无需记住长长的类名即可使用Laravel提供的功能特性,此外,由于他们对PHP动态方法的独到用法,使得它们很容易测试。
但是,使用门面也有需要注意的地方,一个最主要的危险就是类范围蠕变。由于门面如此好用并且不需要注入,在单个类中使用过多门面,会让类很容易变得越来越大。使用依赖注入则会让此类问题缓解,因为一个巨大的构造函数会让我们很容易判断出类在变大。因此,使用门面的时候要尤其注意类的大小,以便控制其有限职责。
注:构建与Laravel交互的第三方扩展包时,最好注入Laravel契约而不是使用门面,因为扩展包在Laravel之外构建,你将不能访问Laravel的门面测试辅助函数。
门面 vs. 依赖注入
依赖注入的最大优点是可以替换注入类的实现,这在测试时很有用,因为你可以注入一个模拟或存根并且在存根上断言不同的方法。
但是在静态类方法上进行模拟或存根却行不通,不过,由于门面使用了动态方法对服务容器中解析出来的对象方法调用进行了代理,我们也可以像测试注入类实例那样测试门面。例如,给定以下路由:
1 | use Illuminate\Support\Facades\Cache; |
我们可以这样编写测试来验证 Cache::get 方法以我们期望的方式被调用:
1 | use Illuminate\Support\Facades\Cache; |
门面 vs. 辅助函数
除了门面之外,Laravel还内置了许多辅助函数用于执行通用任务,比如生成视图、触发事件、分配任务,以及发送 HTTP 响应等。很多辅助函数提供了和相应门面一样的功能,例如,下面这个门面调用和辅助函数调用是等价的:1
2return View::make('profile');
return view('profile');
门面和辅助函数之间并不存在实质性差别,使用辅助函数的时候,可以像测试相应门面那样测试它们。例如,给定以下路由:1
2
3Route::get('/cache', function () {
return cache('key');
});
在调用底层,cache方法会去调用Cache门面上的get方法,因此,尽管我们使用这个辅助函数,我们还是可以编写如下测试来验证这个方法以我们期望的方式和参数被调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16use Illuminate\Support\Facades\Cache;
/**
* A basic functional test example.
*
* @return void
*/
public function testBasicExample()
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$this->visit('/cache')
->see('value');
}
门面工作原理
在Laravel应用中,门面就是一个为容器中对象提供访问方式的类。该机制原理由Facade类实现。Laravel自带的门面,以及我们创建的自定义门面,都会继承自Illuminate\Support\Facades\Facade基类。
门面类只需要实现一个方法:getFacadeAccessor。正是getFacadeAccessor方法定义了从容器中解析什么,然后Facade基类使用魔术方法__callStatic()从你的门面中调用解析对象。
下面的例子中,我们将会调用Laravel的缓存系统,浏览代码后,也许你会觉得我们调用了Cache 的静态方法get:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Http\Controllers;
use Cache;
use App\Http\Controllers\Controller;
class UserController extends Controller{
/**
* 为指定用户显示属性
*
* @param int $id
* @return Response
*/
public function showProfile($id)
{
$user = Cache::get('user:'.$id);
return view('profile', ['user' => $user]);
}
}
注意我们在顶部位置引入了Cache门面。该门面作为代理访问底层Illuminate\Contracts\Cache\Factory接口的实现。我们对门面的所有调用都会被传递给Laravel缓存服务的底层实例。
如果我们查看Illuminate\Support\Facades\Cache类的源码,将会发现其中并没有静态方法get:1
2
3
4
5
6
7
8
9
10
11class Cache extends Facade
{
/**
* 获取组件注册名称
*
* @return string
*/
protected static function getFacadeAccessor() {
return 'cache';
}
}
Cache门面继承Facade基类并定义了getFacadeAccessor方法,该方法的工作就是返回服务容器绑定类的别名,当用户引用Cache类的任何静态方法时,Laravel从服务容器中解析cache绑定,然后在解析出的对象上调用所有请求方法(本例中是 get)。
实时门面
使用实时门面,可以将应用中的任意类当做门面来使用。为了说明如何使用这个功能,我们先看一个替代方案。例如我们假设Podcast模型有一个publish方法,尽管如此,为了发布博客,我们需要注入Publisher实例:
1 |
|
因为可以模拟注入的发布服务,所以注入发布实现到该方法后允许我们轻松在隔离状态下测试该方法。不过,这要求我们每次调用 publish 方法都要传递一个发布服务实例,使用实时门面,我们可以在维持这种易于测试的前提下不必显式传递Publisher实例。要生成一个实时门面,在导入类前面加上Facades命名空间前缀即可:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App;
use Facades\App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
/**
* Publish the podcast.
*
* @return void
*/
public function publish()
{
$this->update(['publishing' => now()]);
Publisher::publish($this);
}
}
使用实时门面后,发布服务实现将会通过使用Facades前缀后的接口或类名在服务容器中解析。在测试的时候,我们可以使用Laravel自带的门面测试辅助函数来模拟这个方法调用:
1 |
|
Facade
1 | namespace Illuminate\Support\Facades; |
Event
1 |
|