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

为什么不用自定义配置或数据库字段做开关?
直接在 .env 里加 FEATURE_PAYMENTS_ENABLED=true 或给用户表加个 is_beta_user 字段,短期看着快,但很快会失控:开关逻辑散落在 Blade 模板、控制器、队列任务里;无法按用户/团队/请求上下文动态启用;没有审计日志;上线回滚时得改代码或发 DB 迁移。Laravel Pennant 就是为解决这些而生的——它把功能开关变成可管理、可作用域化、可测试的一等公民。
安装 Pennant 并初始化基础配置
运行命令安装包并发布配置:
composer require laravel/pennant php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
默认使用 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 App\Features;
use Laravel\Pennant\Feature;
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 App\Features\NewsletterV2; use Laravel\Pennant\Feature; Feature::define(NewsletterV2::class);
在 Blade、控制器和测试中安全使用
Blade 中用 @can 指令最简洁:
@can('enable', App\Features\NewsletterV2::class)
@endcan
控制器中推荐用门面方式判断:
use Laravel\Pennant\Feature;
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)会被序列化进数据库,所以重命名类或移动路径后,旧记录不会自动更新,需手动迁移或清空表。










