> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 门面

> 介绍 Laravel 门面（Facades）的机制与用法、实时门面、测试方式，以及与依赖注入的选择。

## 什么是门面

门面为应用[服务容器](/zh/service-container)中可用的类提供了「静态」接口。Laravel 内置了大量门面，几乎覆盖了框架的所有功能。

Laravel 的门面充当服务容器内实际类的「静态代理」，在保持易测性和灵活性的同时，也提供了比传统静态方法更简洁、更富表达力的语法。

所有 Laravel 门面都定义在 `Illuminate\Support\Facades` 命名空间下。

```php theme={null}
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;

Route::get('/cache', function () {
    return Cache::get('key');
});
```

<Info>
  即使不完全理解门面的原理也没关系。先记住用法继续学习 Laravel 即可。
</Info>

## 门面的工作原理

在 Laravel 应用中，门面是一种提供访问容器中对象的类。这一机制由 `Facade` 类实现。Laravel 所有门面（以及自定义门面）都继承基类 `Illuminate\Support\Facades\Facade`。

基类 `Facade` 通过 `__callStatic()` 魔术方法，把对门面的调用转发给容器解析出的实际对象。

```mermaid theme={null}
flowchart LR
    A["Cache::get('key')"] --> B["Facade::__callStatic()"]
    B --> C["getFacadeAccessor()<br>→ 'cache'"]
    C --> D["服务容器<br>make('cache')"]
    D --> E["实际类实例<br>CacheManager"]
    E --> F["执行 get('key')"]
```

```php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;

class UserController extends Controller
{
    /**
     * 显示指定用户的个人资料
     */
    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`。

```php theme={null}
class Cache extends Facade
{
    /**
     * 获取组件的注册名
     */
    protected static function getFacadeAccessor(): string
    {
        return 'cache';
    }
}
```

`Cache` 门面继承自基类 `Facade` 并实现了 `getFacadeAccessor()` 方法。该方法返回容器中的绑定名。当用户调用 `Cache` 门面的静态方法时，Laravel 会从服务容器中解析 `cache` 绑定，然后在该对象上执行所请求的方法（如 `get`）。

## 何时使用门面，何时避免使用

### 门面的优点

门面有很多优点：无需记住繁长的类名，也不必手动注入或配置，就能以简洁易记的语法使用 Laravel 功能。此外，凭借 PHP 动态方法的独特用法，门面非常易于测试。

### 注意「作用范围蔓延」

使用门面的主要风险是类的「作用范围蔓延（scope creep）」。因为门面易于使用且无需注入，类很容易膨胀并持续使用大量门面。使用依赖注入时，庞大的构造器能带来直观的反馈。使用门面时，请留意类的规模，将职责范围保持在较小的范围内。

<Warning>
  一旦发现类变得过大，请考虑将其拆分为多个更小的类。
</Warning>

### 门面 vs. 依赖注入

依赖注入的主要好处之一是可以替换被注入类的实现，这在测试时非常有用。你可以注入 mock 或 stub，然后断言其上的方法被以何种方式调用。

通常真正的静态方法难以 mock 或 stub，但门面借助动态方法把调用转发给服务容器解析出的对象，所以对门面进行测试就和测试注入的类实例一样简单。

```php theme={null}
use Illuminate\Support\Facades\Cache;

Route::get('/cache', function () {
    return Cache::get('key');
});
```

针对这条路由，可以编写如下测试来验证 `Cache::get` 被以预期参数调用。

```php theme={null}
use Illuminate\Support\Facades\Cache;

test('basic example', function () {
    Cache::shouldReceive('get')
        ->with('key')
        ->andReturn('value');

    $response = $this->get('/cache');

    $response->assertSee('value');
});
```

### 门面 vs. 辅助函数

除了门面，Laravel 还提供了执行常见任务（如生成视图、触发事件、派发任务、发送 HTTP 响应等）的「辅助函数」。许多辅助函数的功能与其对应的门面相同。

```php theme={null}
// 通过门面调用
return Illuminate\Support\Facades\View::make('profile');

// 使用辅助函数的等价调用
return view('profile');
```

门面与辅助函数在本质上没有区别。即使使用辅助函数，也可以像测试对应门面一样进行测试。

```php theme={null}
use Illuminate\Support\Facades\Cache;

test('cache helper test', function () {
    Cache::shouldReceive('get')
        ->with('key')
        ->andReturn('value');

    $response = $this->get('/cache');

    $response->assertSee('value');
});
```

## 实时门面

使用实时门面，可以将应用中的任意类当作门面使用。先看一个没有使用实时门面的例子。

例如，`Podcast` 模型有一个 `publish` 方法。但发布 podcast 需要注入一个 `Publisher` 实例。

