Laravel Pennant
介绍
Laravel Pennant 是一个简洁轻量的功能开关(feature flag)包,没有多余负担。功能开关让你能够自信地逐步发布新功能、对新界面设计进行 A/B 测试、配合主干开发策略,以及做更多事情。
安装
首先,使用 Composer 将 Pennant 安装到项目中:
composer require laravel/pennant接下来,使用 vendor:publish Artisan 命令发布 Pennant 的配置与迁移文件:
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"最后,运行应用的数据库迁移。这将创建 features 表,供 Pennant 的 database 驱动使用:
php artisan migrate配置
发布 Pennant 资源后,配置文件位于 config/pennant.php。该文件可指定 Pennant 存储已解析功能开关值的默认存储机制。
Pennant 支持通过 array 驱动将已解析的功能开关值存入内存数组;也可通过 database 驱动持久化到关系型数据库,后者是 Pennant 的默认存储机制。
定义功能
定义功能时,可使用 Feature Facade 提供的 define 方法。需提供功能名称,以及用于解析功能初始值的闭包。
通常,功能在服务提供者中使用 Feature Facade 定义。闭包会接收功能检查的「作用域」(scope)。最常见的是当前已认证用户。本例将为应用用户逐步发布新 API 定义一个功能:
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::define('new-api', fn (User $user) => match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});
}
}如上所示,该功能遵循以下规则:
- 所有内部团队成员都应使用新 API。
- 任何高流量客户都不应使用新 API。
- 否则,应以 1/100 的概率随机为用户启用该功能。
首次为某用户检查 new-api 功能时,闭包结果会由存储驱动保存。下次对同一用户再次检查时,将从存储读取该值,不再调用闭包。
为方便起见,若功能定义仅返回 lottery,可完全省略闭包:
Feature::define('site-redesign', Lottery::odds(1, 1000));
基于类的功能
Pennant 也支持定义基于类的功能。与基于闭包的定义不同,基于类的功能无需在服务提供者中注册。可执行 pennant:feature Artisan 命令创建;默认情况下,功能类会放在应用的 app/Features 目录中:
php artisan pennant:feature NewApi编写功能类时,只需定义 resolve 方法,用于为给定作用域解析功能的初始值。作用域通常仍是当前已认证用户:
<?php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* Resolve the feature's initial value.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}若要手动解析基于类的功能实例,可调用 Feature Facade 的 instance 方法:
use Illuminate\Support\Facades\Feature;
$instance = Feature::instance(NewApi::class);INFO
功能类通过容器解析,因此可在需要时向功能类构造函数注入依赖。
自定义存储的功能名称
默认情况下,Pennant 会存储功能类的完全限定类名。若希望将存储名称与应用内部结构解耦,可在功能类上添加 Name 属性,其值将替代类名被存储:
<?php
namespace App\Features;
use Laravel\Pennant\Attributes\Name;
#[Name('new-api')]
class NewApi
{
// ...
}检查功能
要判断功能是否启用,可使用 Feature Facade 的 active 方法。默认情况下,功能会针对当前已认证用户进行检查:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
return Feature::active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}默认针对当前已认证用户检查功能,但也可轻松针对其他用户或作用域检查。使用 Feature Facade 的 for 方法即可:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);Pennant 还提供其他便捷方法,用于判断功能是否启用:
// Determine if all of the given features are active...
Feature::allAreActive(['new-api', 'site-redesign']);
// Determine if any of the given features are active...
Feature::someAreActive(['new-api', 'site-redesign']);
// Determine if a feature is inactive...
Feature::inactive('new-api');
// Determine if all of the given features are inactive...
Feature::allAreInactive(['new-api', 'site-redesign']);
// Determine if any of the given features are inactive...
Feature::someAreInactive(['new-api', 'site-redesign']);检查基于类的功能
检查基于类的功能时,应传入类名:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
return Feature::active(NewApi::class)
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}条件执行
when 方法可在功能启用时流畅地执行给定闭包;还可提供第二个闭包,在功能未启用时执行:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
return Feature::when(NewApi::class,
fn () => $this->resolveNewApiResponse($request),
fn () => $this->resolveLegacyApiResponse($request),
);
}
// ...
}unless 方法是 when 的反向版本,在功能未启用时执行第一个闭包:
return Feature::unless(NewApi::class,
fn () => $this->resolveLegacyApiResponse($request),
fn () => $this->resolveNewApiResponse($request),
);HasFeatures Trait
可将 Pennant 的 HasFeatures trait 添加到应用的 User 模型(或任何带功能的模型),以便直接从模型流畅、便捷地检查功能:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;
class User extends Authenticatable
{
use HasFeatures;
// ...
}将 trait 添加到模型后,可调用 features 方法轻松检查功能:
if ($user->features()->active('new-api')) {
// ...
}当然,features 方法还提供许多其他便捷方法用于与功能交互:
// Values...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);
// State...
$user->features()->active('new-api');
$user->features()->allAreActive(['new-api', 'server-api']);
$user->features()->someAreActive(['new-api', 'server-api']);
$user->features()->inactive('new-api');
$user->features()->allAreInactive(['new-api', 'server-api']);
$user->features()->someAreInactive(['new-api', 'server-api']);
// Conditional execution...
$user->features()->when('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
$user->features()->unless('new-api',
fn () => /* ... */,
fn () => /* ... */,
);Blade 指令
为在 Blade 中无缝检查功能,Pennant 提供 @feature 与 @featureany 指令:
@feature('site-redesign')
<!-- 'site-redesign' is active -->
@else
<!-- 'site-redesign' is inactive -->
@endfeature
@featureany(['site-redesign', 'beta'])
<!-- 'site-redesign' or `beta` is active -->
@endfeatureany中间件
Pennant 还提供中间件,可在路由被调用前验证当前已认证用户是否拥有某功能访问权限。可将中间件分配给路由,并指定访问该路由所需的功能。若当前用户任一指定功能未启用,路由将返回 400 Bad Request HTTP 响应。可向静态 using 方法传入多个功能。
use Illuminate\Support\Facades\Route;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
Route::get('/api/servers', function () {
// ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));自定义响应
若要自定义中间件在所列功能未启用时返回的响应,可使用 EnsureFeaturesAreActive 中间件提供的 whenInactive 方法。通常应在应用某个服务提供者的 boot 方法中调用:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
EnsureFeaturesAreActive::whenInactive(
function (Request $request, array $features) {
return new Response(status: 403);
}
);
// ...
}拦截功能检查
有时,在读取某功能的存储值之前先做内存检查会很有用。假设你在功能开关后开发新 API,希望在不禁用存储中已解析功能值的情况下关闭新 API。若发现新 API 有缺陷,可轻松对除内部团队成员外的所有人禁用,修复后再为此前有权限的用户重新启用。
可通过基于类的功能的 before 方法实现。若存在该方法,会在从存储读取值之前始终在内存中执行。若方法返回非 null 值,则在本次请求期间将替代功能的存储值使用:
<?php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* Run an always-in-memory check before the stored value is retrieved.
*/
public function before(User $user): mixed
{
if (Config::get('features.new-api.disabled')) {
return $user->isInternalTeamMember();
}
}
/**
* Resolve the feature's initial value.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}也可利用该能力,为此前受功能开关控制的功能安排全局发布:
<?php
namespace App\Features;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
class NewApi
{
/**
* Run an always-in-memory check before the stored value is retrieved.
*/
public function before(User $user): mixed
{
if (Config::get('features.new-api.disabled')) {
return $user->isInternalTeamMember();
}
if (Carbon::parse(Config::get('features.new-api.rollout-date'))->isPast()) {
return true;
}
}
// ...
}内存缓存
检查功能时,Pennant 会在内存中缓存结果。若使用 database 驱动,同一请求内重复检查同一功能开关不会触发额外数据库查询,并确保功能在请求期间结果一致。
若需手动清空内存缓存,可使用 Feature Facade 的 flushCache 方法:
Feature::flushCache();作用域
指定作用域
如前所述,功能通常针对当前已认证用户检查。但这未必总符合需求,因此可通过 Feature Facade 的 for 方法指定要检查的作用域:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);当然,功能作用域不限于「用户」。假设你构建了新的计费体验,面向整个团队而非单个用户发布。你可能希望较老的团队比新团队更慢地灰度。功能解析闭包可能如下:
use App\Models\Team;
use Illuminate\Support\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
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);
});注意,我们定义的闭包期望的是 Team 模型而非 User。要判断用户团队是否启用该功能,应将团队传给 Feature Facade 的 for 方法:
if (Feature::for($user->team)->active('billing-v2')) {
return redirect('/billing/v2');
}
// ...默认作用域
也可自定义 Pennant 检查功能时使用的默认作用域。例如,所有功能都针对当前用户的团队而非用户本人检查。这样就不必每次检查都调用 Feature::for($user->team),可将团队设为默认作用域。通常应在应用某个服务提供者中完成:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
// ...
}
}若未通过 for 方法显式提供作用域,功能检查将默认使用当前已认证用户的团队:
Feature::active('billing-v2');
// Is now equivalent to...
Feature::for($user->team)->active('billing-v2');可空作用域
若检查功能时提供的作用域为 null,且功能定义未通过可空类型或联合类型中的 null 支持 null,Pennant 会自动将功能结果设为 false。
因此,若传入的作用域可能为 null 且你希望仍调用功能值解析器,应在功能定义中处理该情况。在 Artisan 命令、队列任务或未认证路由中检查功能时,作用域可能为 null;这些场景通常没有已认证用户,默认作用域即为 null。
若并非总是显式指定功能作用域,应确保作用域类型为「可空」,并在功能定义逻辑中处理 null 作用域值:
use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
Feature::define('new-api', fn (User $user) => match (true) {// [tl! remove]
Feature::define('new-api', fn (User|null $user) => match (true) {// [tl! add]
$user === null => true,// [tl! add]
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});标识作用域
Pennant 内置的 array 与 database 存储驱动能正确存储所有 PHP 数据类型及 Eloquent 模型的作用域标识符。但若应用使用第三方 Pennant 驱动,该驱动可能无法正确存储 Eloquent 模型或其他自定义类型的标识符。
为此,Pennant 允许在作为作用域的对象上实现 FeatureScopeable 合约,以格式化用于存储的作用域值。
例如,同一应用中同时使用内置 database 驱动与第三方「Flag Rocket」驱动。「Flag Rocket」驱动无法正确存储 Eloquent 模型,而需要 FlagRocketUser 实例。通过实现 FeatureScopeable 合约定义的 toFeatureIdentifier,可为应用使用的各驱动自定义可存储的作用域值:
<?php
namespace App\Models;
use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;
class User extends Model implements FeatureScopeable
{
/**
* Cast the object to a feature scope identifier for the given driver.
*/
public function toFeatureIdentifier(string $driver): mixed
{
return match($driver) {
'database' => $this,
'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
};
}
}序列化作用域
默认情况下,存储与 Eloquent 模型关联的功能时会使用完全限定类名。若已使用 Eloquent morph map,也可让 Pennant 使用 morph map,使存储的功能与应用结构解耦。
在服务提供者中定义 Eloquent morph map 后,可调用 Feature Facade 的 useMorphMap 方法:
use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
Feature::useMorphMap();富功能值
此前我们主要将功能展示为二元状态(「启用」或「未启用」),但 Pennant 也支持存储富值。
例如,你在测试应用「立即购买」按钮的三种新颜色。功能定义可返回字符串,而非 true 或 false:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn (User $user) => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));可使用 value 方法获取 purchase-button 功能的值:
$color = Feature::value('purchase-button');Pennant 内置的 Blade 指令也便于根据功能当前值条件渲染内容:
@feature('purchase-button', 'blue-sapphire')
<!-- 'blue-sapphire' is active -->
@elsefeature('purchase-button', 'seafoam-green')
<!-- 'seafoam-green' is active -->
@elsefeature('purchase-button', 'tart-orange')
<!-- 'tart-orange' is active -->
@endfeatureINFO
使用富值时需注意:只要值不是 false,功能即视为「启用」。
调用条件 when 方法时,功能的富值会传给第一个闭包:
Feature::when('purchase-button',
fn ($color) => /* ... */,
fn () => /* ... */,
);同样,调用条件 unless 方法时,功能的富值会传给可选的第二个闭包:
Feature::unless('purchase-button',
fn () => /* ... */,
fn ($color) => /* ... */,
);获取多个功能
values 方法可获取给定作用域下的多个功能:
Feature::values(['billing-v2', 'purchase-button']);
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// ]也可使用 all 方法获取给定作用域下所有已定义功能的值:
Feature::all();
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]但基于类的功能是动态注册的,在显式检查之前 Pennant 并不知道它们。因此,若当前请求中尚未检查,基于类的功能可能不会出现在 all 方法的返回结果中。
若希望使用 all 方法时始终包含功能类,可使用 Pennant 的功能发现能力。在应用某个服务提供者中调用 discover 方法即可开始:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::discover();
// ...
}
}discover 方法会注册 app/Features 目录下的所有功能类。此后 all 方法的结果将始终包含这些类,无论当前请求是否已检查过它们:
Feature::all();
// [
// 'App\Features\NewApi' => true,
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]预加载
尽管 Pennant 会在单次请求内缓存所有已解析功能,仍可能出现性能问题。为此,Pennant 支持预加载功能值。
例如,在循环中检查功能是否启用:
use Laravel\Pennant\Feature;
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}假设使用 database 驱动,上述代码会为循环中的每个用户执行一次数据库查询,可能多达数百次。使用 Pennant 的 load 方法,可为用户或作用域集合预加载功能值,消除该性能瓶颈:
Feature::for($users)->load(['notifications-beta']);
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}仅在尚未加载时加载功能值,可使用 loadMissing 方法:
Feature::for($users)->loadMissing([
'new-api',
'purchase-button',
'notifications-beta',
]);可使用 loadAll 方法加载所有已定义功能:
Feature::for($users)->loadAll();更新值
功能值首次解析时,底层驱动会将结果写入存储,这通常用于在多次请求间为用户保持一致体验。但有时你可能需要手动更新功能的存储值。
可使用 activate 和 deactivate 方法将功能切换为「开」或「关」:
use Laravel\Pennant\Feature;
// Activate the feature for the default scope...
Feature::activate('new-api');
// Deactivate the feature for the given scope...
Feature::for($user->team)->deactivate('billing-v2');也可向 activate 方法传入第二个参数,手动设置功能的富值:
Feature::activate('purchase-button', 'seafoam-green');要让 Pennant 忘记功能的存储值,可使用 forget 方法。再次检查该功能时,Pennant 会从功能定义重新解析其值:
Feature::forget('purchase-button');批量更新
要批量更新存储的功能值,可使用 activateForEveryone 和 deactivateForEveryone 方法。
例如,你已确信 new-api 功能稳定,并为结账流程确定了最佳的 'purchase-button' 颜色——可据此为所有用户更新存储值:
use Laravel\Pennant\Feature;
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');也可为所有用户停用该功能:
Feature::deactivateForEveryone('new-api');INFO
这只会更新 Pennant 存储驱动已保存的已解析功能值。你还需要在应用中更新功能定义。
清除功能
有时需要从存储中清除整个功能。这通常发生在已从应用中移除该功能,或调整了功能定义并希望向所有用户重新发布时。
可使用 purge 方法移除某功能的全部存储值:
// Purging a single feature...
Feature::purge('new-api');
// Purging multiple features...
Feature::purge(['new-api', 'purchase-button']);若要清除存储中的_全部_功能,可不带参数调用 purge 方法:
Feature::purge();将功能清除纳入部署流水线会很有用,因此 Pennant 提供 pennant:purge Artisan 命令,用于从存储中清除指定功能:
php artisan pennant:purge new-api
php artisan pennant:purge new-api purchase-button也可清除除给定功能列表外的所有功能。例如,你想清除全部功能但保留存储中「new-api」与「purchase-button」的值,可将这些功能名传给 --except 选项:
php artisan pennant:purge --except=new-api --except=purchase-button为方便起见,pennant:purge 还支持 --except-registered 标志,表示除在服务提供者中显式注册的功能外,其余全部清除:
php artisan pennant:purge --except-registered测试
测试与功能开关交互的代码时,最简单的方式是在测试中重新定义功能以控制返回值。例如,假设在应用某个服务提供者中定义了以下功能:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn () => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));要在测试中修改功能返回值,可在测试开头重新定义该功能。以下测试将始终通过,即使服务提供者中仍存在 Arr::random() 实现:
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');
});use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define('purchase-button', 'seafoam-green');
$this->assertSame('seafoam-green', Feature::value('purchase-button'));
}基于类的功能也可采用相同方式:
use Laravel\Pennant\Feature;
test('it can control feature values', function () {
Feature::define(NewApi::class, true);
expect(Feature::value(NewApi::class))->toBeTrue();
});use App\Features\NewApi;
use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define(NewApi::class, true);
$this->assertTrue(Feature::value(NewApi::class));
}若功能返回 Lottery 实例,可使用若干实用的测试辅助方法。
存储配置
可在应用的 phpunit.xml 中定义 PENNANT_STORE 环境变量,配置测试时 Pennant 使用的存储:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<!-- ... -->
<php>
<env name="PENNANT_STORE" value="array"/>
<!-- ... -->
</php>
</phpunit>添加自定义 Pennant 驱动
实现驱动
若现有存储驱动都不满足需求,可自行编写存储驱动。自定义驱动应实现 Laravel\Pennant\Contracts\Driver 接口:
<?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 {}
}接下来,使用 Redis 连接实现这些方法。实现示例可参考 Pennant 源码中的 Laravel\Pennant\Drivers\DatabaseDriver。
INFO
Laravel 不会自带用于放置扩展的目录,你可自由放置。本例中我们创建了 Extensions 目录来存放 RedisFeatureDriver。
注册驱动
实现驱动后,即可向 Laravel 注册。要向 Pennant 添加额外驱动,可使用 Feature Facade 的 extend 方法。应在应用某个服务提供者的 boot 方法中调用:
<?php
namespace App\Providers;
use App\Extensions\RedisFeatureDriver;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// ...
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::extend('redis', function (Application $app) {
return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
}
}注册驱动后,可在 config/pennant.php 中使用 redis 驱动:
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => null,
],
// ...
],在外部定义功能
若驱动是对第三方功能开关平台的封装,功能更可能在平台上定义,而非使用 Pennant 的 Feature::define 方法。此时自定义驱动还应实现 Laravel\Pennant\Contracts\DefinesFeaturesExternally 接口:
<?php
namespace App\Extensions;
use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Contracts\DefinesFeaturesExternally;
class FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally
{
/**
* Get the features defined for the given scope.
*/
public function definedFeaturesForScope(mixed $scope): array {}
/* ... */
}definedFeaturesForScope 方法应返回为给定作用域定义的功能名称列表。
事件
Pennant 会派发多种事件,便于在应用中跟踪功能开关。
Laravel\Pennant\Events\FeatureRetrieved
每当检查功能时派发此事件,可用于创建并跟踪功能开关在应用中的使用指标。
Laravel\Pennant\Events\FeatureResolved
首次为特定作用域解析功能值时派发此事件。
Laravel\Pennant\Events\UnknownFeatureResolved
首次为特定作用域解析未知功能时派发此事件。若你打算移除功能开关却误留引用,监听此事件会很有用:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnknownFeatureResolved;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(function (UnknownFeatureResolved $event) {
Log::error("Resolving unknown feature [{$event->feature}].");
});
}
}Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass
当基于类的功能在请求中首次被动态检查时派发此事件。
Laravel\Pennant\Events\UnexpectedNullScopeEncountered
当 null 作用域传给不支持 null的功能定义时派发此事件。
该情况会被优雅处理,功能将返回 false。但若想退出该默认行为,可在 AppServiceProvider 的 boot 方法中为此事件注册监听器:
use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));
}Laravel\Pennant\Events\FeatureUpdated
为某作用域更新功能时派发此事件,通常通过调用 activate 或 deactivate。
Laravel\Pennant\Events\FeatureUpdatedForAllScopes
为所有作用域更新功能时派发此事件,通常通过调用 activateForEveryone 或 deactivateForEveryone。
Laravel\Pennant\Events\FeatureDeleted
为某作用域删除功能时派发此事件,通常通过调用 forget。
Laravel\Pennant\Events\FeaturesPurged
清除特定功能时派发此事件。
Laravel\Pennant\Events\AllFeaturesPurged
清除所有功能时派发此事件。