在整个 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 提供的每个辅助函数都在其相应功能文档中进行了说明; 然而, 完整的列表可在专门的辅助函数文档中查阅。
例如,无需使用 Illuminate\Support\Facades\Response 门面来生成 JSON 响应,我们只需使用 response 函数。由于助手函数是全局可用的,您无需导入任何类即可使用它们:
use Illuminate\Support\Facades\Response;
Route::get('/users', function () {
return Response::json([
// ...
]);
});
Route::get('/users', function () {
return response()->json([
// ...
]);
});门面有许多优点。它们提供了一种简洁、易记的语法,让你可以使用 Laravel 的功能,而无需记住那些必须手动注入或配置的长类名。此外,由于它们独特地使用了 PHP 的动态方法,因此它们易于测试。
然而,使用门面时必须小心。门面的主要危险是类的“范围蔓延”。由于门面非常易于使用且不需要注入,因此很容易让你的类持续增长并在单个类中使用许多门面。使用依赖注入,这种潜力可以通过大型构造函数给你的视觉反馈来缓解,即你的类正在变得过大。因此,在使用门面时,请特别注意类的大小,以使其职责范围保持狭窄。如果你的类变得过大,请考虑将其拆分为多个更小的类。
依赖注入的主要优点之一是能够交换被注入类的实现。这在测试期间非常有用,因为你可以注入一个模拟对象或存根,并断言存根上的各种方法已被调用。
通常情况下,不可能模拟或存根一个真正的静态类方法。 然而,由于门面使用动态方法来代理对从服务容器解析的对象的调用,我们实际上可以像测试一个注入的类实例一样测试门面。 例如,对于以下路由:
use Illuminate\Support\Facades\Cache;
Route::get('/cache', function () {
return Cache::get('key');
});利用 Laravel 的门面测试方法,我们可以编写以下测试来验证 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');
}除了门面之外,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;
/**
* 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');
}在 Laravel 应用中,一个门面(facade)是一个类,它提供对容器中一个对象的访问。使其工作的机制位于 Facade 类中。Laravel 的门面,以及你创建的任何自定义门面,都将扩展基础 Illuminate\Support\Facades\Facade 类。
Facade 基类利用 __callStatic() 魔术方法,将来自你的 facade 的调用推迟到从容器中解析出的对象。在下面的示例中,对 Laravel 缓存系统进行了调用。乍一看这段代码,人们可能会认为静态 get 方法正在 Cache 类上被调用:
<?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 门面. 此门面用作访问 Illuminate\Contracts\Cache\Factory 接口底层实现的代理. 我们使用此门面进行的任何调用都将传递给 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 类并定义了方法 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
{
/**
* Publish the podcast.
*/
public function publish(Publisher $publisher): void
{
$this->update(['publishing' => now()]);
$publisher->publish($this);
}
}将一个发布者实现注入到方法中,允许我们轻松地独立测试该方法,因为我们可以模拟被注入的发布者。然而,这要求我们每次调用 publish 方法时都始终传递一个发布者实例。使用实时外观,我们可以在不需要显式传递 Publisher 实例的情况下保持相同的可测试性。要生成一个实时外观,请在导入类的命名空间前加上 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]
}
}当实时外观被使用时,发布器实现将从服务容器中解析,解析时使用接口或类名中出现在 Facades 前缀之后的部分。在测试时,我们可以使用 Laravel 内置的外观测试辅助器来模拟此方法调用:
<?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();
}
}下面您将找到每个门面及其底层类。这是一个有用的工具,用于快速深入了解给定门面根的 API 文档。服务容器绑定 键在适用时也包含在内。