Facades

简介

Facade 为应用的 服务容器 中可用的类提供了一个「静态」接口。Laravel 自带了很多 Facade,可以使用 Laravel 几乎所有的功能。Laravel Facade 充当了服务容器中底层类的「静态代理」,提供了简洁而直观的语法,同时比传统的静态方法更有可测试性和灵活性。

所有的 Laravel Facade 都在 Illuminate\Support\Facades 命名空间中定义。因此,我们可以像这样轻松使用 Facade:

use Illuminate\Support\Facades\Cache;

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

在整个 Laravel 的文档中,很多示例代码都会使用 Facade 来演示框架的各种功能。

什么时候使用 Facade

Facade 有很多好处,它为使用 Laravel 的功能提供了简单、易记的语法,而无须在手动注入或配置时记住长长的类名。此外,由于它们使用了 PHP 的动态方法,使得测试起来也很容易。

然而,在使用 Facade 时也有一些要注意的。使用 Facade 最主要的风险是会引起类作用范围的膨胀。由于 Facade 便于使用而且不需要注入,很容易在单个类中使用许多 Facade,从而导致类变得越来越大。而使用依赖注入时,使用的类越多构造函数就越长,在视觉上就会引起注意,提醒您要注意类有点庞大了。因此在使用 Facade 的时候,要特别注意控制好类的大小,让类的作用范围保持短小。

在开发与 Laravel 交互的第三方扩展包时,最好选择注入 Laravel Contract,而不是使用 Facade。由于扩展包是在 Laravel 本身之外构建的,因此您无法使用 Laravel Facade 测试辅助函数。

Facade Vs. 依赖注入

依赖注入的一个主要好处是能够切换注入类的实现。这在测试时很有用,因为您可以注入一个「模拟实现」或者「桩实现」,并断言在「桩」上调用的各种方法。

通常,真正的静态方法是不可能被「模拟实现」或者「桩实现」的。但是,由于 Facade 使用动态方法来代理服务容器所解析对象的方法调用,我们可以像测试注入类的实例一样来测试 Facade。例如,假设有如下路由:

use Illuminate\Support\Facades\Cache;

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

我们可以编写以下测试来验证是否使用预期的参数调用了 Cache::get 方法:

use Illuminate\Support\Facades\Cache;

/**
 * 一个基本的测试示例
 *
 * @return void
 */
public function testBasicExample()
{
    Cache::shouldReceive('get')
         ->with('key')
         ->andReturn('value');

    $this->visit('/cache')
         ->see('value');
}

Facade Vs. 辅助函数

除了 Facade,Laravel 还包含各种「辅助函数」来实现一些常用的功能,比如生成视图、触发事件、分发任务或者发送 HTTP 响应。许多辅助函数都有和对应的 Facade 相同的功能。例如,调用下面的 Facade 和调用辅助函数效果是一样的:

return View::make('profile');

return view('profile');

Facade 和辅助函数没有任何实质上的区别。使用辅助函数时,您仍然可以像使用对应的 Facade 一样对它们进行测试。例如,假设有如下路由:

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

在底层代码中,辅助函数 cache 调用了 Cache Facade 中的 get 方法。因此,即使使用的是辅助函数,我们依然可以编写以下测试来验证是否使用我们预期的参数调用了该方法:

use Illuminate\Support\Facades\Cache;

/**
 * 一个基本的测试示例
 *
 * @return void
 */
public function testBasicExample()
{
    Cache::shouldReceive('get')
         ->with('key')
         ->andReturn('value');

    $this->visit('/cache')
         ->see('value');
}

Facade 是如何工作的

在 Laravel 应用中,Facade 是一个访问容器中对象的类。其核心部分就是 Facade 类。不管是 Laravel 的 Facade,还是任何自定义的 Facade,都继承自 Illuminate\Support\Facades\Facade 类。

Facade 基类利用 __callStatic() 魔术方法在从容器中解析出对象后再执行 Facade 调用。咋一看下面的代码,可能认为调用了 Cache 类的静态方法 get

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    /**
     * 显示给定用户的配置信息
     *
     * @param  int  $id
     * @return Response
     */
    public function showProfile($id)
    {
        $user = Cache::get('user:'.$id);

        return view('profile', ['user' => $user]);
    }
}

需要注意的是,在文件顶部我们「引入」了 Cache Facade 。此 Facade 是访问 Illuminate\Contracts\Cache\Factory 接口底层实现的代理。我们对 Facade 的任何调用都将传递给 Laravel 缓存服务的底层实例。

如果我们查看 Illuminate\Support\Facades\Cache 类,会发现类中根本没有 get 静态方法:

class Cache extends Facade
{
    /**
     * 获取组件注册的名称
     *
     * @return string
     */
    protected static function getFacadeAccessor() { return 'cache'; }
}