```php theme={null}
<?php

namespace App\Models;

use App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;

class Podcast extends Model
{
    /**
     * 发布 podcast
     */
    public function publish(Publisher $publisher): void
    {
        $this->update(['publishing' => now()]);

        $publisher->publish($this);
    }
}
```

使用实时门面后，就无需显式传入 `Publisher` 实例，同时也保持了同等的可测试性。要生成实时门面，只需在要导入的类的命名空间前加上 `Facades` 前缀即可。

```php theme={null}
<?php

namespace App\Models;

use Facades\App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;

class Podcast extends Model
{
    /**
     * 发布 podcast
     */
    public function publish(): void
    {
        $this->update(['publishing' => now()]);

        Publisher::publish($this);
    }
}
```

使用实时门面时，Laravel 会根据 `Facades` 前缀后面的接口或类名，从服务容器解析出对应的实现。

```php theme={null}
<?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();
});
```

<Tip>
  当希望简化测试 mock，同时避免将依赖作为参数显式传递时，实时门面非常方便。
</Tip>

## 测试门面

测试门面时使用 `shouldReceive` 方法，会返回一个 Mockery mock 实例。由于门面实际上是通过服务容器解析和管理的，因此比普通静态类更易于测试。

```php theme={null}
use Illuminate\Support\Facades\Cache;

test('显示用户个人资料', function () {
    Cache::shouldReceive('get')
        ->once()
        ->with('user:1')
        ->andReturn(['name' => 'Taylor']);

    $response = $this->get('/users/1');

    $response->assertSee('Taylor');
});
```

常用的 mock 方法如下：

| 方法                        | 说明        |
| ------------------------- | --------- |
| `shouldReceive('method')` | 期望方法被调用   |
| `once()`                  | 期望只调用 1 次 |
| `times(n)`                | 期望调用 n 次  |
| `with(args)`              | 期望以特定参数调用 |
| `andReturn(value)`        | 返回指定值     |
| `andReturnNull()`         | 返回 null   |

## 常用门面一览

以下是常用门面与其对应实际类、服务容器绑定名的对照表。

| 门面             | 类                                         | 绑定           |
| -------------- | ----------------------------------------- | ------------ |
| `App`          | `Illuminate\Foundation\Application`       | `app`        |
| `Auth`         | `Illuminate\Auth\AuthManager`             | `auth`       |
| `Cache`        | `Illuminate\Cache\CacheManager`           | `cache`      |
| `Config`       | `Illuminate\Config\Repository`            | `config`     |
| `Cookie`       | `Illuminate\Cookie\CookieJar`             | `cookie`     |
| `Crypt`        | `Illuminate\Encryption\Encrypter`         | `encrypter`  |
| `DB`           | `Illuminate\Database\DatabaseManager`     | `db`         |
| `Event`        | `Illuminate\Events\Dispatcher`            | `events`     |
| `File`         | `Illuminate\Filesystem\Filesystem`        | `files`      |
| `Gate`         | `Illuminate\Contracts\Auth\Access\Gate`   | —            |
| `Hash`         | `Illuminate\Contracts\Hashing\Hasher`     | `hash`       |
| `Http`         | `Illuminate\Http\Client\Factory`          | —            |
| `Log`          | `Illuminate\Log\LogManager`               | `log`        |
| `Mail`         | `Illuminate\Mail\Mailer`                  | `mailer`     |
| `Notification` | `Illuminate\Notifications\ChannelManager` | —            |
| `Queue`        | `Illuminate\Queue\QueueManager`           | `queue`      |
| `RateLimiter`  | `Illuminate\Cache\RateLimiter`            | —            |
| `Redirect`     | `Illuminate\Routing\Redirector`           | `redirect`   |
| `Request`      | `Illuminate\Http\Request`                 | `request`    |
| `Route`        | `Illuminate\Routing\Router`               | `router`     |
| `Schema`       | `Illuminate\Database\Schema\Builder`      | —            |
| `Session`      | `Illuminate\Session\SessionManager`       | `session`    |
| `Storage`      | `Illuminate\Filesystem\FilesystemManager` | `filesystem` |
| `URL`          | `Illuminate\Routing\UrlGenerator`         | `url`        |
| `Validator`    | `Illuminate\Validation\Factory`           | `validator`  |
| `View`         | `Illuminate\View\Factory`                 | `view`       |

## 下一步

<Card title="Contracts（契约）" icon="file-contract" href="/zh/contracts">
  了解与门面成对的 Contracts 概念及选择方式。
</Card>


## Related topics

- [Mock](/zh/mocking.md)
- [服务容器](/zh/service-container.md)
- [日志](/zh/logging.md)
- [认证入门](/zh/authentication.md)
- [缓存](/zh/cache.md)
