门面
介绍
门面为应用程序的服务容器中的类提供了一个“静态”接口。Laravel 附带了许多门面,几乎可以访问 Laravel 的所有功能。Laravel 门面作为服务容器中底层类的“静态代理”,提供了简洁、富有表现力的语法,同时比传统的静态方法具有更好的可测试性和灵活性。
所有 Laravel 的门面都定义在 Illuminate\Support\Facades
命名空间中。因此,我们可以轻松地访问一个门面,如下所示:
use Illuminate\Support\Facades\Cache;
Route::get('/cache', function () {
return Cache::get('key');
});
在 Laravel 文档中,许多示例将使用门面来演示框架的各种功能。
何时使用门面
门面有很多好处。它们提供了一种简洁、易记的语法,使您无需记住必须手动注入或配置的长类名即可使用 Laravel 的功能。此外,由于它们独特地使用 PHP 的动态方法,它们易于测试。
然而,使用门面时必须小心。门面的主要危险是类的范围蔓延。由于门面使用起来非常简单且不需要注入,因此很容易让您的类继续增长,并在单个类中使用许多门面。使用依赖注入,这种可能性通过大型构造函数提供的视觉反馈得到了缓解,提醒您类的增长过大。因此,使用门面时,请特别注意类的大小,以确保其责任范围保持狭窄。
在构建与 Laravel 交互的第三方包时,最好注入 Laravel 合约 而不是使用门面。由于包是在 Laravel 本身之外构建的,您将无法访问 Laravel 的门面测试助手。
门面与依赖注入
依赖注入的主要好处之一是能够交换注入类的实现。这在测试期间非常有用,因为您可以注入一个模拟或存根,并断言在存根上调用了各种方法。
通常,不可能模拟或存根真正的静态类方法。然而,由于门面使用动态方法将方法调用代理到从服务容器解析的对象,我们实际上可以像测试注入的类实例一样测试门面。例如,给定以下路由:
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');
}
门面与辅助函数
除了门面,Laravel 还包括各种“辅助”函数,可以执行常见任务,如生成视图、触发事件、调度作业或发送 HTTP 响应。许多这些辅助函数执行的功能与相应的门面相同。例如,这个门面调用和辅助调用是等价的:
return View::make('profile');
return view('profile');
门面和辅助函数之间没有实际区别。使用辅助函数时,您仍然可以像测试相应的门面一样测试它们。例如,给定以下路由:
Route::get('/cache', function () {
return cache('key');
});
在后台,cache
辅助函数将调用 Cache
门面底层类的 get
方法。因此,即使我们使用辅助函数,我们也可以编写以下测试来验证该方法是否使用我们预期的参数被调用:
use Illuminate\Support\Facades\Cache;
/**
* 一个基本的功能测试示例。
*
* @return void
*/
public function testBasicExample()
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$this->visit('/cache')
->see('value');
}
门面的工作原理
在 Laravel 应用程序中,门面是一个类,它提供对容器中对象的访问。使这项工作成为可能的机制在 Facade
类中。Laravel 的门面以及您创建的任何自定义门面都将扩展基本的 Illuminate\Support\Facades\Facade
类。
Facade
基类利用 __callStatic()
魔术方法将来自您的门面的调用推迟到从容器解析的对象。在下面的示例中,调用了 Laravel 缓存系统。通过浏览此代码,人们可能会认为静态方法 get
正在 Cache
类上被调用:
<?php
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
门面。这个门面作为访问 Illuminate\Contracts\Cache\Factory
接口的底层实现的代理。我们使用门面进行的任何调用都将传递给 Laravel 缓存服务的底层实例。
如果我们查看 Illuminate\Support\Facades\Cache
类,您会看到没有静态方法 get
:
class Cache extends Facade
{
/**
* 获取组件的注册名称。
*
* @return string
*/
protected static function getFacadeAccessor() { return 'cache'; }
}
相反,Cache
门面扩展了基本的 Facade
类并定义了方法 getFacadeAccessor()
。此方法的工作是返回服务容器绑定的名称。当用户引用 Cache
门面上的任何静态方法时,Laravel 将从服务容器解析 cache
绑定,并针对该对象运行请求的方法(在本例中为 get
)。
实时门面
使用实时门面,您可以将应用程序中的任何类视为门面。为了说明如何使用它,让我们检查一个替代方案。例如,假设我们的 Podcast
模型有一个 publish
方法。然而,为了发布播客,我们需要注入一个 Publisher
实例:
<?php
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
方法时都要传递一个发布者实例。使用实时门面,我们可以保持相同的可测试性,而不需要显式传递 Publisher
实例。要生成实时门面,请在导入类的命名空间前加上 Facades
前缀:
<?php
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);
}
}
使用实时门面时,将使用 Facades
前缀后出现的接口或类名部分从服务容器中解析发布者实现。在测试时,我们可以使用 Laravel 内置的门面测试助手来模拟此方法调用:
<?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;
/**
* 一个测试示例。
*
* @return void
*/
public function test_podcast_can_be_published()
{
$podcast = factory(Podcast::class)->create();
Publisher::shouldReceive('publish')->once()->with($podcast);
$podcast->publish();
}
}
门面类参考
下面您将找到每个门面及其底层类。这是一个快速深入了解给定门面根的 API 文档的有用工具。还包括服务容器绑定键(如适用)。