跳至内容

Laravel Pennant

介绍

Laravel Pennant是一个简洁轻量的功能开关包,没有任何繁琐的配置。功能开关让您能够自信地逐步推出新的应用功能、对新的界面设计进行 A/B 测试、补充基于主干的开发策略等等。

安装

首先,使用 Composer 包管理器将 Pennant 安装到您的项目中:

1composer require laravel/pennant

接下来,您应该使用 Artisan 命令发布 Pennant 配置和迁移文件vendor:publish

1php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"

最后,你应该运行应用程序的数据库迁移。这将创建一个features表,供 Pennant 驱动使用database

1php artisan migrate

配置

发布 Pennant 的资产后,其配置文件将位于config/pennant.php。此配置文件允许您指定 Pennant 用于存储已解析功能标志值的默认存储机制。

Pennant 支持通过驱动程序将已解析的功能标志值存储在内存数组中array。或者,Pennant 也可以通过驱动程序将已解析的功能标志值持久存储在关系数据库中database,这是 Pennant 使用的默认存储机制。

定义特征

要定义一个特性,你可以使用外观层define提供的方法Feature。你需要为该特性提供一个名称,以及一个将被调用来解析该特性初始值的闭包。

通常,功能是在服务提供者中使用Feature外观 (Facade) 定义的。闭包将接收功能检查的“作用域”。最常见的是,作用域是当前已验证的用户。在本例中,我们将定义一个功能,用于逐步向应用程序的用户推出新的 API:

1<?php
2 
3namespace App\Providers;
4 
5use App\Models\User;
6use Illuminate\Support\Lottery;
7use Illuminate\Support\ServiceProvider;
8use Laravel\Pennant\Feature;
9 
10class AppServiceProvider extends ServiceProvider
11{
12 /**
13 * Bootstrap any application services.
14 */
15 public function boot(): void
16 {
17 Feature::define('new-api', fn (User $user) => match (true) {
18 $user->isInternalTeamMember() => true,
19 $user->isHighTrafficCustomer() => false,
20 default => Lottery::odds(1 / 100),
21 });
22 }
23}

如您所见,我们的功能有以下规则:

  • 所有内部团队成员都应该使用新的 API。
  • 任何高流量客户都不应使用新的 API。
  • 否则,该功能应随机分配给有 1/100 机会被激活的用户。

第一次new-api针对特定用户检查该功能时,闭包的结果将由存储驱动程序存储。下次针对同一用户检查该功能时,将从存储中检索该值,并且不会调用闭包。

为了方便起见,如果功能定义仅返回Lottery,则可以完全省略闭包:

1Feature::define('site-redesign', Lottery::odds(1, 1000));

基于类的特征

Pennant 还允许您定义基于类的特征。与基于闭包的特征定义不同,基于类的特征无需在服务提供者中注册。要创建基于类的特征,您可以调用pennant:featureArtisan 命令。默认情况下,特征类将放置在应用程序的app/Features目录中:

1php artisan pennant:feature NewApi

编写特性类时,只需定义一个resolve方法,该方法将被调用来解析给定范围的特性初始值。同样,范围通常是当前已认证的用户:

1<?php
2 
3namespace App\Features;
4 
5use App\Models\User;
6use Illuminate\Support\Lottery;
7 
8class NewApi
9{
10 /**
11 * Resolve the feature's initial value.
12 */
13 public function resolve(User $user): mixed
14 {
15 return match (true) {
16 $user->isInternalTeamMember() => true,
17 $user->isHighTrafficCustomer() => false,
18 default => Lottery::odds(1 / 100),
19 };
20 }
21}

如果您想要手动解析基于类的功能的实例,您可以调用外观instance上的方法Feature

1use Illuminate\Support\Facades\Feature;
2 
3$instance = Feature::instance(NewApi::class);

要素类是通过容器解析的,因此您可以在需要时将依赖项注入要素类的构造函数中。

自定义存储的特征名称

默认情况下,Pennant 将存储要素类的完全限定类名。如果您希望将存储的要素名称与应用程序的内部结构分离,则可以$name在要素类上指定一个属性。此属性的值将代替类名存储:

1<?php
2 
3namespace App\Features;
4 
5class NewApi
6{
7 /**
8 * The stored name of the feature.
9 *
10 * @var string
11 */
12 public $name = 'new-api';
13 
14 // ...
15}

检查功能

要判断某个功能是否处于Events状态,可以使用外观层active上的方法Feature。默认情况下,将根据当前已验证的用户来检查功能:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Response;
7use Laravel\Pennant\Feature;
8 
9class PodcastController
10{
11 /**
12 * Display a listing of the resource.
13 */
14 public function index(Request $request): Response
15 {
16 return Feature::active('new-api')
17 ? $this->resolveNewApiResponse($request)
18 : $this->resolveLegacyApiResponse($request);
19 }
20 
21 // ...
22}

虽然默认情况下会根据当前已认证的用户检查功能,但你可以轻松地根据其他用户或范围检查该功能。为此,请使用外观for提供的方法Feature

1return Feature::for($user)->active('new-api')
2 ? $this->resolveNewApiResponse($request)
3 : $this->resolveLegacyApiResponse($request);

Pennant 还提供了一些额外的便捷方法,这些方法在确定某个功能是否处于Events状态时可能会有用:

1// Determine if all of the given features are active...
2Feature::allAreActive(['new-api', 'site-redesign']);
3 
4// Determine if any of the given features are active...
5Feature::someAreActive(['new-api', 'site-redesign']);
6 
7// Determine if a feature is inactive...
8Feature::inactive('new-api');
9 
10// Determine if all of the given features are inactive...
11Feature::allAreInactive(['new-api', 'site-redesign']);
12 
13// Determine if any of the given features are inactive...
14Feature::someAreInactive(['new-api', 'site-redesign']);

在 HTTP 上下文之外使用 Pennant 时(例如在 Artisan 命令或队列作业中),通常应明确指定该功能的 scope。或者,您可以定义一个默认范围,该范围同时涵盖已验证的 HTTP 上下文和未验证的上下文。

检查基于类的特征

对于基于类的功能,您应该在检查功能时提供类名:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Features\NewApi;
6use Illuminate\Http\Request;
7use Illuminate\Http\Response;
8use Laravel\Pennant\Feature;
9 
10class PodcastController
11{
12 /**
13 * Display a listing of the resource.
14 */
15 public function index(Request $request): Response
16 {
17 return Feature::active(NewApi::class)
18 ? $this->resolveNewApiResponse($request)
19 : $this->resolveLegacyApiResponse($request);
20 }
21 
22 // ...
23}

条件执行

如果某个功能处于Events状态,则该when方法可用于流畅地执行给定的闭包。此外,还可以提供第二个闭包,如果该功能处于非Events状态,则将执行该闭包:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Features\NewApi;
6use Illuminate\Http\Request;
7use Illuminate\Http\Response;
8use Laravel\Pennant\Feature;
9 
10class PodcastController
11{
12 /**
13 * Display a listing of the resource.
14 */
15 public function index(Request $request): Response
16 {
17 return Feature::when(NewApi::class,
18 fn () => $this->resolveNewApiResponse($request),
19 fn () => $this->resolveLegacyApiResponse($request),
20 );
21 }
22 
23 // ...
24}

unless方法充当方法的逆when,如果该功能处于非Events状态,则执行第一个闭包:

1return Feature::unless(NewApi::class,
2 fn () => $this->resolveLegacyApiResponse($request),
3 fn () => $this->resolveNewApiResponse($request),
4);

特质HasFeatures

Pennant 的HasFeatures特性可以添加到您的应用程序User模型(或任何其他具有特性的模型)中,以提供一种流畅、方便的方式来直接从模型检查特性:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Foundation\Auth\User as Authenticatable;
6use Laravel\Pennant\Concerns\HasFeatures;
7 
8class User extends Authenticatable
9{
10 use HasFeatures;
11 
12 // ...
13}

一旦将特征添加到模型中,您就可以通过调用该features方法轻松检查特性:

