简介

在 Laravel 文档中,你会看到通过“门面(facades)”与 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 类中。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]);
    }
}

注意,在文件顶部我们“导入”了 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;
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;

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;

    /**
     * 一个测试示例。
     */
    public function test_podcast_can_be_published(): void
    {
        $podcast = Podcast::factory()->create();

        Publisher::shouldReceive('publish')->once()->with($podcast);

        $podcast->publish();
    }
}

门式类参考

下面可以找到每个门式类及其底层类。这是一个实用的工具,可帮助您快速查阅特定门式根类的 API 文档。同时,在适用情况下,也包含了服务容器绑定键。

外观类服务容器绑定
AppIlluminate\Foundation\Applicationapp
ArtisanIlluminate\Contracts\Console\Kernel 
Auth (Instance)Illuminate\Contracts\Auth\Guardauth.driver
AuthIlluminate\Auth\AuthManagerauth
BladeIlluminate\View\Compilers\BladeCompilerblade.compiler
Broadcast (Instance)Illuminate\Contracts\Broadcasting\Broadcaster 
BroadcastIlluminate\Contracts\Broadcasting\Factory 
BusIlluminate\Contracts\Bus\Dispatcher 
Cache (Instance)Illuminate\Cache\Repositorycache.store
CacheIlluminate\Cache\CacheManagercache
CloudIlluminate\Foundation\Cloud\CloudManager 
ConfigIlluminate\Config\Repositoryconfig
ContextIlluminate\Log\Context\Repository 
CookieIlluminate\Cookie\CookieJarcookie
CryptIlluminate\Encryption\Encrypterencrypter
DateIlluminate\Support\DateFactorydate
DB (Instance)Illuminate\Database\Connectiondb.connection
DBIlluminate\Database\DatabaseManagerdb
EventIlluminate\Events\Dispatcherevents
Exceptions (Instance)Illuminate\Contracts\Debug\ExceptionHandler 
ExceptionsIlluminate\Foundation\Exceptions\Handler 
FileIlluminate\Filesystem\Filesystemfiles
GateIlluminate\Contracts\Auth\Access\Gate 
HashIlluminate\Contracts\Hashing\Hasherhash
HttpIlluminate\Http\Client\Factory 
LangIlluminate\Translation\Translatortranslator
LogIlluminate\Log\LogManagerlog
Mail (Instance)Illuminate\Mail\Mailermailer
MailIlluminate\Mail\MailManagermail.manager
NotificationIlluminate\Notifications\ChannelManager 
Password (Instance)Illuminate\Auth\Passwords\PasswordBrokerauth.password.broker
PasswordIlluminate\Auth\Passwords\PasswordBrokerManagerauth.password
Pipeline (Instance)Illuminate\Pipeline\Pipeline 
ProcessIlluminate\Process\Factory 
Queue (Base Class)Illuminate\Queue\Queue 
Queue (Instance)Illuminate\Contracts\Queue\Queuequeue.connection
QueueIlluminate\Queue\QueueManagerqueue
RateLimiterIlluminate\Cache\RateLimiter 
RedirectIlluminate\Routing\Redirectorredirect
Redis (Instance)Illuminate\Redis\Connections\Connectionredis.connection
RedisIlluminate\Redis\RedisManagerredis
RequestIlluminate\Http\Requestrequest
Response (Instance)Illuminate\Http\Response 
ResponseIlluminate\Contracts\Routing\ResponseFactory 
RouteIlluminate\Routing\Routerrouter
ScheduleIlluminate\Console\Scheduling\Schedule 
SchemaIlluminate\Database\Schema\Builder 
Session (Instance)Illuminate\Session\Storesession.store
SessionIlluminate\Session\SessionManagersession
Storage (Instance)Illuminate\Contracts\Filesystem\Filesystemfilesystem.disk
StorageIlluminate\Filesystem\FilesystemManagerfilesystem
URLIlluminate\Routing\UrlGeneratorurl
Validator (Instance)Illuminate\Validation\Validator 
ValidatorIlluminate\Validation\Factoryvalidator
View (Instance)Illuminate\View\View 
ViewIlluminate\View\Factoryview
ViteIlluminate\Foundation\Vite