Pennant
这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。
Laravel Pennant
简介
Laravel Pennant 是一个简单且轻量的 功能开关(feature flag) 包 —— 没有多余的复杂性。
功能开关让你能够:
- 逐步发布新的应用功能;
- 自信地进行功能上线;
- 对新的界面设计进行 A/B 测试;
- 辅助 主干式开发(trunk-based development) 策略;
- 以及更多扩展用途。
安装
首先,使用 Composer 包管理器将 Pennant 安装到你的项目中:
composer require laravel/pennant
接下来,你需要通过 vendor:publish
Artisan 命令发布 Pennant 的配置文件和迁移文件:
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
最后,运行项目的数据库迁移。这会创建一个 features
表,Pennant 将使用该表来驱动其 database
驱动:
php artisan migrate
配置(Configuration)
在发布 Pennant 的资源之后,其配置文件将位于 config/pennant.php
。
该配置文件允许你指定 默认的存储机制,用于存储 Pennant 已解析的功能开关值。
Pennant 支持以下存储方式:
- 通过
array
驱动将已解析的功能开关值存储在 内存数组 中; - 或通过
database
驱动将已解析的功能开关值 持久化存储 到关系型数据库中。
其中,database
驱动是 Pennant 默认使用的存储机制。
定义功能(Defining Features)
要定义一个功能开关,可以使用 Feature
facade 提供的 define
方法。
你需要为功能指定一个名称,并提供一个闭包(closure),该闭包将在解析功能的初始值时被调用。
通常,功能会在 服务提供者(service provider) 中使用 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
{
/**
* 引导应用服务。
*/
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));
基于类的功能(Class Based Features)
Pennant 还支持定义基于类的功能。
与基于闭包的功能定义不同,基于类的功能无需在服务提供者中注册。
要创建一个基于类的功能,可以运行以下 Artisan 命令。
默认情况下,功能类会被放置在应用程序的 app/Features
目录中:
php artisan pennant:feature NewApi
当编写一个功能类时,只需要定义一个 resolve
方法。
该方法会被调用,用于解析给定作用域(scope)的功能初始值。
通常,这个作用域就是当前已登录的用户:
<?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),
};
}
}
如果你希望手动解析一个基于类的功能实例,可以通过 Feature
facade 调用 instance
方法:
use Illuminate\Support\Facades\Feature;
$instance = Feature::instance(NewApi::class);
[!注意]
功能类是通过 Laravel 容器 解析的,
因此你可以在功能类的构造函数中注入所需的依赖。
自定义存储的功能名称(Customizing the Stored Feature Name)
默认情况下,Pennant 会将功能类的完整命名空间类名作为存储标识。
如果你希望将存储的功能名称与应用程序的内部结构解耦,可以在功能类中定义一个 $name
属性。
此属性的值将会替代类名被保存:
<?php
namespace App\Features;
class NewApi
{
/**
* 功能在存储中使用的名称。
*
* @var string
*/
public $name = 'new-api';
// ...
}
检查功能状态(Checking Features)
要判断某个功能是否处于启用状态,可以使用 Feature
facade 的 active
方法。
默认情况下,Pennant 会针对当前已认证的用户检查功能状态:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* 显示资源列表。
*/
public function index(Request $request): Response
{
return Feature::active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}
虽然默认是针对当前登录用户进行检查,但你也可以轻松地针对其他用户或作用域(scope)进行检查。
要实现这一点,可以使用 Feature
facade 提供的 for
方法:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
Pennant 还提供了一些额外的便捷方法,用于判断功能是否处于激活或未激活状态:
// 判断给定的所有功能是否都已激活...
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']);
[!注意]
当在 HTTP 上下文之外 使用 Pennant(例如在 Artisan 命令或队列任务中)时,
通常应显式指定功能的作用域(scope)。
或者,你也可以定义一个默认作用域(default scope),
以同时兼容已认证与未认证的上下文环境。
检查基于类的功能(Class Based Features)
对于基于类的功能,在检查功能时,你应当提供类名:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* 显示资源列表。
*/
public function index(Request $request): Response
{
return Feature::active(NewApi::class)
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}
条件执行(Conditional Execution)
when
方法可以用于在某个功能处于激活状态时,流畅地执行指定的闭包。此外,还可以提供第二个闭包,当功能未激活时将执行该闭包:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* 显示资源列表。
*/
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
方法还提供了许多其他便捷的方法,用于与功能进行交互:
// 获取值...
$value = $user->features()->value('purchase-button');
$values = $user->features()->values(['new-api', 'purchase-button']);
// 状态判断...
$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']);
// 条件执行...
$user->features()->when('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
$user->features()->unless('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
Blade 指令(Blade Directive)
为了让在 Blade 模板中检查功能更加方便,Pennant 提供了 @feature
和 @featureany
指令:
@feature('site-redesign')
<!-- “site-redesign” 功能处于激活状态 -->
@else
<!-- “site-redesign” 功能未激活 -->
@endfeature
@featureany(['site-redesign', 'beta'])
<!-- “site-redesign” 或 “beta” 功能处于激活状态 -->
@endfeatureany
中间件(Middleware)
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'));
自定义响应(Customizing the Response)
如果你希望在中间件检测到某个指定功能未激活时自定义返回的响应,可以使用 EnsureFeaturesAreActive
中间件提供的 whenInactive
方法。
通常,这个方法应在应用的某个服务提供者(Service Provider)的 boot
方法中调用:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* 启动应用服务。
*/
public function boot(): void
{
EnsureFeaturesAreActive::whenInactive(
function (Request $request, array $features) {
return new Response(status: 403);
}
);
// ...
}
拦截功能检查(Intercepting Feature Checks)
有时,在获取某个功能的存储值之前执行一些内存检查会非常有用。
想象一下,你正在开发一个通过功能标志控制的新 API,并且希望在不丢失任何已解析的功能值的情况下,能够暂时禁用这个新 API。
例如,当你在新 API 中发现了一个 Bug 时,可以轻松地为除内部团队成员外的所有用户禁用该功能,修复 Bug 后,再重新启用该功能给原先有访问权限的用户。
你可以通过 基于类的功能(class-based feature) 的 before
方法实现这一点。
当存在 before
方法时,它会在检索存储值之前始终在内存中运行。
如果该方法返回了一个非 null
的值,那么此值将在本次请求期间替代存储中的功能值:
<?php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* 在检索存储值之前,运行始终驻留于内存中的检查。
*/
public function before(User $user): mixed
{
if (Config::get('features.new-api.disabled')) {
return $user->isInternalTeamMember();
}
}
/**
* 解析功能的初始值。
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}
你也可以利用这个功能来安排一个此前受功能标志(feature flag)控制的功能的全局发布:
<?php
namespace App\Features;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
class NewApi
{
/**
* 在检索存储值之前运行始终驻留于内存中的检查。
*/
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;
}
}
// ...
}
内存缓存(In-Memory Cache)
在检查某个功能时,Pennant 会创建一个内存缓存来保存结果。
如果你使用的是 database
驱动,那么在同一个请求中重复检查相同的功能标志,将不会触发额外的数据库查询。
这同时也确保了该功能在整个请求周期内返回的结果保持一致。
如果你需要手动清空内存缓存,可以使用 Feature
facade 提供的 flushCache
方法:
Feature::flushCache();
范围(Scope)
指定范围(Specifying the Scope)
如前所述,功能通常是针对当前已认证的用户进行检查的。
然而,这并不总是符合你的需求。
因此,你可以通过 Feature
facade 的 for
方法,指定要检查功能的作用范围(scope):
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
当然,功能的作用范围不仅限于“用户”。
想象一下,你构建了一个新的账单系统体验,并且希望按团队(team)而不是按用户进行逐步发布。
也许你希望创建时间较早的团队比新团队获得更新的速度更慢。
你的功能解析闭包(resolution closure)可以像这样编写:
use App\Models\Team;
use Carbon\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);
});
你会注意到,我们定义的闭包参数并不是 User
,而是一个 Team
模型。
要判断某个功能对用户所属团队是否启用,你应该将团队对象传递给 Feature
facade 提供的 for
方法:
if (Feature::for($user->team)->active('billing-v2')) {
return redirect('/billing/v2');
}
// ...
默认作用域(Default Scope)
你还可以自定义 Pennant 在检查功能时使用的默认作用域(scope)。
例如,假设你希望所有功能都基于当前已认证用户所属的团队进行检查,而不是用户本身。
与其每次都调用 Feature::for($user->team)
,你可以直接将团队指定为默认作用域。
通常,这应在应用程序的某个 服务提供者(Service Provider) 中完成:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* 启动任何应用服务。
*/
public function boot(): void
{
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
// ...
}
}
如果在检查功能时没有通过 for
方法显式指定作用域,
那么现在功能检查将默认使用“当前已认证用户的团队”作为作用域:
Feature::active('billing-v2');
// 等价于:
Feature::for($user->team)->active('billing-v2');
可空作用域(Nullable Scope)
如果在检查功能时你传入的作用域是 null
,
并且该功能的定义中没有通过可空类型(nullable type)或联合类型中包含 null
来支持 null
值,
那么 Pennant 会自动返回 false
作为该功能的结果值。
因此,如果你传递给某个功能的作用域(scope)可能为 null
,并且你希望功能值解析器仍然能够被调用,那么你需要在功能定义中考虑这种情况。
当在 Artisan 命令、队列任务(queued job) 或 未认证路由 中检查功能时,作用域可能会为 null
,
因为在这些上下文中通常没有已认证用户,所以默认作用域会是 null
。
如果你并不总是会显式指定功能作用域,
那么你应确保作用域的类型是 可空的(nullable),
并在功能定义逻辑中处理 null
作用域的情况:
use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
// 原本:
// Feature::define('new-api', fn (User $user) => match (true) {
// 修改后:
Feature::define('new-api', fn (User|null $user) => match (true) {
$user === null => true,
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});
标识作用域(Identifying Scope)
Pennant 内置的 array
和 database
存储驱动,
能够正确地为所有 PHP 数据类型以及 Eloquent 模型存储作用域标识符(scope identifier)。
然而,如果你的应用使用了第三方的 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
{
/**
* 将对象转换为给定驱动可存储的功能作用域标识符。
*/
public function toFeatureIdentifier(string $driver): mixed
{
return match($driver) {
'database' => $this,
'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
};
}
}
序列化作用域(Serializing Scope)
默认情况下,Pennant 在存储与 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();
丰富功能值(Rich Feature Values)
到目前为止,我们展示的功能主要是二元状态,即“激活”或“未激活”。然而,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' 被激活 -->
@elsefeature('purchase-button', 'seafoam-green')
<!-- 'seafoam-green' 被激活 -->
@elsefeature('purchase-button', 'tart-orange')
<!-- 'tart-orange' 被激活 -->
@endfeature
[!注意]
使用丰富功能值时,需要注意:当功能值不是false
时,该功能被视为“激活”。
当调用条件方法 when
时,功能(feature)的完整值(rich value) 会被传递给第一个闭包参数:
Feature::when('purchase-button',
fn ($color) => /* ... */,
fn () => /* ... */,
);
同样地,当调用条件方法 unless
时,功能的完整值会被传递给第二个可选闭包:
Feature::unless('purchase-button',
fn () => /* ... */,
fn ($color) => /* ... */,
);
获取多个功能(Retrieving Multiple Features)
values
方法允许你为某个作用域(scope)一次性获取多个功能的值:
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,
// ]
不过,需要注意的是:基于类(class-based)的功能 是动态注册的。
Pennant 在功能被首次检查(checked) 之前,并不知道这些功能的存在。
因此,如果某个类功能在当前请求中尚未被检查过,它可能不会出现在 all
方法的返回结果中。
如果你希望在调用 all
方法时始终包含这些类功能(无论它们是否被检查过),
可以启用 Pennant 的功能自动发现(feature discovery) 能力。
要启用自动发现,请在应用的某个服务提供者(Service Provider)中调用 discover
方法:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* 启动任何应用服务。
*/
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,
// ]
预加载(Eager Loading)
尽管 Pennant 会在单次请求中对所有解析过的功能保持内存缓存,但在某些情况下仍可能出现性能问题。为了解决这个问题,Pennant 提供了预加载(eager load)功能值的能力。
例如,假设我们在循环中检查某个功能是否激活:
use Laravel\Pennant\Feature;
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
如果使用的是数据库驱动,上述代码会对每个用户执行一次数据库查询——可能执行上百次查询。
然而,使用 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();
更新功能值(Updating Values)
当功能值首次被解析时,底层驱动会将结果存储到存储器中。
这通常是为了确保用户在多次请求中的体验一致。不过,有时你可能希望手动更新功能的存储值。
为此,你可以使用 activate
和 deactivate
方法,将功能“开启”或“关闭”:
use Laravel\Pennant\Feature;
// 为默认作用域激活功能...
Feature::activate('new-api');
// 为指定作用域(如用户团队)禁用功能...
Feature::for($user->team)->deactivate('billing-v2');
你也可以通过向 activate
方法提供第二个参数来手动设置功能的丰富值:
Feature::activate('purchase-button', 'seafoam-green');
如果你希望 Pennant 忘记某个功能的存储值,可以使用 forget
方法。当该功能再次被检查时,Pennant 会根据功能定义重新解析其值:
Feature::forget('purchase-button');
批量更新(Bulk Updates)
要批量更新存储的功能值,可以使用 activateForEveryone
和 deactivateForEveryone
方法。
例如,假设你已经确认 new-api
功能的稳定性,并确定了结账流程中 'purchase-button'
的最佳颜色,你可以相应地为所有用户更新存储值:
use Laravel\Pennant\Feature;
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
或者,你也可以为所有用户停用该功能:
Feature::deactivateForEveryone('new-api');
[!注意]
这只会更新由 Pennant 的存储驱动存储的已解析功能值。你还需要在应用中更新功能定义。
清除功能(Purging Features)
有时,将整个功能从存储中清除是有用的。通常在以下情况下需要这样做:
-
你已经从应用中移除了该功能
-
或者你修改了功能定义,并希望将新的定义应用到所有用户
可以使用 purge
方法移除某个功能的所有存储值:
// 清除单个功能...
Feature::purge('new-api');
// 清除多个功能...
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
测试(Testing)
在测试与功能标志交互的代码时,控制功能标志返回值最简单的方法是重新定义该功能。例如,假设你在应用的某个服务提供者中定义了如下功能:
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'));
}
The same approach may be used for class based features:
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));
}
If your feature is returning a Lottery
instance, there are a handful of useful testing helpers available.
Store Configuration
You may configure the store that Pennant will use during testing by defining the PENNANT_STORE
environment variable in your application's phpunit.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<!-- ... -->
<php>
<env name="PENNANT_STORE" value="array"/>
<!-- ... -->
</php>
</phpunit>
Adding Custom Pennant Drivers
Implementing the Driver
If none of Pennant's existing storage drivers fit your application's needs, you may write your own storage driver. Your custom driver should implement the Laravel\Pennant\Contracts\Driver
interface:
<?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 {}
}
Now, we just need to implement each of these methods using a Redis connection. For an example of how to implement each of these methods, take a look at the Laravel\Pennant\Drivers\DatabaseDriver
in the Pennant source code
[!NOTE]
Laravel does not ship with a directory to contain your extensions. You are free to place them anywhere you like. In this example, we have created anExtensions
directory to house theRedisFeatureDriver
.
Registering the Driver
Once your driver has been implemented, you are ready to register it with Laravel. To add additional drivers to Pennant, you may use the extend
method provided by the Feature
facade. You should call the extend
method from the boot
method of one of your application's service provider:
<?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'), []);
});
}
}
Once the driver has been registered, you may use the redis
driver in your application's config/pennant.php
configuration file:
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => null,
],
// ...
],
Defining Features Externally
If your driver is a wrapper around a third-party feature flag platform, you will likely define features on the platform rather than using Pennant's Feature::define
method. If that is the case, your custom driver should also implement the Laravel\Pennant\Contracts\DefinesFeaturesExternally
interface:
<?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 {}
/* ... */
}
The definedFeaturesForScope
method should return a list of feature names defined for the provided scope.
Events
Pennant dispatches a variety of events that can be useful when tracking feature flags throughout your application.
Laravel\Pennant\Events\FeatureRetrieved
This event is dispatched whenever a feature is checked. This event may be useful for creating and tracking metrics against a feature flag's usage throughout your application.
Laravel\Pennant\Events\FeatureResolved
This event is dispatched the first time a feature's value is resolved for a specific scope.
Laravel\Pennant\Events\UnknownFeatureResolved
This event is dispatched the first time an unknown feature is resolved for a specific scope. Listening to this event may be useful if you have intended to remove a feature flag but have accidentally left stray references to it throughout your application:
<?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
This event is dispatched when a class based feature is dynamically checked for the first time during a request.
Laravel\Pennant\Events\UnexpectedNullScopeEncountered
This event is dispatched when a null
scope is passed to a feature definition that doesn't support null.
This situation is handled gracefully and the feature will return false
. However, if you would like to opt out of this feature's default graceful behavior, you may register a listener for this event in the boot
method of your application's AppServiceProvider
:
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
This event is dispatched when updating a feature for a scope, usually by calling activate
or deactivate
.
Laravel\Pennant\Events\FeatureUpdatedForAllScopes
This event is dispatched when updating a feature for all scopes, usually by calling activateForEveryone
or deactivateForEveryone
.
Laravel\Pennant\Events\FeatureDeleted
This event is dispatched when deleting a feature for a scope, usually by calling forget
.
Laravel\Pennant\Events\FeaturesPurged
This event is dispatched when purging specific features.
Laravel\Pennant\Events\AllFeaturesPurged
This event is dispatched when purging all features.
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。