> ## 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 Pennant

> 介绍如何使用 Laravel Pennant 管理功能开关（Feature Flags），涵盖功能定义、判断、作用域设置以及测试等内容。

## 什么是 Laravel Pennant

[Laravel Pennant](https://github.com/laravel/pennant) 是一个轻量、简洁的功能开关（Feature Flag）扩展包。功能开关可以让你分阶段发布新功能、进行 A/B 测试，或者补充主干开发流程。

### 什么是功能开关

```mermaid theme={null}
flowchart TD
    A["请求"] --> B{"Feature::active('new-api')"}
    B -->|true| C["新版 API 处理"]
    B -->|false| D["旧版 API 处理"]
    C --> E["响应"]
    D --> E
```

借助功能开关，可以将代码部署与功能发布分离。代码可以先部署到生产环境，通过配置来打开或关闭功能。

***

## 安装

<Steps>
  <Step title="安装扩展包">
    通过 Composer 安装 Pennant。

    ```bash theme={null}
    composer require laravel/pennant
    ```
  </Step>

  <Step title="发布配置文件与迁移">
    通过 `vendor:publish` Artisan 命令发布文件。

    ```bash theme={null}
    php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
    ```

    这会在 `config/pennant.php` 与 `database/migrations` 中生成对应文件。
  </Step>

  <Step title="执行迁移">
    创建 Pennant 用来保存功能开关值的 `features` 表。

    ```bash theme={null}
    php artisan migrate
    ```
  </Step>
</Steps>

***

## 配置

在 `config/pennant.php` 中可以配置要使用的存储驱动，Pennant 支持两种。

| 驱动         | 说明                |
| ---------- | ----------------- |
| `database` | 将值持久化到关系型数据库（默认）  |
| `array`    | 保存在内存中（适合测试或临时使用） |

```php theme={null}
// config/pennant.php
'default' => env('PENNANT_STORE', 'database'),
```

***

## 定义功能

### 基于闭包的定义

功能通过 `Feature` Facade 的 `define` 方法定义，通常写在服务提供者的 `boot` 方法中。闭包会接收一个“作用域（scope）”参数（一般是已认证用户）。

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

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Feature::define('new-api', fn (User $user) => match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        });
    }
}
```

上面这个功能的逻辑是：

* 团队内部成员始终开启
* 高流量客户始终关闭
* 其余用户按 1% 的概率开启

当功能首次被检查时，闭包返回的结果会写入存储驱动，后续判断都直接使用已保存的值。

<Info>
  如果定义体只是返回 Lottery，可以省略闭包。

  ```php theme={null}
  Feature::define('site-redesign', Lottery::odds(1, 1000));
  ```
</Info>

### 基于类的定义

Pennant 也支持基于类的功能定义。基于类时无需在服务提供者中注册。

```bash theme={null}
php artisan pennant:feature NewApi
```

生成的类位于 `app/Features` 目录，只需实现 `resolve` 方法。

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

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class NewApi
{
    /**
     * 解析功能的初始值。
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}
```

#### 自定义保存名

默认使用完全限定类名保存。可以通过 `Name` Attribute 自定义名字。

```php theme={null}
use Laravel\Pennant\Attributes\Name;

#[Name('new-api')]
class NewApi
{
    // ...
}
```

#### 拦截判断（`before` 方法）

基于类的功能可以定义 `before` 方法。它会在读取存储值之前在内存中执行，若返回非 `null` 值就直接使用该值。

```php theme={null}
class NewApi
{
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
    }

    public function resolve(User $user): mixed
    {
        // ...
    }
}
```

<Tip>
  `before` 方法适合在出现 bug 时紧急关闭功能，或在指定时间点做灰度发布。
</Tip>

***

## 判断功能状态

### `Feature::active()` / `Feature::inactive()`

`active` 方法可以判断功能是否启用。默认使用当前已认证用户作为作用域。

```php theme={null}
use Laravel\Pennant\Feature;

if (Feature::active('new-api')) {
    // 使用新版 API 的处理
}
```

基于类的功能则传入类名。

```php theme={null}
use App\Features\NewApi;

if (Feature::active(NewApi::class)) {
    // ...
}
```

其他常用方法：

```php theme={null}
// 是否所有功能都启用
Feature::allAreActive(['new-api', 'site-redesign']);

// 是否有任意功能启用
Feature::someAreActive(['new-api', 'site-redesign']);

// 是否功能未启用
Feature::inactive('new-api');

// 是否所有功能都未启用
Feature::allAreInactive(['new-api', 'site-redesign']);

// 是否有任意功能未启用
Feature::someAreInactive(['new-api', 'site-redesign']);
```

### 条件执行（`when` / `unless`）

`when` 方法只在功能启用时执行闭包。

```php theme={null}
return Feature::when(NewApi::class,
    fn () => $this->resolveNewApiResponse($request),
    fn () => $this->resolveLegacyApiResponse($request),
);
```

