如何为Laravel应用实现功能开关(Feature Flags)? (Laravel Pennant)

9次阅读

laravel Pennant 解决功能开关失控问题——它将开关变为可管理、可作用域化、可测试的一等公民,支持动态判断、审计与回滚,避免散落逻辑和硬编码

如何为Laravel应用实现功能开关(Feature Flags)? (Laravel Pennant)

为什么不用自定义配置或数据库字段做开关?

直接在 .env 里加 FEATURE_PAYMENTS_ENABLED=true 或给用户表加个 is_beta_user 字段,短期看着快,但很快会失控:开关逻辑散落在 Blade 模板、控制器、队列任务里;无法按用户/团队/请求上下文动态启用;没有审计日志;上线回滚时得改代码或发 DB 迁移。Laravel Pennant 就是为解决这些而生的——它把功能开关变成可管理、可作用域化、可测试的一等公民。

安装 Pennant 并初始化基础配置

运行命令安装包并发布配置:

composer require laravel/pennant php artisan vendor:publish --provider="LaravelPennantPennantServiceProvider"

默认使用 Eloquent 驱动,开关状态存在 pennant_features 表中。你不需要手动建表,执行迁移即可:

php artisan migrate

确保 config/pennant.php 中的 driver'eloquent'(默认值),如果要用 redis 做高性能缓存开关状态,需额外配置连接并设为 'redis',但注意 Redis 驱动不支持作用域化的条件判断(比如“仅对 ID > 1000 的用户开启”),这种场景必须用 Eloquent。

定义开关并绑定作用域逻辑

用 Artisan 命令生成一个开关类:

php artisan make:feature NewsletterV2

它会在 app/Features/ 下创建 NewsletterV2.php。关键不是写“开/关”,而是定义“谁能看到”:

namespace AppFeatures;  use LaravelPennantFeature;  class NewsletterV2 {     public function resolve($notifiable)     {         // $notifiable 是当前用户(Auth::user()),也可接收 Request、Team 等任意对象         return $notifiable->hasRole('admin') ||                 ($notifiable->id % 10 === 0); // 千分位用户灰度     } }

这个 resolve 方法返回布尔值,Pennant 在每次检查时调用它——所以你可以读数据库、查 Redis、甚至调用外部 API。别在这里写硬编码 return true,否则就退化成静态开关了。

注册该开关到服务提供者(通常在 AppServiceProvider::boot()):

use AppFeaturesNewsletterV2; use LaravelPennantFeature;  Feature::define(NewsletterV2::class);

在 Blade、控制器和测试中安全使用

Blade 中用 @can 指令最简洁:

@can('enable', AppFeaturesNewsletterV2::class)      @endcan

控制器中推荐用门面方式判断:

use LaravelPennantFeature;  if (Feature::active(NewsletterV2::class)) {     return new NewsletterV2Response(); }  // 或带作用域:只对当前用户判断 if (Feature::for(auth()->user())->active(NewsletterV2::class)) {     // ... }

测试时别 mock 全局状态,直接用 Feature::define 覆盖解析逻辑:

Feature::define(NewsletterV2::class, fn () => true);  $this->assertTrue(Feature::active(NewsletterV2::class));

切记:不要在模型观察者或队列 handle 方法里直接调用 Feature::active() 而不传作用域对象,因为队列中 auth()->user()NULL——必须显式传入目标用户实例或 ID。

开关名(如 NewsletterV2::class)会被序列化进数据库,所以重命名类或移动路径后,旧记录不会自动更新,需手动迁移或清空表。

text=ZqhQzanResources