门面
简介
翻阅 Laravel 文档时,你会经常看到用"门面(Facade)"来调用 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 还提供了一系列全局"辅助函数",让常见操作更顺手。常用的有 view、response、url、config 等等。每个辅助函数的用法都在对应功能的文档中有说明;完整的列表可以在专门的辅助函数文档中找到。
比如,不需要用 Illuminate\Support\Facades\Response 门面来生成 JSON 响应,直接用 response 函数就行。辅助函数是全局可用的,不用 import 任何类:
use Illuminate\Support\Facades\Response;
Route::get('/users', function () {
return Response::json([
// ...
]);
});
Route::get('/users', function () {
return response()->json([
// ...
]);
});
何时使用门面
门面有很多好处。语法简洁好记,不用记住那些又长又需要手动注入或配置的类名。而且因为底层用了 PHP 动态方法,测试起来也很方便。
不过用门面时也得注意一点。最大的隐患是类的"范围蔓延"——门面太方便了,不用注入,一个类里不知不觉塞了十几个门面,类就膨胀了。用依赖注入的话,构造函数里一堆参数就是天然的"你的类太大了"的视觉提醒。所以用门面的时候,多留意你的类大小,保持职责单一。如果类太大了,考虑拆成几个小类。
阅读提示:知道什么是门面,Laravel自带了哪些门面就可以了
系统自带的几十个门面已经把能做的事全包了——路由、缓存、队列、日志、数据库……你日常开发碰到的任何需求都有对应的内置门面或辅助函数。自己创建门面只有两个理由:
- 写扩展包给别人用——包需要提供一个简洁的静态入口,比如 MyPackage::doSomething()
- 公司内部建了一套公共服务库——封装了复杂业务逻辑,用门面暴露给团队其他人使用更方便
自己的业务代码完全不需要。直接用依赖注入或 app() 辅助函数就够了,没必要包一层门面。
门面 vs 依赖注入
依赖注入最大的好处之一就是可以替换注入类的实现。测试时特别有用——注入一个 mock 或 stub,然后断言它的方法有没有被调用。
按常理说,真正静态的类方法是没法 mock 的。但门面用的是动态方法去代理从服务容器解析出来的对象,所以门面其实可以像注入的类实例一样被测试。比如下面这个路由:
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');
}
门面 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;
/**
* 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 基类。Laravel 自带的门面和你自己创建的门面,都要继承 Illuminate\Support\Facades\Facade 这个基类。
Facade 基类利用 __callStatic() 魔术方法,把对门面的静态调用转发到从容器解析出来的真实对象上。看下面这个调用 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]);
}
}
注意文件顶部我们 "import" 了 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);
}
}
把 publisher 的实现注入到方法里,测试时可以 mock 掉它,很方便。但这就要求每次调用 publish 方法都得传一个 publisher 实例。用实时门面,既能保持同样的可测试性,又不用显式传 Publisher 实例。生成实时门面的方法很简单——在 import 的类名前面加上 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]
}
}
使用实时门面时,publisher 的实现会被服务容器自动解析——解析依据是 Facades 前缀后面的接口或类名部分。测试时,可以用 Laravel 内置的门面测试辅助方法来 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();
}
}
门面类参考
下面是所有门面及其底层类的对照表。当你需要快速查阅某个门面的 API 文档时,这个表很有用。适用时也会列出对应的服务容器绑定键名。