`unless` 与 `when` 相反，在功能未启用时执行第一个闭包。

```php theme={null}
return Feature::unless(NewApi::class,
    fn () => $this->resolveLegacyApiResponse($request),
    fn () => $this->resolveNewApiResponse($request),
);
```

### `HasFeatures` trait

在 `User` 模型上添加 `HasFeatures` trait 后，就可以直接从模型上检查功能。

```php theme={null}
use Laravel\Pennant\Concerns\HasFeatures;

class User extends Authenticatable
{
    use HasFeatures;
}
```

```php theme={null}
if ($user->features()->active('new-api')) {
    // ...
}

// 获取值
$value = $user->features()->value('purchase-button');

// 条件执行
$user->features()->when('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);
```

### Blade 指令

Blade 模板中可以使用 `@feature` 指令。

```blade theme={null}
@feature('site-redesign')
    {{-- 'site-redesign' 启用时 --}}
@else
    {{-- 'site-redesign' 未启用时 --}}
@endfeature

@featureany(['site-redesign', 'beta'])
    {{-- 任一功能启用时 --}}
@endfeatureany
```

### 中间件

`EnsureFeaturesAreActive` 中间件可以为路由声明必须启用的功能。若未启用会返回 `400 Bad Request`。

```php theme={null}
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;

Route::get('/api/servers', function () {
    // ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));
```

要自定义响应，使用 `whenInactive` 方法。

```php theme={null}
EnsureFeaturesAreActive::whenInactive(
    function (Request $request, array $features) {
        return new Response(status: 403);
    }
);
```

### 内存缓存

Pennant 会在同一请求内把功能结果缓存到内存中。即使多次判断同一个功能，也不会产生额外的数据库查询。

要手动清空缓存，使用 `flushCache`。

```php theme={null}
Feature::flushCache();
```

***

## 作用域（Scope）

### 指定作用域

默认情况下作用域是已认证用户，可以通过 `for` 方法指定任意作用域。

```php theme={null}
// 检查特定用户
Feature::for($user)->active('new-api');

// 检查团队
Feature::for($user->team)->active('billing-v2');
```

按团队管理功能开关的示例：

```php theme={null}
Feature::define('billing-v2', function (Team $team) {
    if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
        return true;
    }

    if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
        return Lottery::odds(1 / 100);
    }

    return Lottery::odds(1 / 1000);
});
```

### 自定义默认作用域

可以通过 `Feature::resolveScopeUsing` 修改默认作用域。

```php theme={null}
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
```

设置后不加 `for` 时会使用默认作用域。

```php theme={null}
Feature::active('billing-v2');
// 等同于
Feature::for($user->team)->active('billing-v2');
```

### Nullable 作用域

当作用域为 `null`（未认证路由、Artisan 命令等）时，如果功能定义不支持 null，会自动返回 `false`。若需要支持 null，请在定义中使用可空类型。

```php theme={null}
Feature::define('new-api', fn (User|null $user) => match (true) {
    $user === null => true,
    $user->isInternalTeamMember() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 100),
});
```

***

## 富功能值（Rich Feature Values）

功能不只可以返回布尔值。例如 A/B 测试中控制按钮颜色：

```php theme={null}
Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));
```

取值时使用 `value` 方法。

```php theme={null}
$color = Feature::value('purchase-button');
```

Blade 中可以基于值进行分支：

```blade theme={null}
@feature('purchase-button', 'blue-sapphire')
    {{-- blue-sapphire 启用 --}}
@elsefeature('purchase-button', 'seafoam-green')
    {{-- seafoam-green 启用 --}}
@elsefeature('purchase-button', 'tart-orange')
    {{-- tart-orange 启用 --}}
@endfeature
```

<Info>
  使用富值时，除 `false` 之外的所有值都视为启用。
</Info>

将富值传入 `when` 时，第一个闭包会收到该值。

```php theme={null}
Feature::when('purchase-button',
    fn ($color) => /* $color 是当前值 */,
    fn () => /* 未启用 */,
);
```

***

## 一次性获取多个功能

`values` 方法可以一次性获取多个功能的值。

```php theme={null}
Feature::values(['billing-v2', 'purchase-button']);

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]
```

`all` 方法返回已定义的所有功能的值。

```php theme={null}
Feature::all();
```

若希望 `all` 的结果包含基于类的功能，在服务提供者中调用 `discover`。

```php theme={null}
Feature::discover();
```

这样 `app/Features` 目录下的所有功能类都会被注册。

***

## Eager Loading

在循环里检查功能可能带来性能问题。可以通过 `load` 提前一次性取值。

```php theme={null}
// NG：每次循环都会产生数据库查询
foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

// OK：提前批量获取
Feature::for($users)->load(['notifications-beta']);

foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}
```

只加载未取过的值时使用 `loadMissing`。

```php theme={null}
Feature::for($users)->loadMissing([
    'new-api',
    'purchase-button',
    'notifications-beta',
]);
```

***

## 更新值

### 手动更新

