门面
简介
在 Laravel 文档中,您会看到通过“门面”与 Laravel 功能交互的代码示例。门面为应用程序服务容器中可用的类提供了一个“静态”接口。Laravel 附带了多个门面,它们提供了对几乎所有 Laravel 功能的访问。
Laravel 门面充当服务容器中底层类的“静态代理”,提供了简洁、富有表现力的语法优点,同时比传统静态方法具有更好的可测试性和灵活性。不完全理解门面的工作原理也没关系——只要继续学习 Laravel 即可。
所有 Laravel 门面都定义在 Illuminate\Support\Facades 命名空间中。因此,我们可以轻松地像这样访问门面:
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
Route::get('/cache', function () {
return Cache::get('key');
});在 Laravel 文档中,许多示例将使用门面来演示框架的各种功能。
辅助函数
为了补充门面,Laravel 提供了各种全局“辅助函数”,使得与常见 Laravel 功能的交互更加容易。您可能会用到的一些常见辅助函数包括 view、response、url、config 等。Laravel 提供的每个辅助函数都在其对应的功能文档中有所说明;不过,完整的列表可以在专门的辅助函数文档中找到。
例如,我们可以简单地使用 response 函数,而不是使用 Illuminate\Support\Facades\Response 门面来生成 JSON 响应。由于辅助函数是全局可用的,您无需导入任何类即可使用它们:
use Illuminate\Support\Facades\Response;
Route::get('/users', function () {
return Response::json([
// ...
]);
});
Route::get('/users', function () {
return response()->json([
// ...
]);
});何时使用门面
门面有许多好处。它们提供了简洁、易记的语法,让您无需记住必须手动注入或配置的长类名即可使用 Laravel 的功能。此外,由于它们独特地使用了 PHP 的动态方法,因此易于测试。
但是,使用门面时需要谨慎。门面的主要危险在于类的“范围蔓延”。由于门面非常易于使用且无需注入,很容易让您的类不断增长并在单个类中使用许多门面。使用依赖注入时,庞大的构造函数会给您视觉反馈,表明您的类变得过大,从而减轻了这种可能性。因此,在使用门面时,要特别注意类的大小,使其职责范围保持狭窄。如果您的类变得太大,请考虑将其拆分为多个较小的类。
门面 vs. 依赖注入
依赖注入的一个主要好处是能够替换注入类的实现。这在测试期间很有用,因为您可以注入一个模拟或桩件,并断言在桩件上调用了各种方法。
通常,不可能模拟或桩件一个真正的静态类方法。但是,由于门面使用动态方法将对方法的调用代理到从服务容器解析的对象,我们实际上可以像测试注入的类实例一样测试门面。例如,给定以下路由:
use Illuminate\Support\Facades\Cache;
Route::get('/cache', function () {
return Cache::get('key');
});使用 Laravel 的门面测试方法,我们可以编写以下测试来验证 Cache::get 方法是否以我们期望的参数被调用:
use Illuminate\Support\Facades\Cache;
test('基本示例', function () {
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$response = $this->get('/cache');
$response->assertSee('value');
});use Illuminate\Support\Facades\Cache;
/**
* 一个基本的功能测试示例。
*/
public function test_basic_example(): void
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$response = $this->get('/cache');
$response->assertSee('value');
}门面 vs. 辅助函数
除了门面,Laravel 还包括各种“辅助”函数,它们可以执行常见任务,如生成视图、触发事件、分发任务或发送 HTTP 响应。这些辅助函数中的许多执行与相应门面相同的功能。例如,这个门面调用和辅助函数调用是等效的:
return Illuminate\Support\Facades\View::make('profile');
return view('profile');门面和辅助函数之间绝对没有实际区别。使用辅助函数时,您仍然可以像测试相应门面一样测试它们。例如,给定以下路由:
Route::get('/cache', function () {
return cache('key');
});cache 辅助函数将调用 Cache 门面底层类上的 get 方法。因此,即使我们使用辅助函数,我们也可以编写以下测试来验证该方法是否以我们期望的参数被调用:
use Illuminate\Support\Facades\Cache;
/**
* 一个基本的功能测试示例。
*/
public function test_basic_example(): void
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$response = $this->get('/cache');
$response->assertSee('value');
}门面的工作原理
在 Laravel 应用程序中,门面是一个提供对容器中对象的访问的类。实现这一机制的部件位于 Facade 类中。Laravel 的门面以及您创建的任何自定义门面都将扩展基础的 Illuminate\Support\Facades\Facade 类。
Facade 基类利用 __callStatic() 魔术方法将对门面的调用延迟到从容器解析的对象。在下面的示例中,调用了 Laravel 缓存系统。查看此代码,人们可能会认为静态 get 方法正在 Cache 类上被调用:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* 显示给定用户的个人资料。
*/
public function showProfile(string $id): View
{
$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
{
/**
* 获取组件的注册名称。
*/
protected static function getFacadeAccessor(): string
{
return 'cache';
}
}相反,Cache 门面扩展了基础 Facade 类并定义了 getFacadeAccessor() 方法。此方法的职责是返回服务容器绑定的名称。当用户引用 Cache 门面上的任何静态方法时,Laravel 从服务容器解析 cache 绑定,并针对该对象运行请求的方法(在本例中为 get)。
实时门面
使用实时门面,您可以将应用程序中的任何类视为门面。为了说明如何使用它,让我们首先检查一些未使用实时门面的代码。例如,假设我们的 Podcast 模型有一个 publish 方法。但是,为了发布播客,我们需要注入一个 Publisher 实例:
<?php
namespace App\Models;
use App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
/**
* 发布播客。
*/
public function publish(Publisher $publisher): void
{
$this->update(['publishing' => now()]);
$publisher->publish($this);
}
}将发布者实现注入方法允许我们轻松地隔离测试该方法,因为我们可以模拟注入的发布者。然而,它要求我们在每次调用 publish 方法时都传递一个发布者实例。使用实时门面,我们可以保持相同的可测试性,同时不需要显式传递 Publisher 实例。要生成实时门面,请在导入的类的命名空间前加上 Facades:
<?php
namespace App\Models;
use App\Contracts\Publisher;
use Facades\App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
/**
* 发布播客。
*/
public function publish(Publisher $publisher): void
public function publish(): void
{
$this->update(['publishing' => now()]);
$publisher->publish($this);
Publisher::publish($this);
}
}当使用实时门面时,发布者实现将使用出现在 Facades 前缀之后的接口或类名部分从服务容器中解析出来。在测试时,我们可以使用 Laravel 内置的门面测试辅助函数来模拟此方法调用:
<?php
use App\Models\Podcast;
use Facades\App\Contracts\Publisher;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('播客可以被发布', function () {
$podcast = Podcast::factory()->create();
Publisher::shouldReceive('publish')->once()->with($podcast);
$podcast->publish();
});<?php
namespace Tests\Feature;
use App\Models\Podcast;
use Facades\App\Contracts\Publisher;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PodcastTest extends TestCase
{
use RefreshDatabase;
/**
* 一个测试示例。
*/
public function test_podcast_can_be_published(): void
{
$podcast = Podcast::factory()->create();
Publisher::shouldReceive('publish')->once()->with($podcast);
$podcast->publish();
}
}门面类参考
下面您将找到每个门面及其底层类。这是一个快速查找给定门面根的 API 文档的有用工具。服务容器绑定键也包含在内(如果适用)。