门面

简介

翻阅 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 文档时,这个表很有用。适用时也会列出对应的服务容器绑定键名。

门面 类 服务容器绑定
App Illuminate\Foundation\Application app
Artisan Illuminate\Contracts\Console\Kernel artisan
Auth (Instance) Illuminate\Contracts\Auth\Guard auth.driver
Auth Illuminate\Auth\AuthManager auth
Blade Illuminate\View\Compilers\BladeCompiler blade.compiler
Broadcast (Instance) Illuminate\Contracts\Broadcasting\Broadcaster  
Broadcast Illuminate\Contracts\Broadcasting\Factory  
Bus Illuminate\Contracts\Bus\Dispatcher  
Cache (Instance) Illuminate\Cache\Repository cache.store
Cache Illuminate\Cache\CacheManager cache
Config Illuminate\Config\Repository config
Context Illuminate\Log\Context\Repository  
Cookie Illuminate\Cookie\CookieJar cookie
Crypt Illuminate\Encryption\Encrypter encrypter
Date Illuminate\Support\DateFactory date
DB (Instance) Illuminate\Database\Connection db.connection
DB Illuminate\Database\DatabaseManager db
Event Illuminate\Events\Dispatcher events
Exceptions (Instance) Illuminate\Contracts\Debug\ExceptionHandler  
Exceptions Illuminate\Foundation\Exceptions\Handler  
File Illuminate\Filesystem\Filesystem files
Gate Illuminate\Contracts\Auth\Access\Gate  
Hash Illuminate\Contracts\Hashing\Hasher hash
Http Illuminate\Http\Client\Factory  
Lang Illuminate\Translation\Translator translator
Log Illuminate\Log\LogManager log
Mail Illuminate\Mail\Mailer mailer
Notification Illuminate\Notifications\ChannelManager  
Password (Instance) Illuminate\Auth\Passwords\PasswordBroker auth.password.broker
Password Illuminate\Auth\Passwords\PasswordBrokerManager auth.password
Pipeline (Instance) Illuminate\Pipeline\Pipeline  
Process Illuminate\Process\Factory  
Queue (Base Class) Illuminate\Queue\Queue  
Queue (Instance) Illuminate\Contracts\Queue\Queue queue.connection
Queue Illuminate\Queue\QueueManager queue
RateLimiter Illuminate\Cache\RateLimiter  
Redirect Illuminate\Routing\Redirector redirect
Redis (Instance) Illuminate\Redis\Connections\Connection redis.connection
Redis Illuminate\Redis\RedisManager redis
Request Illuminate\Http\Request request
Response (Instance) Illuminate\Http\Response  
Response Illuminate\Contracts\Routing\ResponseFactory  
Route Illuminate\Routing\Router router
Schedule Illuminate\Console\Scheduling\Schedule  
Schema Illuminate\Database\Schema\Builder  
Session (Instance) Illuminate\Session\Store session.store
Session Illuminate\Session\SessionManager session
Storage (Instance) Illuminate\Contracts\Filesystem\Filesystem filesystem.disk
Storage Illuminate\Filesystem\FilesystemManager filesystem
URL Illuminate\Routing\UrlGenerator url
Validator (Instance) Illuminate\Validation\Validator  
Validator Illuminate\Validation\Factory validator
View (Instance) Illuminate\View\View  
View Illuminate\View\Factory view
Vite Illuminate\Foundation\Vite