`activate` / `deactivate` 用于打开或关闭功能。

```php theme={null}
// 在默认作用域下启用
Feature::activate('new-api');

// 在特定作用域下禁用
Feature::for($user->team)->deactivate('billing-v2');

// 设置富值
Feature::activate('purchase-button', 'seafoam-green');
```

若要让保存的值“被遗忘”，使用 `forget`。下次判断时会重新从定义计算。

```php theme={null}
Feature::forget('purchase-button');
```

### 批量更新

`activateForEveryone` / `deactivateForEveryone` 会一次性作用于存储中的所有作用域。

```php theme={null}
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
Feature::deactivateForEveryone('new-api');
```

### 清除功能（purge）

当你把某功能从代码中移除或修改了定义时，可以从存储中清除对应值（purge）。

```php theme={null}
// 清除单个功能
Feature::purge('new-api');

// 清除多个功能
Feature::purge(['new-api', 'purchase-button']);

// 清除全部
Feature::purge();
```

也可以通过 Artisan 命令 purge，非常适合部署流水线。

```bash theme={null}
php artisan pennant:purge new-api

# 多个功能
php artisan pennant:purge new-api purchase-button

# 除指定功能外全部 purge
php artisan pennant:purge --except=new-api --except=purchase-button

# 除已在服务提供者注册的功能外全部 purge
php artisan pennant:purge --except-registered
```

***

## 测试

### 重新定义功能

测试中可以通过 `Feature::define` 重新定义，控制返回值。

```php tab=Pest theme={null}
use Laravel\Pennant\Feature;

test('it can control feature values', function () {
    Feature::define('purchase-button', 'seafoam-green');

    expect(Feature::value('purchase-button'))->toBe('seafoam-green');
});
```

```php tab=PHPUnit theme={null}
use Laravel\Pennant\Feature;

public function test_it_can_control_feature_values(): void
{
    Feature::define('purchase-button', 'seafoam-green');

    $this->assertSame('seafoam-green', Feature::value('purchase-button'));
}
```

基于类的功能同样如此。

```php tab=Pest theme={null}
test('it can control feature values', function () {
    Feature::define(NewApi::class, true);

    expect(Feature::value(NewApi::class))->toBeTrue();
});
```

```php tab=PHPUnit theme={null}
use App\Features\NewApi;

public function test_it_can_control_feature_values(): void
{
    Feature::define(NewApi::class, true);

    $this->assertTrue(Feature::value(NewApi::class));
}
```

### 测试用存储配置

可以在 `phpunit.xml` 中通过环境变量指定测试用存储。

```xml theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <php>
        <env name="PENNANT_STORE" value="array"/>
    </php>
</phpunit>
```

***

## 自定义驱动

如果已有驱动不能满足需求，可以自定义驱动。实现 `Laravel\Pennant\Contracts\Driver` 接口即可。

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

namespace App\Extensions;

use Laravel\Pennant\Contracts\Driver;

class RedisFeatureDriver implements Driver
{
    public function define(string $feature, callable $resolver): void {}
    public function defined(): array {}
    public function getAll(array $features): array {}
    public function get(string $feature, mixed $scope): mixed {}
    public function set(string $feature, mixed $scope, mixed $value): void {}
    public function setForAllScopes(string $feature, mixed $value): void {}
    public function delete(string $feature, mixed $scope): void {}
    public function purge(array|null $features): void {}
}
```

在服务提供者的 `boot` 方法中通过 `extend` 注册。

```php theme={null}
Feature::extend('redis', function (Application $app) {
    return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
```

注册后即可在 `config/pennant.php` 中使用。

```php theme={null}
'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => null,
    ],
],
```

***

## 小结

| 想做的事       | 方法                                           |
| ---------- | -------------------------------------------- |
| 安装         | `composer require laravel/pennant`           |
| 定义功能       | `Feature::define('name', fn ($user) => ...)` |
| 判断功能       | `Feature::active('name')`                    |
| Blade 中判断  | `@feature('name') ... @endfeature`           |
| 更新值        | `Feature::activate('name')` / `deactivate`   |
| 对所有作用域生效   | `Feature::activateForEveryone('name')`       |
| 在测试中控制     | 用 `Feature::define('name', true)` 重定义        |
| 从存储中 purge | `Feature::purge('name')`                     |

## 下一步

<Columns cols={2}>
  <Card title="调试与错误处理" icon="circle-x" href="/zh/error-handling">
    了解应用异常处理与上报机制。
  </Card>

  <Card title="Laravel Pulse" icon="chart-line" href="/zh/pulse">
    引入应用性能监控仪表盘。
  </Card>
</Columns>


## Related topics

- [Laravel Pennant 实战用例](/zh/blog/laravel-pennant.md)
- [Laravel Boost](/zh/boost.md)
- [Laravel Telescope](/zh/telescope.md)
- [Laravel Bluesky](/zh/packages/laravel-bluesky/index.md)
- [Laravel Octane](/zh/octane.md)
