Laravel-Facade

思考并回答以下问题:

简介

门面为应用服务容器中的绑定类提供了一个「静态」接口。Laravel内置了很多门面,你可能在不知道的情况下正在使用它们。Laravel的门面作为服务容器中底层类的「静态代理」,相比于传统静态方法,在维护时能够提供更加易于测试、更加灵活、简明优雅的语法。

Laravel的所有门面都定义在Illuminate\Support\Facades命名空间下,所以我们可以轻松访问到门面:

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

Route::get('/cache', function () {
return Cache::get('key');
});

在整个Laravel文档中,很多例子使用了门面来演示框架的各种功能特性。

何时使用门面

门面有诸多优点,其提供了简单、易记的语法,让我们无需记住长长的类名即可使用Laravel提供的功能特性,此外,由于他们对PHP动态方法的独到用法,使得它们很容易测试。

但是,使用门面也有需要注意的地方,一个最主要的危险就是类范围蠕变。由于门面如此好用并且不需要注入,在单个类中使用过多门面,会让类很容易变得越来越大。使用依赖注入则会让此类问题缓解,因为一个巨大的构造函数会让我们很容易判断出类在变大。因此,使用门面的时候要尤其注意类的大小,以便控制其有限职责。

注:构建与Laravel交互的第三方扩展包时,最好注入Laravel契约而不是使用门面,因为扩展包在Laravel之外构建,你将不能访问Laravel的门面测试辅助函数。

门面 vs. 依赖注入

依赖注入的最大优点是可以替换注入类的实现,这在测试时很有用,因为你可以注入一个模拟或存根并且在存根上断言不同的方法。

但是在静态类方法上进行模拟或存根却行不通,不过,由于门面使用了动态方法对服务容器中解析出来的对象方法调用进行了代理,我们也可以像测试注入类实例那样测试门面。例如,给定以下路由:

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

Route::get('/cache', function () {
return Cache::get('key');
});

我们可以这样编写测试来验证 Cache::get 方法以我们期望的方式被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use 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');
}

门面 vs. 辅助函数

除了门面之外,Laravel还内置了许多辅助函数用于执行通用任务,比如生成视图、触发事件、分配任务,以及发送 HTTP 响应等。很多辅助函数提供了和相应门面一样的功能,例如,下面这个门面调用和辅助函数调用是等价的:

1
2
return View::make('profile');
return view('profile');

门面和辅助函数之间并不存在实质性差别,使用辅助函数的时候,可以像测试相应门面那样测试它们。例如,给定以下路由:

1
2
3
Route::get('/cache', function () {
return cache('key');
});

在调用底层,cache方法会去调用Cache门面上的get方法,因此,尽管我们使用这个辅助函数,我们还是可以编写如下测试来验证这个方法以我们期望的方式和参数被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use 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
<?php

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
11
class Cache extends Facade
{
/**
* 获取组件注册名称
*
* @return string
*/
protected static function getFacadeAccessor() {
return 'cache';
}
}

Cache门面继承Facade基类并定义了getFacadeAccessor方法,该方法的工作就是返回服务容器绑定类的别名,当用户引用Cache类的任何静态方法时,Laravel从服务容器中解析cache绑定,然后在解析出的对象上调用所有请求方法(本例中是 get)。

实时门面

使用实时门面,可以将应用中的任意类当做门面来使用。为了说明如何使用这个功能,我们先看一个替代方案。例如我们假设Podcast模型有一个publish方法,尽管如此,为了发布博客,我们需要注入Publisher实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App;

use App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;

class Podcast extends Model
{
/**
* Publish the podcast.
*
* @param Publisher $publisher
* @return void
*/
public function publish(Publisher $publisher)
{
$this->update(['publishing' => now()]);

$publisher->publish($this);
}
}

因为可以模拟注入的发布服务,所以注入发布实现到该方法后允许我们轻松在隔离状态下测试该方法。不过,这要求我们每次调用 publish 方法都要传递一个发布服务实例,使用实时门面,我们可以在维持这种易于测试的前提下不必显式传递Publisher实例。要生成一个实时门面,在导入类前面加上Facades命名空间前缀即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

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
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
<?php

