跳至内容

Laravel Pennant

简介

Laravel Pennant 是一个简单轻量级的特性标记包 - 没有多余的功能。特性标记使您能够自信地逐步推出新的应用程序特性,对新的界面设计进行 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 驱动程序在内存数组中存储已解析的特性标记值。或者,Pennant 可以通过 database 驱动程序将已解析的特性标记值持久存储在关系数据库中,这是 Pennant 使用的默认存储机制。

定义特性

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

通常,特性是在服务提供者中使用 Feature 门面定义的。闭包将接收特性检查的“作用域”。最常见的是,作用域是当前已认证的用户。在本例中,我们将为逐步向应用程序用户推出新 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 特性时,闭包的结果将由存储驱动程序存储。下次针对同一用户检查特性时,将从存储中检索该值,并且不会调用闭包。

为方便起见,如果特性定义只返回一个彩票,您可以完全省略闭包

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 门面上调用 instance 方法

use Illuminate\Support\Facades\Feature;
 
$instance = Feature::instance(NewApi::class);
lightbulb

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

自定义存储的特性名称

默认情况下,Pennant 将存储特性类的完全限定类名。如果您想将存储的特性名称与应用程序的内部结构分离,可以在特性类上指定一个 $name 属性。此属性的值将存储在类名的位置

<?php
 
namespace App\Features;
 
class NewApi
{
/**
* The stored name of the feature.
*
* @var string
*/
public $name = 'new-api';
 
// ...
}

检查特性

要确定特性是否处于活动状态,可以在 Feature 门面上使用 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 门面提供的 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']);
lightbulb

在 HTTP 上下文之外使用 Pennant 时,例如在 Artisan 命令或排队作业中,您通常应该 显式指定特性的作用域。或者,您可以定义一个 默认作用域,该作用域同时考虑已认证的 HTTP 上下文和未认证的上下文。

检查基于类的特性

对于基于类的特性,您应该在检查特性时提供类名

<?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 特性

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

<?php
 
namespace App\Models;
 
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;
 
class User extends Authenticatable
{
use HasFeatures;
 
// ...
}

将特性添加到模型后,您可以通过调用 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 中发现错误,您可以轻松地为除内部团队成员以外的所有用户禁用它,修复错误,然后为以前有权访问该特性的用户重新启用该 API。

您可以使用 基于类的特性before 方法实现此目的。如果存在,则 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 门面提供的 flushCache 方法。

Feature::flushCache();

作用域

指定作用域

如上所述,功能通常会针对当前已认证的用户进行检查。但是,这可能并不总是适合您的需求。因此,可以通过 Feature 门面的 for 方法指定您希望针对其检查给定功能的范围。

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

当然,功能范围不仅限于“用户”。假设您构建了一个新的账单体验,您正在将其推广到整个团队而不是单个用户。也许您希望较旧的团队比较新的团队拥有更慢的推广速度。您的功能解析闭包可能如下所示

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 门面提供的 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,并且功能的定义不支持通过可空类型或在联合类型中包含 nullnull,则 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) {
Feature::define('new-api', fn (User|null $user) => match (true) {
$user === null => true,
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});

识别作用域

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

鉴于此,Pennant 允许您通过在应用程序中用作 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),
};
}
}

序列化作用域

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

为此,在服务提供程序中定义 Eloquent 形态映射后,您可以调用 Feature 门面的 useMorphMap 方法。

use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;
 
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
 
Feature::useMorphMap();

丰富的特性值

到目前为止,我们主要将功能显示为二进制状态,这意味着它们要么“活动”,要么“非活动”,但 Pennant 也允许您存储丰富的值。

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

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 -->
@endfeature
lightbulb

使用丰富的值时,重要的是要知道当功能具有除 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);
}
}

假设我们使用的是数据库驱动程序,此代码将为循环中的每个用户执行数据库查询——可能执行数百个查询。但是,使用 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($user)->loadAll();

更新值

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

为此,您可以使用 activatedeactivate 方法来切换功能的“开启”或“关闭”。

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

批量更新

要批量更新存储的功能值,可以使用 activateForEveryonedeactivateForEveryone 方法。

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

use Laravel\Pennant\Feature;
 
Feature::activateForEveryone('new-api');
 
Feature::activateForEveryone('purchase-button', 'seafoam-green');

或者,您可以停用所有用户的此功能。

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

这只会更新 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

测试

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

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 驱动程序

实现驱动程序

如果 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

lightbulb

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

注册驱动程序

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

<?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 分派各种事件,这些事件在整个应用程序中跟踪功能标志时非常有用。

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

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

更新范围的功能时(通常通过调用activatedeactivate)会分发此事件。

Laravel\Pennant\Events\FeatureUpdatedForAllScopes

更新所有范围的功能时(通常通过调用activateForEveryonedeactivateForEveryone)会分发此事件。

Laravel\Pennant\Events\FeatureDeleted

删除范围的功能时(通常通过调用forget)会分发此事件。

Laravel\Pennant\Events\FeaturesPurged

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

Laravel\Pennant\Events\AllFeaturesPurged

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