而是,Cache Facade 继承了 Facade 基类,并定义了 getFacadeAccessor() 方法。该方法的作用是返回服务容器绑定的名称。当用户调用 Cache Facade 中的任何静态方法时,Laravel 会从 服务容器 中解析出 cache 绑定,并对该对象运行所请求的方法(在本例中,是 get 方法)。

实时 Facade

使用实时 Facade,您可以把应用中的任何类视为 Facade。为了说明如何使用它,我们来看另外一个例子。例如,假设 Podcast 模型有一个 publish 方法。为了发布播客,我们需要注入一个 Publisher 实例:

namespace App;

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

class Podcast extends Model
{
    /**
     * 发布播客
     *
     * @param  Publisher  $publisher
     * @return void
     */
    public function publish(Publisher $publisher)
    {
        $this->update(['publishing' => now()]);

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

将发布者的实现注入到上述方法中可以让我们轻松地单独测试该方法,因为我们可以模拟要注入的发布者。但是,它要求我们每次调用 publish 方法时都要传递一个发布者实例。使用实时 Facade,我们可以有同样的可测试性,而不需要显式地传递 Publisher 实例。要生成实时 Facade,要在导入的命名空间类前加上 Facades

namespace App;

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

class Podcast extends Model
{
    /**
     * 发布播客
     *
     * @return void
     */
    public function publish()
    {
        $this->update(['publishing' => now()]);

        Publisher::publish($this);
    }
}

使用实时 Facade 时,将使用 Facades 前缀后面的接口或者类名部分从服务容器中解析发布者的实现。测试时,我们可以使用 Laravel 内置的 Facade 辅助函数来模拟该方法的调用:

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;

    /**
     * 一个测试示例
     *
     * @return void
     */
    public function test_podcast_can_be_published()
    {
        $podcast = factory(Podcast::class)->create();

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

        $podcast->publish();
    }
}

Facade 类参考

在下面您可以找到每个 Facade 类以及对应的底层类。这是一个快速查找给定 Facade 类的 API 文档的工具。也包括对应的 服务容器 绑定键。

Facade 服务容器绑定
App Illuminate\Foundation\Application app
Artisan Illuminate\Contracts\Console\Kernel artisan
Auth Illuminate\Auth\AuthManager auth
Auth (Instance) Illuminate\Contracts\Auth\Guard auth.driver
Blade Illuminate\View\Compilers\BladeCompiler blade.compiler
Broadcast Illuminate\Contracts\Broadcasting\Factory  
Broadcast (Instance) Illuminate\Contracts\Broadcasting\Broadcaster  
Bus Illuminate\Contracts\Bus\Dispatcher  
Cache Illuminate\Cache\CacheManager cache
Cache (Instance) Illuminate\Cache\Repository cache.store
Config Illuminate\Config\Repository config
Cookie Illuminate\Cookie\CookieJar cookie
Crypt Illuminate\Encryption\Encrypter encrypter
DB Illuminate\Database\DatabaseManager db
DB (Instance) Illuminate\Database\Connection db.connection
Event Illuminate\Events\Dispatcher events
File Illuminate\Filesystem\Filesystem files
Gate Illuminate\Contracts\Auth\Access\Gate  
Hash Illuminate\Contracts\Hashing\Hasher hash
Lang Illuminate\Translation\Translator translator
Log Illuminate\Log\Logger log
Mail Illuminate\Mail\Mailer mailer
Notification Illuminate\Notifications\ChannelManager  
Password Illuminate\Auth\Passwords\PasswordBrokerManager auth.password
Password (Instance) Illuminate\Auth\Passwords\PasswordBroker auth.password.broker
Queue Illuminate\Queue\QueueManager queue
Queue (Instance) Illuminate\Contracts\Queue\Queue queue.connection
Queue (Base Class) Illuminate\Queue\Queue  
Redirect Illuminate\Routing\Redirector redirect
Redis Illuminate\Redis\RedisManager redis
Redis (Instance) Illuminate\Redis\Connections\Connection redis.connection
Request Illuminate\Http\Request request
Response Illuminate\Contracts\Routing\ResponseFactory  
Response (Instance) Illuminate\Http\Response  
Route Illuminate\Routing\Router router
Schema Illuminate\Database\Schema\Builder  
Session Illuminate\Session\SessionManager session
Session (Instance) Illuminate\Session\Store session.store
Storage Illuminate\Filesystem\FilesystemManager filesystem
Storage (Instance) Illuminate\Contracts\Filesystem\Filesystem filesystem.disk
URL Illuminate\Routing\UrlGenerator url
Validator Illuminate\Validation\Factory validator
Validator (Instance) Illuminate\Validation\Validator  
View Illuminate\View\Factory view
View (Instance) Illuminate\View\View