门面 (Facades)
简介
在整个 Laravel 文档中,你会看到许多通过“门面(facades)”与 Laravel 功能交互的代码示例。门面为应用程序服务容器中可用的类提供了“静态”接口。Laravel 附带了许多门面,几乎涵盖了 Laravel 的所有功能。
Laravel 门面作为服务容器中底层类的“静态代理”,既提供了简洁、富有表现力的语法优势,又保持了比传统静态方法更好的可测试性和灵活性。如果你不完全理解门面是如何工作的也没关系——顺其自然,继续学习 Laravel 即可。
Laravel 的所有门面都定义在 Illuminate\Support\Facades 命名空间中。因此,我们可以像这样轻松访问门面:
1use Illuminate\Support\Facades\Cache;2use Illuminate\Support\Facades\Route;3 4Route::get('/cache', function () {5 return Cache::get('key');6});
在整个 Laravel 文档中,许多示例都将使用门面来演示框架的各种功能。
辅助函数
为了补充门面,Laravel 提供了各种全局“辅助函数(helper functions)”,使与常见 Laravel 功能的交互变得更加容易。你可能会用到的一些常用辅助函数包括 view、response、url、config 等。Laravel 提供的每个辅助函数都记录在其相应的功能文档中;不过,完整的列表可在专门的辅助函数文档中查看。
例如,与其使用 Illuminate\Support\Facades\Response 门面来生成 JSON 响应,我们完全可以使用 response 函数。由于辅助函数是全局可用的,因此无需导入任何类即可使用它们:
1use Illuminate\Support\Facades\Response; 2 3Route::get('/users', function () { 4 return Response::json([ 5 // ... 6 ]); 7}); 8 9Route::get('/users', function () {10 return response()->json([11 // ...12 ]);13});
何时使用门面
门面有很多好处。它们提供了简洁、易记的语法,让你能够使用 Laravel 的功能,而无需记住必须手动注入或配置的长类名。此外,由于它们独特地使用了 PHP 的动态方法,因此它们非常易于测试。
然而,使用门面时需要小心。门面的主要风险在于类“作用域蔓延(scope creep)”。由于门面使用起来非常简单且不需要注入,很容易导致你的类持续增长,并在单个类中使用过多的门面。使用依赖注入时,大型构造函数会提供视觉反馈,提示你的类正变得过于臃肿,从而缓解了这种可能性。因此,在使用门面时,请特别注意类的大小,以确保其职责范围保持在狭窄的范围内。如果你的类变得过大,请考虑将其拆分为多个较小的类。
门面 vs. 依赖注入
依赖注入的主要好处之一是能够替换注入类的实现。这在测试过程中非常有用,因为你可以注入一个模拟对象(mock)或存根(stub),并断言该存根上的各种方法已被调用。
通常,测试真正的静态类方法是不可能的。但是,由于门面使用动态方法将方法调用代理给从服务容器解析出来的对象,我们实际上可以像测试已注入的类实例一样测试门面。例如,给定以下路由:
1use Illuminate\Support\Facades\Cache;2 3Route::get('/cache', function () {4 return Cache::get('key');5});
使用 Laravel 的门面测试方法,我们可以编写以下测试来验证 Cache::get 方法是否按预期参数被调用:
1use Illuminate\Support\Facades\Cache; 2 3test('basic example', function () { 4 Cache::shouldReceive('get') 5 ->with('key') 6 ->andReturn('value'); 7 8 $response = $this->get('/cache'); 9 10 $response->assertSee('value');11});
1use Illuminate\Support\Facades\Cache; 2 3/** 4 * A basic functional test example. 5 */ 6public function test_basic_example(): void 7{ 8 Cache::shouldReceive('get') 9 ->with('key')10 ->andReturn('value');11 12 $response = $this->get('/cache');13 14 $response->assertSee('value');15}
门面 vs. 辅助函数
除了门面之外,Laravel 还包含各种可以执行常见任务(如生成视图、触发事件、分发任务或发送 HTTP 响应)的“辅助”函数。许多这些辅助函数执行与相应门面相同的功能。例如,以下门面调用和辅助函数调用是等效的:
1return Illuminate\Support\Facades\View::make('profile');2 3return view('profile');
门面和辅助函数之间在实际使用中没有任何区别。当使用辅助函数时,你仍然可以像测试相应门面一样对其进行测试。例如,给定以下路由:
1Route::get('/cache', function () {2 return cache('key');3});
cache 辅助函数将调用 Cache 门面底层类上的 get 方法。因此,即使我们使用的是辅助函数,我们也可以编写以下测试来验证该方法是否按预期参数被调用:
1use Illuminate\Support\Facades\Cache; 2 3/** 4 * A basic functional test example. 5 */ 6public function test_basic_example(): void 7{ 8 Cache::shouldReceive('get') 9 ->with('key')10 ->andReturn('value');11 12 $response = $this->get('/cache');13 14 $response->assertSee('value');15}
门面如何工作
在 Laravel 应用程序中,门面是一个提供对容器中对象访问的类。实现这一功能的机制位于 Facade 类中。Laravel 的门面以及你自己创建的任何自定义门面,都将继承基础的 Illuminate\Support\Facades\Facade 类。
Facade 基类利用 __callStatic() 魔术方法将调用从你的门面延迟到从容器解析出的对象。在下面的示例中,调用了 Laravel 的缓存系统。仅看这段代码,人们可能会认为是在 Cache 类上调用了静态 get 方法:
1<?php 2 3namespace App\Http\Controllers; 4 5use Illuminate\Support\Facades\Cache; 6use Illuminate\View\View; 7 8class UserController extends Controller 9{10 /**11 * Show the profile for the given user.12 */13 public function showProfile(string $id): View14 {15 $user = Cache::get('user:'.$id);16 17 return view('profile', ['user' => $user]);18 }19}
请注意,在文件顶部我们“导入”了 Cache 门面。此门面作为访问 Illuminate\Contracts\Cache\Factory 接口底层实现的代理。我们使用门面进行的任何调用都将传递给 Laravel 缓存服务的底层实例。
如果我们查看那个 Illuminate\Support\Facades\Cache 类,你会发现其中并没有静态的 get 方法:
1class Cache extends Facade 2{ 3 /** 4 * Get the registered name of the component. 5 */ 6 protected static function getFacadeAccessor(): string 7 { 8 return 'cache'; 9 }10}
相反,Cache 门面继承了基类 Facade 并定义了 getFacadeAccessor() 方法。此方法的作用是返回服务容器绑定的名称。当用户在 Cache 门面上引用任何静态方法时,Laravel 会从服务容器中解析 cache 绑定,并对该对象运行请求的方法(在本例中为 get)。
实时门面
使用实时门面,你可以将应用程序中的任何类视为门面。为了说明它是如何使用的,我们先来看一些不使用实时门面的代码。例如,假设我们的 Podcast 模型有一个 publish 方法。然而,为了发布播客,我们需要注入一个 Publisher 实例:
1<?php 2 3namespace App\Models; 4 5use App\Contracts\Publisher; 6use Illuminate\Database\Eloquent\Model; 7 8class Podcast extends Model 9{10 /**11 * Publish the podcast.12 */13 public function publish(Publisher $publisher): void14 {15 $this->update(['publishing' => now()]);16 17 $publisher->publish($this);18 }19}
将发布者实现注入到该方法中,使我们能够轻松地独立测试该方法,因为我们可以模拟注入的发布者。但是,它要求我们每次调用 publish 方法时都必须传递一个发布者实例。使用实时门面,我们可以保持相同的可测试性,同时无需显式传递 Publisher 实例。要生成实时门面,只需在导入类的命名空间前加上 Facades 前缀即可:
1<?php 2 3namespace App\Models; 4 5use App\Contracts\Publisher; 6use Facades\App\Contracts\Publisher; 7use Illuminate\Database\Eloquent\Model; 8 9class Podcast extends Model10{11 /**12 * Publish the podcast.13 */14 public function publish(Publisher $publisher): void 15 public function publish(): void 16 {17 $this->update(['publishing' => now()]);18 19 $publisher->publish($this); 20 Publisher::publish($this); 21 }22}
当使用实时门面时,发布者实现将使用 Facades 前缀之后的那部分接口或类名从服务容器中解析出来。在测试时,我们可以使用 Laravel 内置的门面测试辅助函数来模拟此方法调用:
1<?php 2 3use App\Models\Podcast; 4use Facades\App\Contracts\Publisher; 5use Illuminate\Foundation\Testing\RefreshDatabase; 6 7pest()->use(RefreshDatabase::class); 8 9test('podcast can be published', function () {10 $podcast = Podcast::factory()->create();11 12 Publisher::shouldReceive('publish')->once()->with($podcast);13 14 $podcast->publish();15});
1<?php 2 3namespace Tests\Feature; 4 5use App\Models\Podcast; 6use Facades\App\Contracts\Publisher; 7use Illuminate\Foundation\Testing\RefreshDatabase; 8use Tests\TestCase; 9 10class PodcastTest extends TestCase11{12 use RefreshDatabase;13 14 /**15 * A test example.16 */17 public function test_podcast_can_be_published(): void18 {19 $podcast = Podcast::factory()->create();20 21 Publisher::shouldReceive('publish')->once()->with($podcast);22 23 $podcast->publish();24 }25}
门面类参考
下面列出了每个门面及其底层类。这是一个快速查阅特定门面根(facade root)API 文档的有用工具。适用时,还包括了服务容器绑定键。