namespace Tests\Feature;

use App\Podcast;
use Tests\TestCase;
use Facades\App\Contracts\Publisher;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PodcastTest extends TestCase
{
use RefreshDatabase;

/**
* A test example.
*
* @return void
*/
public function test_podcast_can_be_published()
{
$podcast = factory(Podcast::class)->create();

Publisher::shouldReceive('publish')->once()->with($podcast);

$podcast->publish();
}
}

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
namespace Illuminate\Support\Facades;

use Closure;
use Mockery;
use Mockery\MockInterface;
use RuntimeException;

abstract class Facade
{
/**
* The application instance being facaded.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected static $app; // 两个函数有这个有关

/**
* The resolved object instances.
*
* @var array
*/
protected static $resolvedInstance; // 重点在维护这个数组,有两个清除函数

public static function resolved(Closure $callback)
{
$accessor = static::getFacadeAccessor();

if (static::$app->resolved($accessor) === true) {
$callback(static::getFacadeRoot());
}
}

/**
* Get the registered name of the component.
*
* @return string
*
* @throws \RuntimeException
*/
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}

/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}

/**
* 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];
}
}

/**
* Clear a resolved facade instance.
*
* @param string $name
* @return void
*/
public static function clearResolvedInstance($name)
{
unset(static::$resolvedInstance[$name]);
}

/**
* Clear all of the resolved instances.
*
* @return void
*/
public static function clearResolvedInstances()
{
static::$resolvedInstance = [];
}

/**
* Get the application instance behind the facade.
*
* @return \Illuminate\Contracts\Foundation\Application
*/
public static function getFacadeApplication()
{
return static::$app;
}

/**
* Set the application instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public static function setFacadeApplication($app)
{
static::$app = $app;
}

/**
* 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);
}
}

Event

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
<?php

namespace Illuminate\Support\Facades;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Testing\Fakes\EventFake;

/**
* @method static void listen(string|array $events, \Closure|string $listener)
* @method static bool hasListeners(string $eventName)
* @method static void push(string $event, array $payload = [])
* @method static void flush(string $event)
* @method static void subscribe(object|string $subscriber)
* @method static array|null until(string|object $event, mixed $payload = [])
* @method static array|null dispatch(string|object $event, mixed $payload = [], bool $halt = false)
* @method static array getListeners(string $eventName)
* @method static \Closure makeListener(\Closure|string $listener, bool $wildcard = false)
* @method static \Closure createClassListener(string $listener, bool $wildcard = false)
* @method static void forget(string $event)
* @method static void forgetPushed()
* @method static \Illuminate\Events\Dispatcher setQueueResolver(callable $resolver)
* @method static void assertDispatched(string $event, callable|int $callback = null)
* @method static void assertDispatchedTimes(string $event, int $times = 1)
* @method static void assertNotDispatched(string $event, callable|int $callback = null)
*
* @see \Illuminate\Events\Dispatcher
*/
class Event extends Facade
{
/**
* Replace the bound instance with a fake.
*
* @param array|string $eventsToFake
* @return \Illuminate\Support\Testing\Fakes\EventFake
*/
public static function fake($eventsToFake = [])
{
static::swap($fake = new EventFake(static::getFacadeRoot(), $eventsToFake));

Model::setEventDispatcher($fake);
Cache::refreshEventDispatcher();

return $fake;
}

/**
* Replace the bound instance with a fake during the given callable's execution.
*
* @param callable $callable
* @param array $eventsToFake
* @return callable
*/
public static function fakeFor(callable $callable, array $eventsToFake = [])
{
$originalDispatcher = static::getFacadeRoot();

static::fake($eventsToFake);

return tap($callable(), function () use ($originalDispatcher) {
static::swap($originalDispatcher);

Model::setEventDispatcher($originalDispatcher);
Cache::refreshEventDispatcher();
});
}

/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'events';
}
}
0%