1if ($user->features()->active('new-api')) {
2 // ...
3}

当然,该features方法还提供了许多其他方便与功能交互的方法:

1// Values...
2$value = $user->features()->value('purchase-button')
3$values = $user->features()->values(['new-api', 'purchase-button']);
4 
5// State...
6$user->features()->active('new-api');
7$user->features()->allAreActive(['new-api', 'server-api']);
8$user->features()->someAreActive(['new-api', 'server-api']);
9 
10$user->features()->inactive('new-api');
11$user->features()->allAreInactive(['new-api', 'server-api']);
12$user->features()->someAreInactive(['new-api', 'server-api']);
13 
14// Conditional execution...
15$user->features()->when('new-api',
16 fn () => /* ... */,
17 fn () => /* ... */,
18);
19 
20$user->features()->unless('new-api',
21 fn () => /* ... */,
22 fn () => /* ... */,
23);

Blade 指令

为了使 Blade 中的检查功能成为一种无缝体验,Pennant 提供了@feature@featureany指令:

1@feature('site-redesign')
2 <!-- 'site-redesign' is active -->
3@else
4 <!-- 'site-redesign' is inactive -->
5@endfeature
6 
7@featureany(['site-redesign', 'beta'])
8 <!-- 'site-redesign' or `beta` is active -->
9@endfeatureany

中间件

Pennant 还包含一个中间件,可用于在调用路由之前验证当前已验证用户是否有权访问某个功能。您可以将该中间件分配给路由,并指定访问该路由所需的功能。如果任何指定的功能对于当前已验证用户处于非Events状态,则400 Bad Request路由将返回 HTTP 响应。可以将多个功能传递给静态using方法。

1use Illuminate\Support\Facades\Route;
2use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
3 
4Route::get('/api/servers', function () {
5 // ...
6})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));

自定义响应

如果您想自定义中间件在某个列出的功能处于非Events状态时返回的响应,可以使用中间件whenInactive提供的方法EnsureFeaturesAreActive。通常,此方法应该在boot应用程序的某个服务提供商的方法中调用:

1use Illuminate\Http\Request;
2use Illuminate\Http\Response;
3use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 EnsureFeaturesAreActive::whenInactive(
11 function (Request $request, array $features) {
12 return new Response(status: 403);
13 }
14 );
15 
16 // ...
17}

拦截功能检查

有时,在检索特定功能的存储值之前执行一些内存检查会很有用。假设您正在开发一个带有功能标志的新 API,并且希望能够在不丢失存储中任何已解析的功能值的情况下禁用该新 API。如果您发现新 API 中存在错误,您可以轻松地为除内部团队成员之外的所有人禁用该 API,修复错误,然后为之前有权访问该功能的用户重新启用新 API。

您可以使用基于类的功能 before方法来实现这一点。如果存在,该before方法始终在内存中运行,然后再从存储中检索值。如果null该方法返回非值,则在请求期间,它将代替功能存储的值:

1<?php
2 
3namespace App\Features;
4 
5use App\Models\User;
6use Illuminate\Support\Facades\Config;
7use Illuminate\Support\Lottery;
8 
9class NewApi
10{
11 /**
12 * Run an always-in-memory check before the stored value is retrieved.
13 */
14 public function before(User $user): mixed
15 {
16 if (Config::get('features.new-api.disabled')) {
17 return $user->isInternalTeamMember();
18 }
19 }
20 
21 /**
22 * Resolve the feature's initial value.
23 */
24 public function resolve(User $user): mixed
25 {
26 return match (true) {
27 $user->isInternalTeamMember() => true,
28 $user->isHighTrafficCustomer() => false,
29 default => Lottery::odds(1 / 100),
30 };
31 }
32}

您还可以使用此功能来安排以前处于功能标志之后的功能的全局推出:

1<?php
2 
3namespace App\Features;
4 
5use Illuminate\Support\Carbon;
6use Illuminate\Support\Facades\Config;
7 
8class NewApi
9{
10 /**
11 * Run an always-in-memory check before the stored value is retrieved.
12 */
13 public function before(User $user): mixed
14 {
15 if (Config::get('features.new-api.disabled')) {
16 return $user->isInternalTeamMember();
17 }
18 
19 if (Carbon::parse(Config::get('features.new-api.rollout-date'))->isPast()) {
20 return true;
21 }
22 }
23 
24 // ...
25}

