Facades
简介
在 Laravel 文档中,你会看到许多通过「Facades」与 Laravel 功能交互的代码示例。Facades 为应用 服务容器 中可用的类提供「静态」接口。Laravel 内置了许多 Facades,几乎可以访问 Laravel 的全部功能。
Laravel Facades 充当服务容器中底层类的「静态代理」,既带来简洁、富有表现力的语法,又比传统静态方法更具可测试性与灵活性。即便你尚未完全理解 Facades 的工作原理也没关系——跟着节奏继续学习 Laravel 即可。
Laravel 的全部 Facades 都定义在 Illuminate\Support\Facades 命名空间中。因此,你可以像这样轻松访问 Facade:
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
Route::get('/cache', function () {
return Cache::get('key');
});在 Laravel 文档中,许多示例会使用 Facades 来演示框架的各项功能。
辅助函数
作为 Facades 的补充,Laravel 还提供了多种全局「辅助函数」,让与常用 Laravel 功能的交互更加轻松。你可能会用到的常见辅助函数包括 view、response、url、config 等。Laravel 提供的每个辅助函数都会在对应功能文档中说明;完整列表请参阅专门的 辅助函数文档。
例如,不必使用 Illuminate\Support\Facades\Response Facade 生成 JSON 响应,可以直接使用 response 函数。由于辅助函数是全局可用的,使用时无需导入任何类:
use Illuminate\Support\Facades\Response;
Route::get('/users', function () {
return Response::json([
// ...
]);
});
Route::get('/users', function () {
return response()->json([
// ...
]);
});何时使用 Facades
Facades 有许多优点。它们提供简洁、易记的语法,让你无需记住必须注入或手动配置的冗长类名即可使用 Laravel 的功能。此外,由于它们独特地使用了 PHP 的动态方法,测试起来也很方便。
不过,使用 Facades 时仍需谨慎。Facades 的主要风险是类的「职责膨胀」。由于 Facades 极易使用且无需注入,很容易让类不断变大,并在单个类中使用大量 Facades。使用依赖注入时,过大的构造函数会给出直观反馈,提醒你类已经过大,从而减轻这种风险。因此,使用 Facades 时要特别注意类的规模,使其职责范围保持狭窄。若类变得过大,可考虑拆分为多个更小的类。
Facades 与依赖注入
依赖注入的主要好处之一,是能够替换被注入类的实现。这在测试时很有用,因为你可以注入 mock 或 stub,并断言 stub 上调用了哪些方法。
通常,真正的静态类方法无法被 mock 或 stub。然而,由于 Facades 使用动态方法将方法调用代理到从服务容器解析出的对象,我们实际上可以像测试注入的类实例一样测试 Facades。例如,给定以下路由:
use Illuminate\Support\Facades\Cache;
Route::get('/cache', function () {
return Cache::get('key');
});使用 Laravel 的 Facade 测试方法,我们可以编写如下测试,验证 Cache::get 是否以我们期望的参数被调用:
use Illuminate\Support\Facades\Cache;
test('basic example', function () {
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$response = $this->get('/cache');
$response->assertSee('value');
});use Illuminate\Support\Facades\Cache;
/**
* A basic functional test example.
*/
public function test_basic_example(): void
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$response = $this->get('/cache');
$response->assertSee('value');
}Facades 与辅助函数
除了 Facades,Laravel 还包含多种「辅助」函数,可执行生成视图、触发事件、分发任务或发送 HTTP 响应等常见任务。其中许多辅助函数与对应 Facade 的功能相同。例如,下面的 Facade 调用与辅助函数调用是等价的:
return Illuminate\Support\Facades\View::make('profile');
return view('profile');Facades 与辅助函数在实际使用上没有区别。使用辅助函数时,你仍可以像测试对应 Facade 一样测试它们。例如,给定以下路由:
Route::get('/cache', function () {
return cache('key');
});cache 辅助函数会调用 Cache Facade 底层类上的 get 方法。因此,即使我们使用的是辅助函数,也可以编写如下测试,验证该方法是否以我们期望的参数被调用:
use Illuminate\Support\Facades\Cache;
/**
* A basic functional test example.
*/
public function test_basic_example(): void
{
Cache::shouldReceive('get')
->with('key')
->andReturn('value');
$response = $this->get('/cache');
$response->assertSee('value');
}Facades 如何工作
在 Laravel 应用中,Facade 是一个提供从容器访问对象的类。实现这一机制的核心位于 Facade 类中。Laravel 的 Facades 以及你创建的任何自定义 Facades,都会扩展基类 Illuminate\Support\Facades\Facade。
Facade 基类利用 __callStatic() 魔术方法,将你对 Facade 的调用转发给从容器解析出的对象。在下面的示例中,调用了 Laravel 缓存系统。粗看这段代码,可能会以为是在 Cache 类上调用静态 get 方法:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* Show the profile for the given user.
*/
public function showProfile(string $id): View
{
$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
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return 'cache';
}
}相反,Cache Facade 扩展了基类 Facade,并定义了 getFacadeAccessor() 方法。该方法的职责是返回服务容器绑定的名称。当用户引用 Cache Facade 上的任何静态方法时,Laravel 会从 服务容器 解析 cache 绑定,并在该对象上运行所请求的方法(本例中为 get)。
实时 Facades
使用实时 Facades,你可以将应用中的任意类当作 Facade 来使用。为说明用法,我们先看一段未使用实时 Facades 的代码。例如,假设我们的 Podcast 模型有一个 publish 方法。但要发布播客,需要注入一个 Publisher 实例:
<?php
namespace App\Models;
use App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
/**
* Publish the podcast.
*/
public function publish(Publisher $publisher): void
{
$this->update(['publishing' => now()]);
$publisher->publish($this);
}
}向方法注入 publisher 实现,让我们可以轻松地单独测试该方法,因为可以 mock 被注入的 publisher。不过,每次调用 publish 方法时都必须传入 publisher 实例。使用实时 Facades,我们可以保持相同的可测试性,而无需显式传入 Publisher 实例。要生成实时 Facade,请在导入类的命名空间前加上 Facades 前缀:
<?php
namespace App\Models;
use App\Contracts\Publisher; // [tl! remove]
use Facades\App\Contracts\Publisher; // [tl! add]
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
/**
* Publish the podcast.
*/
public function publish(Publisher $publisher): void // [tl! remove]
public function publish(): void // [tl! add]
{
$this->update(['publishing' => now()]);
$publisher->publish($this); // [tl! remove]
Publisher::publish($this); // [tl! add]
}
}使用实时 Facade 时,会使用 Facades 前缀之后的接口或类名部分,从服务容器解析 publisher 实现。测试时,可以使用 Laravel 内置的 Facade 测试辅助方法来 mock 该方法调用:
<?php
use App\Models\Podcast;
use Facades\App\Contracts\Publisher;
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->use(RefreshDatabase::class);
test('podcast can be published', 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;
/**
* A test example.
*/
public function test_podcast_can_be_published(): void
{
$podcast = Podcast::factory()->create();
Publisher::shouldReceive('publish')->once()->with($podcast);
$podcast->publish();
}
}Facade 类参考
下面列出了每个 Facade 及其底层类。这有助于快速查阅给定 Facade 根的 API 文档。在适用处也包含了 服务容器绑定 键。