跳转至内容

Laravel Pennant

简介

Laravel Pennant 是一个简洁、轻量级的功能标志(feature flag)包——没有任何冗余。功能标志使您能够自信地逐步推出新的应用程序功能、对新界面设计进行 A/B 测试、辅助基于主干的开发策略等等。

安装

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

1composer require laravel/pennant

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

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

最后,您应该运行应用程序的数据库迁移。这将创建一个 features 表,Pennant 使用该表来为其 database 驱动程序提供支持。

1php artisan migrate

配置

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

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

定义功能(Features)

要定义一个功能,您可以使用 Feature 门面(facade)提供的 define 方法。您需要提供功能的名称,以及一个将被调用以解析功能初始值的闭包。

通常,功能是在服务提供者中使用 Feature 门面定义的。闭包将接收功能检查的“作用域”。最常见的是,作用域是当前经过身份验证的用户。在此示例中,我们将定义一个用于逐步向应用程序用户推出新 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:feature Artisan 命令。默认情况下,功能类将放置在应用程序的 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}

如果您想手动解析基于类的功能实例,可以在 Feature 门面上调用 instance 方法:

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

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

自定义存储的功能名称

默认情况下,Pennant 将存储功能类的完全限定类名。如果您希望将存储的功能名称与应用程序的内部结构解耦,可以在功能类上添加 Name 属性。此属性的值将代替类名进行存储:

1<?php
2 
3namespace App\Features;
4 
5use Laravel\Pennant\Attributes\Name;
6 
7#[Name('new-api')]
8class NewApi
9{
10 // ...
11}

检查功能

要确定某个功能是否处于活动状态,您可以使用 Feature 门面上的 active 方法。默认情况下,将针对当前经过身份验证的用户检查功能:

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}

虽然默认情况下会针对当前经过身份验证的用户检查功能,但您可以轻松地针对其他用户或 作用域 检查该功能。为此,请使用 Feature 门面提供的 for 方法:

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

Pennant 还提供了一些在确定功能是否活动时可能有用的额外便捷方法:

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 命令或排队作业中)时,通常应该 显式指定功能的作用域。或者,您可以定义一个 默认作用域,同时考虑到经过身份验证的 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}

条件执行

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

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 方法的逆向操作,在功能处于非活动状态时执行第一个闭包:

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

HasFeatures Trait

Pennant 的 HasFeatures trait 可以添加到您应用程序的 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}

将 trait 添加到模型后,您可以通过调用 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 还包含一个 中间件,可用于在调用路由之前验证当前经过身份验证的用户是否有权访问某个功能。您可以将中间件分配给路由并指定访问该路由所需的功能。如果指定的功能对于当前经过身份验证的用户处于非活动状态,则路由将返回 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'));

自定义响应

如果您想自定义当列出的功能之一处于非活动状态时中间件返回的响应,可以使用 EnsureFeaturesAreActive 中间件提供的 whenInactive 方法。通常,此方法应在应用程序服务提供者的 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。

您可以使用 基于类的功能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 驱动程序,这意味着在单个请求中重新检查同一个功能标志不会触发额外的数据库查询。这也确保了功能在请求期间具有一致的结果。

如果您需要手动刷新内存缓存,可以使用 Feature 门面提供的 flushCache 方法:

1Feature::flushCache();

作用域(Scope)

指定作用域

如前所述,功能通常针对当前经过身份验证的用户进行检查。但是,这可能并不总是适合您的需求。因此,可以通过 Feature 门面的 for 方法来指定您要针对其检查给定功能的作用域:

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

当然,功能作用域并不局限于“用户”。想象一下,您构建了一个新的计费体验,您正在将其推出到整个团队,而不是个人用户。也许您希望最老的团队比新团队推出的速度更慢。您的功能解析闭包可能如下所示:

1use App\Models\Team;
2use Illuminate\Support\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 模型。要确定此功能对于用户的团队是否处于活动状态,应将该团队传递给 Feature 门面提供的 for 方法:

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,并且功能定义不支持通过可为空类型或在联合类型中包含 nullnull,Pennant 将自动返回 false 作为功能的结果值。

因此,如果您传递给功能的作用域可能是 null,并且您希望调用该功能的值解析器,则应该在功能定义中考虑这一点。如果您在 Artisan 命令、排队作业或未经身份验证的路由中检查功能,可能会出现 null 作用域。由于在这些上下文中通常没有经过身份验证的用户,因此默认作用域将为 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 允许您通过在应用程序中用作 Pennant 作用域的对象上实现 FeatureScopeable 契约(contract)来格式化存储的作用域值。

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

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();

富功能值(Rich Feature Values)

到目前为止,我们主要展示了处于二进制状态的功能,即它们要么是“活动的”,要么是“非活动的”,但 Pennant 也允许您存储富值。

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

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]));

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

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

使用富值时,重要的是要知道当功能具有除 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/Features 目录中的所有功能类。无论这些类在当前请求期间是否已被检查,all 方法现在都将把它们包含在结果中。

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 提供了预加载功能值的能力。

为了说明这一点,想象一下我们正在循环中检查功能是否处于活动状态:

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 功能的稳定性充满信心,并确定了结账流程的最佳 '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();

由于在应用程序的部署流水线中清除功能很有用,Pennant 包含了一个 pennant:purge Artisan 命令,它将从存储中清除提供的功能:

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 实例,那么有一些有用的 测试助手可用

存储配置

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

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 连接来实现这些方法中的每一个。有关如何实现这些方法中的每一个的示例,请查看 Pennant 源代码 中的 Laravel\Pennant\Drivers\DatabaseDriver

Laravel 没有附带用于包含扩展的目录。您可以随意将其放置在任何地方。在此示例中,我们创建了一个 Extensions 目录来存放 RedisFeatureDriver

注册驱动

驱动程序实现后,您就可以将其注册到 Laravel 了。要向 Pennant 添加额外的驱动程序,可以使用 Feature 门面提供的 extend 方法。您应该从应用程序的 服务提供者boot 方法中调用 extend 方法:

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}

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

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 方法应返回为所提供作用域定义的功能名称列表。

活动

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。但是,如果您想选择退出此功能的默认优雅行为,可以在应用程序 AppServiceProviderboot 方法中为此事件注册监听器:

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

当更新作用域的功能时(通常通过调用 activatedeactivate),就会分发此事件。

Laravel\Pennant\Events\FeatureUpdatedForAllScopes

当更新所有作用域的功能时(通常通过调用 activateForEveryonedeactivateForEveryone),就会分发此事件。

Laravel\Pennant\Events\FeatureDeleted

当删除作用域的功能时(通常通过调用 forget),就会分发此事件。

Laravel\Pennant\Events\FeaturesPurged

当清除特定功能时,就会分发此事件。

Laravel\Pennant\Events\AllFeaturesPurged

当清除所有功能时,就会分发此事件。