内存缓存

检查要素时,Pennant 会在内存中创建结果缓存。如果您正在使用database驱动程序,这意味着在单个请求中重新检查相同的功能标志不会触发额外的数据库查询。这也确保了该功能在请求期间具有一致的结果。

如果需要手动刷新内存缓存,可以使用外观flushCache提供的方法Feature

1Feature::flushCache();

范围

指定范围

如上所述,功能通常会根据当前已认证的用户进行检查。然而,这可能并不总是适合您的需求。因此,您可以通过Feature外观层的for方法指定要检查特定功能的范围:

1return Feature::for($user)->active('new-api')
2 ? $this->resolveNewApiResponse($request)
3 : $this->resolveLegacyApiResponse($request);

当然,功能范围不仅限于“用户”。假设您构建了一个新的计费体验,并将其推广到整个团队,而不是单个用户。也许您希望最老的团队的推广速度比新团队慢。您的功能解决闭包可能如下所示:

1use App\Models\Team;
2use Carbon\Carbon;
3use Illuminate\Support\Lottery;
4use Laravel\Pennant\Feature;
5 
6Feature::define('billing-v2', function (Team $team) {
7 if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
8 return true;
9 }
10 
11 if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
12 return Lottery::odds(1 / 100);
13 }
14 
15 return Lottery::odds(1 / 1000);
16});

你会注意到,我们定义的闭包并不期望传入一个User,而是一个Team模型。要判断此功能是否对用户的团队有效,你应该将团队传递给外观层for提供的方法Feature

1if (Feature::for($user->team)->active('billing-v2')) {
2 return redirect('/billing/v2');
3}
4 
5// ...

默认范围

您还可以自定义 Pennant 用于检查功能的默认范围。例如,也许所有功能都针对当前已验证用户的团队进行检查,而不是针对用户本身。Feature::for($user->team)您不必每次检查功能时都调用此方法,而是可以将团队指定为默认范围。通常,这应该在您的应用程序的某个服务提供商中完成:

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\Facades\Auth;
6use Illuminate\Support\ServiceProvider;
7use Laravel\Pennant\Feature;
8 
9class AppServiceProvider extends ServiceProvider
10{
11 /**
12 * Bootstrap any application services.
13 */
14 public function boot(): void
15 {
16 Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
17 
18 // ...
19 }
20}

如果没有通过该方法明确提供范围for,则功能检查现在将使用当前经过身份验证的用户的团队作为默认范围:

1Feature::active('billing-v2');
2 
3// Is now equivalent to...
4 
5Feature::for($user->team)->active('billing-v2');

可空范围

如果您在检查功能时提供的范围是,null并且该功能的定义不支持null通过可空类型或包含null在联合类型中,则 Pennant 将自动返回false该功能的结果值。

因此,如果您传递给某个功能的作用域是潜在的,null并且您希望调用该功能的值解析器,则应该在功能定义中考虑到这一点。null如果您在 Artisan 命令、队列作业或未经身份验证的路由中检查某个功能,则可能会出现作用域。由于这些上下文中通常没有经过身份验证的用户,因此默认作用域将是null

如果您不总是明确指定功能范围,那么您应该确保范围的类型是“可空的”,并null在功能定义逻辑中处理范围值:

1use App\Models\User;
2use Illuminate\Support\Lottery;
3use Laravel\Pennant\Feature;
4 
5Feature::define('new-api', fn (User $user) => match (true) {
6Feature::define('new-api', fn (User|null $user) => match (true) {
7 $user === null => true,
8 $user->isInternalTeamMember() => true,
9 $user->isHighTrafficCustomer() => false,
10 default => Lottery::odds(1 / 100),
11});

确定范围

Pennant 的内置驱动程序arraydatabase存储驱动程序知道如何正确存储所有 PHP 数据类型以及 Eloquent 模型的作用域标识符。但是,如果您的应用程序使用第三方 Pennant 驱动程序,该驱动程序可能无法正确存储 Eloquent 模型或其他自定义类型的标识符。

鉴于此,Pennant 允许您通过FeatureScopeable在应用程序中用作 Pennant 范围的对象上实施契约来格式化存储的范围值。

例如,假设您在一个应用程序中使用两个不同的功能驱动程序:内置驱动database程序和第三方“Flag Rocket”驱动程序。“Flag Rocket”驱动程序不知道如何正确存储 Eloquent 模型。相反,它需要一个FlagRocketUser实例。通过实现契约toFeatureIdentifier中定义的FeatureScopeable,我们可以自定义提供给应用程序使用的每个驱动程序的可存储范围值:

1<?php
2 
3namespace App\Models;
4 
5use FlagRocket\FlagRocketUser;
6use Illuminate\Database\Eloquent\Model;
7use Laravel\Pennant\Contracts\FeatureScopeable;
8 
9class User extends Model implements FeatureScopeable
10{
11 /**
12 * Cast the object to a feature scope identifier for the given driver.
13 */
14 public function toFeatureIdentifier(string $driver): mixed
15 {
16 return match($driver) {
17 'database' => $this,
18 'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
19 };
20 }
21}

序列化范围

默认情况下,Pennant 在存储与 Eloquent 模型关联的功能时会使用完全限定的类名。如果您已使用Eloquent 变形图,则可以选择让 Pennant 也使用变形图,以便将存储的功能与应用程序结构解耦。

为了实现这一点,在服务提供商中定义 Eloquent 变形图后,您可以调用Feature外观的useMorphMap方法:

1use Illuminate\Database\Eloquent\Relations\Relation;
2use Laravel\Pennant\Feature;
3 
4Relation::enforceMorphMap([
5 'post' => 'App\Models\Post',
6 'video' => 'App\Models\Video',
7]);
8 
9Feature::useMorphMap();

丰富的功能值

到目前为止,我们主要将特征显示为二进制状态,这意味着它们要么是“Events的”,要么是“不Events的”,但 Pennant 还允许您存储丰富的值。

例如,假设您正在为应用程序的“立即购买”按钮测试三种新颜色。您可以返回一个字符串,而不是从功能定义中返回true或:false

1use Illuminate\Support\Arr;
2use Laravel\Pennant\Feature;
3 
4Feature::define('purchase-button', fn (User $user) => Arr::random([
5 'blue-sapphire',
6 'seafoam-green',
7 'tart-orange',
8]));

purchase-button您可以使用下列方法检索该功能的值value

1$color = Feature::value('purchase-button');

Pennant 包含的 Blade 指令还可以轻松地根据功能的当前值有条件地呈现内容:

1@feature('purchase-button', 'blue-sapphire')
2 <!-- 'blue-sapphire' is active -->
3@elsefeature('purchase-button', 'seafoam-green')
4 <!-- 'seafoam-green' is active -->
5@elsefeature('purchase-button', 'tart-orange')
6 <!-- 'tart-orange' is active -->
7@endfeature

当使用丰富的值时,重要的是要知道当一个功能具有除 之外的任何值时,该功能被视为“Events”的false

当调用条件when方法时,该特征的丰富值将提供给第一个闭包:

1Feature::when('purchase-button',
2 fn ($color) => /* ... */,
3 fn () => /* ... */,
4);

同样,当调用条件unless方法时,该特征的丰富值将提供给可选的第二个闭包:

1Feature::unless('purchase-button',
2 fn () => /* ... */,
3 fn ($color) => /* ... */,
4);

检索多个特征

values方法允许检索给定范围的多个特征:

1Feature::values(['billing-v2', 'purchase-button']);
2 
3// [
4// 'billing-v2' => false,
5// 'purchase-button' => 'blue-sapphire',
6// ]

或者,您可以使用该all方法检索给定范围的所有已定义特征的值:

1Feature::all();
2 
3// [
4// 'billing-v2' => false,
5// 'purchase-button' => 'blue-sapphire',
6// 'site-redesign' => true,
7// ]

然而,基于类的功能是动态注册的,只有在明确检查后才会被 Pennant 识别。这意味着,all如果您应用程序的基于类的功能在当前请求期间尚未被检查,则它们可能不会出现在该方法返回的结果中。

如果您希望确保在使用该all方法时始终包含要素类,可以使用 Pennant 的要素发现功能。首先,请discover在您的应用程序的某个服务提供者中调用该方法:

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\ServiceProvider;
6use Laravel\Pennant\Feature;
7 
8class AppServiceProvider extends ServiceProvider
9{
10 /**
11 * Bootstrap any application services.
12 */
13 public function boot(): void
14 {
15 Feature::discover();
16 
17 // ...
18 }
19}

discover方法将注册应用程序目录中的所有要素类app/Featuresall无论在当前请求中是否已检查过这些类,该方法现在都会将其包含在结果中:

1Feature::all();
2 
3// [
4// 'App\Features\NewApi' => true,
5// 'billing-v2' => false,
6// 'purchase-button' => 'blue-sapphire',
7// 'site-redesign' => true,
8// ]

预先加载

尽管 Pennant 会在内存中缓存单个请求中所有已解析的特征,但仍可能遇到性能问题。为了缓解这个问题,Pennant 提供了预先加载特征值的功能。

为了说明这一点,假设我们正在检查循环中某个功能是否处于Events状态:

1use Laravel\Pennant\Feature;
2 
3foreach ($users as $user) {
4 if (Feature::for($user)->active('notifications-beta')) {
5 $user->notify(new RegistrationSuccess);
6 }
7}

假设我们使用数据库驱动程序,这段代码将在循环中为每个用户执行一次数据库查询——可能会执行数百个查询。但是,使用 Pennant 的load方法,我们可以通过预先加载一组用户或范围的特征值来消除这个潜在的性能瓶颈:

1Feature::for($users)->load(['notifications-beta']);
2 
3foreach ($users as $user) {
4 if (Feature::for($user)->active('notifications-beta')) {
5 $user->notify(new RegistrationSuccess);
6 }
7}

要仅在尚未加载时加载特征值,可以使用该loadMissing方法:

1Feature::for($users)->loadMissing([
2 'new-api',
3 'purchase-button',
4 'notifications-beta',
5]);

您可以使用该方法加载所有定义的功能loadAll

1Feature::for($users)->loadAll();

更新值

首次解析功能值时,底层驱动程序会将结果存储在存储中。这通常是必要的,以确保用户在各个请求中获得一致的体验。但是,有时您可能需要手动更新功能的存储值。

为了实现这一点,您可以使用activatedeactivate方法来切换功能“开”或“关”:

1use Laravel\Pennant\Feature;
2 
3// Activate the feature for the default scope...
4Feature::activate('new-api');
5 
6// Deactivate the feature for the given scope...
7Feature::for($user->team)->deactivate('billing-v2');

还可以通过向activate方法提供第二个参数来手动设置功能的丰富值:

1Feature::activate('purchase-button', 'seafoam-green');

要指示 Pennant 忘记某个特征的存储值,可以使用该forget方法。当再次检查该特征时,Pennant 将从其特征定义中解析该特征的值:

1Feature::forget('purchase-button');

批量更新

要批量更新存储的特征值,您可以使用activateForEveryonedeactivateForEveryone方法。

例如,假设您现在对该功能的稳定性充满信心new-api,并且已经为结帐Processes找到了最佳'purchase-button'颜色 - 您可以相应地更新所有用户的存储值:

1use Laravel\Pennant\Feature;
2 
3Feature::activateForEveryone('new-api');
4 
5Feature::activateForEveryone('purchase-button', 'seafoam-green');

或者,您可以为所有用户停用该功能:

1Feature::deactivateForEveryone('new-api');

这只会更新 Pennant 存储驱动程序已存储的已解析特征值。您还需要更新应用程序中的特征定义。

清除功能

有时,从存储中清除整个功能可能会很有用。如果您已从应用程序中删除该功能,或者您对该功能的定义进行了调整并希望将其推广给所有用户,则通常需要执行此操作。

您可以使用下列方法删除某个功能的所有存储值purge

1// Purging a single feature...
2Feature::purge('new-api');
3 
4// Purging multiple features...
5Feature::purge(['new-api', 'purchase-button']);

如果您想要从存储中清除所有purge特征,您可以调用不带任何参数的方法:

1Feature::purge();

由于清除功能作为应用程序部署Pipeline的一部分非常有用,因此 Pennant 包含一个pennant:purgeArtisan 命令,该命令将从存储中清除所提供的功能:

1php artisan pennant:purge new-api
2 
3php artisan pennant:purge new-api purchase-button

您还可以清除除给定功能列表中的功能之外的所有功能。例如,假设您想清除所有功能,但保留“new-api”和“purchase-button”功能的值。为此,您可以将这些功能名称传递给以下--except选项:

1php artisan pennant:purge --except=new-api --except=purchase-button

为了方便起见,该pennant:purge命令还支持一个--except-registered标志。此标志表示应清除除服务提供商中明确注册的功能之外的所有功能:

1php artisan pennant:purge --except-registered

测试

在测试与功能标志交互的代码时,控制功能标志返回值的最简单方法就是重新定义该功能。例如,假设您在应用程序的某个服务提供商中定义了以下功能:

1use Illuminate\Support\Arr;
2use Laravel\Pennant\Feature;
3 
4Feature::define('purchase-button', fn () => Arr::random([
5 'blue-sapphire',
6 'seafoam-green',
7 'tart-orange',
8]));

要在测试中修改功能的返回值,您可以在测试开始时重新定义该功能。即使Arr::random()服务提供者中仍然存在该实现,以下测试也始终会通过:

1use Laravel\Pennant\Feature;
2 
3test('it can control feature values', function () {
4 Feature::define('purchase-button', 'seafoam-green');
5 
6 expect(Feature::value('purchase-button'))->toBe('seafoam-green');
7});
1use Laravel\Pennant\Feature;
2 
3public function test_it_can_control_feature_values()
4{
5 Feature::define('purchase-button', 'seafoam-green');
6 
7 $this->assertSame('seafoam-green', Feature::value('purchase-button'));
8}

同样的方法也适用于基于类的特征:

1use Laravel\Pennant\Feature;
2 
3test('it can control feature values', function () {
4 Feature::define(NewApi::class, true);
5 
6 expect(Feature::value(NewApi::class))->toBeTrue();
7});
1use App\Features\NewApi;
2use Laravel\Pennant\Feature;
3 
4public function test_it_can_control_feature_values()
5{
6 Feature::define(NewApi::class, true);
7 
8 $this->assertTrue(Feature::value(NewApi::class));
9}

如果您的功能是返回一个Lottery实例,那么有一些有用的测试Helpers可用

商店配置

PENNANT_STORE您可以通过在应用程序文件中定义环境变量来配置 Pennant 在测试期间将使用的存储phpunit.xml

1<?xml version="1.0" encoding="UTF-8"?>
2<phpunit colors="true">
3 <!-- ... -->
4 <php>
5 <env name="PENNANT_STORE" value="array"/>
6 <!-- ... -->
7 </php>
8</phpunit>

添加自定义Pennant驱动程序

实现驱动程序

如果 Pennant 现有的存储驱动程序都不符合您的应用需求,您可以编写自己的存储驱动程序。您的自定义驱动程序应实现以下Laravel\Pennant\Contracts\Driver接口:

1<?php
2 
3namespace App\Extensions;
4 
5use Laravel\Pennant\Contracts\Driver;
6 
7class RedisFeatureDriver implements Driver
8{
9 public function define(string $feature, callable $resolver): void {}
10 public function defined(): array {}
11 public function getAll(array $features): array {}
12 public function get(string $feature, mixed $scope): mixed {}
13 public function set(string $feature, mixed $scope, mixed $value): void {}
14 public function setForAllScopes(string $feature, mixed $value): void {}
15 public function delete(string $feature, mixed $scope): void {}
16 public function purge(array|null $features): void {}
17}

现在,我们只需要使用 Redis 连接来实现这些方法。有关如何实现这些方法的示例,请查看Laravel\Pennant\Drivers\DatabaseDriverPennant源代码。

Laravel 本身没有自带扩展目录。你可以随意将它们放置在任何你喜欢的位置。在本例中,我们创建了一个Extensions目录来存放RedisFeatureDriver.

注册驱动程序

一旦你的驱动程序实现完毕,你就可以将其注册到 Laravel 中了。要向 Pennant 添加其他驱动程序,你可以使用Facadeextend提供的方法。你应该应用程序的某个服务提供者Feature中调用该方法extendboot

1<?php
2 
3namespace App\Providers;
4 
5use App\Extensions\RedisFeatureDriver;
6use Illuminate\Contracts\Foundation\Application;
7use Illuminate\Support\ServiceProvider;
8use Laravel\Pennant\Feature;
9 
10class AppServiceProvider extends ServiceProvider
11{
12 /**
13 * Register any application services.
14 */
15 public function register(): void
16 {
17 // ...
18 }
19 
20 /**
21 * Bootstrap any application services.
22 */
23 public function boot(): void
24 {
25 Feature::extend('redis', function (Application $app) {
26 return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
27 });
28 }
29}

一旦驱动程序注册完毕,您就可以redis在应用程序的config/pennant.php配置文件中使用该驱动程序:

1'stores' => [
2 
3 'redis' => [
4 'driver' => 'redis',
5 'connection' => null,
6 ],
7 
8 // ...
9 
10],

外部定义特征

如果您的驱动程序是第三方功能标志平台的包装器,您很可能会在该平台上定义功能,而不是使用 Pennant 的Feature::define方法。在这种情况下,您的自定义驱动程序也应该实现以下Laravel\Pennant\Contracts\DefinesFeaturesExternally接口:

1<?php
2 
3namespace App\Extensions;
4 
5use Laravel\Pennant\Contracts\Driver;
6use Laravel\Pennant\Contracts\DefinesFeaturesExternally;
7 
8class FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally
9{
10 /**
11 * Get the features defined for the given scope.
12 */
13 public function definedFeaturesForScope(mixed $scope): array {}
14 
15 /* ... */
16}

definedFeaturesForScope方法应该返回为提供的范围定义的功能名称列表。

Events

Pennant 会调度各种事件,这些事件在整个应用程序中跟踪功能标志时非常有用。

Laravel\Pennant\Events\FeatureRetrieved

每当选中某个功能时,都会调度此事件。此事件可能有助于在整个应用程序中创建和跟踪针对功能标志使用情况的指标。

Laravel\Pennant\Events\FeatureResolved

首次针对特定范围解析特征值时,将分派此事件。

Laravel\Pennant\Events\UnknownFeatureResolved

首次解析特定范围的未知功能时,会调度此事件。如果您打算移除某个功能标志,但意外地在整个应用程序中留下了对它的引用,那么监听此事件可能会很有用:

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\ServiceProvider;
6use Illuminate\Support\Facades\Event;
7use Illuminate\Support\Facades\Log;
8use Laravel\Pennant\Events\UnknownFeatureResolved;
9 
10class AppServiceProvider extends ServiceProvider
11{
12 /**
13 * Bootstrap any application services.
14 */
15 public function boot(): void
16 {
17 Event::listen(function (UnknownFeatureResolved $event) {
18 Log::error("Resolving unknown feature [{$event->feature}].");
19 });
20 }
21}

Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass

在请求期间第一次动态检查基于类的功能时,会分派此事件。

Laravel\Pennant\Events\UnexpectedNullScopeEncountered

null当范围传递给不支持 null的功能定义时,将调度此事件

这种情况会被妥善处理,该功能将恢复正常false。但是,如果您不想使用此功能的默认正常行为,您可以在boot应用程序的 方法中注册此事件的监听器AppServiceProvider

1use Illuminate\Support\Facades\Log;
2use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));
10}

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

清除所有功能时会分派此事件。