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),
});
}
}如您所见,我们有针对我们功能的以下规则:
首次为给定用户检查 new-api 功能时,闭包的结果将由存储驱动程序存储。下次针对同一用户检查该功能时,将从存储中检索该值,并且不会调用闭包。
为方便起见,如果一个特性定义只返回一个彩票,您可以完全省略闭包:
Feature::定义('site-redesign', Lottery::几率(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);[!NOTE]
特性类通过 容器 解析,因此,您可以在需要时将依赖项注入特性类的构造函数。
默认情况下,Pennant 将存储特性类的完全限定类名。如果您想将存储的特性名称与应用程序的内部结构解耦,可以在特性类上指定一个 $name 属性。此属性的值将取代类名进行存储:
<?php
namespace App\Features;
class NewApi
{
/**
* The stored name of the feature.
*
* @var string
*/
public $name = 'new-api';
// ...
}要确定某个功能是否激活,您可以使用 active 方法,位于 Feature 门面。默认情况下,功能会根据当前已认证的用户进行检查:
<?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']);[!注意]
当在 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;
// ...
}一旦该 trait 被添加到您的模型中,您可以通过调用 features 方法轻松检查特性:
if ($user->features()->active('new-api')) {
// ...
}当然,features 方法提供了许多其他便捷方法来与特性进行交互:
// Values...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);
// State...
$user->features()->active('new-api');
$user->features()->allAreActive(['new-api', 'server-api']);
$user->features()->someAreActive(['new-api', 'server-api']);
$user->features()->inactive('new-api');
$user->features()->allAreInactive(['new-api', 'server-api']);
$user->features()->someAreInactive(['new-api', 'server-api']);
// Conditional execution...
$user->features()->when('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
$user->features()->unless('new-api',
fn () => /* ... */,
fn () => /* ... */,
);为了使在 Blade 中检查特性成为一种无缝体验,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 -->
@endfeatureanyPennant 还包含一个 中间件 该中间件可用于在路由被调用之前,验证当前已认证的用户是否具有某个功能的访问权限。你可以将该中间件分配给某个路由,并指定访问该路由所需的功能。如果当前已认证用户的任何指定功能处于非活动状态,则路由将返回一个 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'));如果您想自定义当中列出的某个功能处于非活动状态时,中间件返回的响应,您可以使用 whenInactive 方法,该方法由 EnsureFeaturesAreActive 中间件提供。通常,此方法应在您的应用程序的某个服务提供者的 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);
}
);
// ...
}有时,在检索某个给定功能的存储值之前,执行一些内存中的检查可能会很有用。想象你正在开发一个由功能标志(feature flag)控制的新 API,并希望在不丢失存储中任何已解析功能值的情况下,能够禁用该新 API。如果你在新 API 中发现了一个 bug,你可以轻松地为除了内部团队成员之外的所有人禁用它,修复这个 bug,然后为那些之前有权访问此功能的用户重新启用这个新 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 facade 的 for 方法来指定你希望检查特定功能的范围:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);当然,功能范围不限于“用户”。想象你构建了一个新的计费体验,你正在向整个团队而不是独立用户推出。也许你希望最老的团队有一个比新的团队更慢的推出速度。你的功能解析闭包可能看起来像下面这样:
use App\Models\Team;
use Illuminate\Support\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
Feature::define('billing-v2', function (Team $team) {
if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
return true;
}
if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
return Lottery::odds(1 / 100);
}
return Lottery::odds(1 / 1000);
});您会注意到,我们定义的闭包没有预期 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,并且该功能的定义不支持通过可空类型或在联合类型中包含 null 来支持 null,Pennant 将自动返回 false 作为该功能的结果值。
因此,如果传递给某个功能的范围可能是 null 并且你希望调用该功能的值解析器,你应该在你的功能定义中考虑到这一点。当你在 Artisan 命令、队列任务或未经身份验证的路由中检查某个功能时,可能会出现 null 范围。由于在这些上下文中通常没有经过身份验证的用户,默认范围将是 null。
如果您并非总是显式指定您的功能范围那么您应该确保范围的类型是“可空”的并处理null范围值在您的功能定义逻辑中:
use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
Feature::define('new-api', fn (User $user) => match (true) {// [tl! remove]
Feature::define('new-api', fn (User|null $user) => match (true) {// [tl! add]
$user === null => true,// [tl! add]
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});Pennant 内置的 array 和 database 存储驱动器知道如何正确存储所有 PHP 数据类型的范围标识符以及 Eloquent 模型。但是,如果您的应用使用第三方 Pennant 驱动器,该驱动器可能不知道如何正确存储您的应用中 Eloquent 模型或其他自定义类型的标识符。
鉴于此,Pennant 允许您通过在应用程序中用作 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 Facade 的 useMorphMap 方法:
use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
Feature::useMorphMap();迄今为止,我们主要展示了功能处于二元状态,意味着它们是“激活”或“非激活”的,但 Pennant 也允许您存储丰富的值。
例如,假设你正在为你的应用程序的“立即购买”按钮测试三种新颜色。你可以不从功能定义中返回 true 或 false,而是返回一个字符串:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn (User $user) => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));您可以使用 value 方法获取 purchase-button 特性的值:
$color = Feature::value('purchase-button');Pennant 附带的 Blade 指令也使得根据功能的当前值有条件地渲染内容变得容易:
@feature('purchase-button', 'blue-sapphire')
<!-- 'blue-sapphire' is active -->
@elsefeature('purchase-button', 'seafoam-green')
<!-- 'seafoam-green' is active -->
@elsefeature('purchase-button', 'tart-orange')
<!-- 'tart-orange' is active -->
@endfeature[!NOTE]
当使用富值时,需要注意的是,当一个功能的值不是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($users)->loadAll();当功能的值首次被解析时,底层驱动程序会将结果存储起来。这通常是为了确保你的用户在跨请求时获得一致的体验。然而,有时你可能希望手动更新该功能的存储值。
为实现此目的,您可以使用 activate 和 deactivate 方法来切换某个功能的 "开启" 或 "关闭":
use Laravel\Pennant\Feature;
// Activate the feature for the default scope...
Feature::activate('new-api');
// Deactivate the feature for the given scope...
Feature::for($user->team)->deactivate('billing-v2');还可以通过向 activate 方法提供第二个参数,手动为一个特性设置一个丰富的值:
Feature::activate('purchase-button', 'seafoam-green');要指示 Pennant 忘记某个功能的存储值,您可以使用 forget 方法。当再次检查该功能时,Pennant 将从其功能定义中解析该功能的值:
Feature::forget('purchase-button');要批量更新存储的功能值, 您可以使用 activateForEveryone 和 deactivateForEveryone 方法.
例如,假设您现在对 new-api 功能的稳定性充满信心,并已为您的结账流程确定了最佳的 'purchase-button' 颜色 - 您可以相应地更新所有用户的存储值:
use Laravel\Pennant\Feature;
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');另外,您也可以为所有用户停用此功能:
Feature::deactivateForEveryone('new-api');[!NOTE]
这只会更新由 Pennant 的存储驱动程序存储的已解析的功能值。您还需要更新应用程序中的功能定义。
有时,清除存储中的整个功能会很有用。这通常在您已从应用程序中删除了该功能,或者您已对该功能的定义进行了调整并希望将其推广给所有用户时是必要的。
您可以使用 purge 方法清除某个功能的所有存储值:
// Purging a single feature...
Feature::purge('new-api');
// Purging multiple features...
Feature::purge(['new-api', 'purchase-button']);如果您想从存储中清除所有功能,您可以调用 purge 方法,无需任何参数:
Feature::purge();鉴于在您的应用程序部署管道中清除特性可能很有用,Pennant 包含一个 pennant:purge Artisan 命令,该命令将从存储中清除提供的特性:
php artisan pennant:purge new-api
php artisan pennant:purge new-api purchase-button也可以清除所有功能,除了给定功能列表中的功能。例如,假设您想清除所有功能,但保留存储中"new-api"和"purchase-button"功能的值。为此,您可以将这些功能名称传递给 --except 选项:
php artisan pennant:purge --except=new-api --except=purchase-button为方便起见,pennant:purge 命令还支持一个 --except-registered 标志。此标志表示除了在服务提供商中明确注册的特性外,所有特性都应被清除:
php artisan pennant:purge --except-registered在测试与功能标志交互的代码时,在测试中控制功能标志的返回值最简单的方法是直接重新定义该功能。例如,假设您的应用程序某个服务提供者中定义了以下功能:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn () => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));为了在测试中修改该特性返回的值,你可以在测试开始时重新定义该特性。以下测试将始终通过,即使 Arr::random() 的实现仍然存在于服务提供者中:
use Laravel\Pennant\Feature;
test('it can control feature values', function () {
Feature::define('purchase-button', 'seafoam-green');
expect(Feature::value('purchase-button'))->toBe('seafoam-green');
});use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define('purchase-button', 'seafoam-green');
$this->assertSame('seafoam-green', Feature::value('purchase-button'));
}同样的方法可用于基于类的特性:
use Laravel\Pennant\Feature;
test('it can control feature values', function () {
Feature::define(NewApi::class, true);
expect(Feature::value(NewApi::class))->toBeTrue();
});use App\Features\NewApi;
use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define(NewApi::class, true);
$this->assertTrue(Feature::value(NewApi::class));
}如果你的功能返回一个 Lottery 实例,则有一些有用的 测试辅助工具可用。
您可以通过在您的应用程序的 phpunit.xml 文件中定义 PENNANT_STORE 环境变量来配置 Pennant 在测试期间将使用的存储:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<!-- ... -->
<php>
<env name="PENNANT_STORE" value="array"/>
<!-- ... -->
</php>
</phpunit>如果 Pennant 现有的存储驱动都不符合您的应用程序需求,您可以编写自己的存储驱动。您的自定义驱动应该实现 Laravel\Pennant\Contracts\Driver 接口:
<?php
namespace App\Extensions;
use Laravel\Pennant\Contracts\Driver;
class RedisFeatureDriver implements Driver
{
public function define(string $feature, callable $resolver): void {}
public function defined(): array {}
public function getAll(array $features): array {}
public function get(string $feature, mixed $scope): mixed {}
public function set(string $feature, mixed $scope, mixed $value): void {}
public function setForAllScopes(string $feature, mixed $value): void {}
public function delete(string $feature, mixed $scope): void {}
public function purge(array|null $features): void {}
}现在,我们只需使用 Redis 连接实现这些方法。要了解如何实现这些方法的示例,请查看 Pennant 源代码 中的 Laravel\Pennant\Drivers\DatabaseDriver。
[!注意]
Laravel 不自带用于存放扩展的目录。
你可以自由地将它们放置在任何你喜欢的地方。
在这个例子中,我们创建了一个Extensions目录来存放RedisFeatureDriver。
一旦你的驱动已实现,你就可以将其注册到 Laravel。要向 Pennant 添加额外的驱动,你可以使用 Feature facade 提供的 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 的 Feature::define 方法。如果是这样,你的自定义驱动程序还应该实现 Laravel\Pennant\Contracts\DefinesFeaturesExternally 接口:
<?php
namespace App\Extensions;
use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Contracts\DefinesFeaturesExternally;
class FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally
{
/**
* Get the features defined for the given scope.
*/
public function definedFeaturesForScope(mixed $scope): array {}
/* ... */
}definedFeaturesForScope 方法应返回为所提供的范围定义的特性名称列表。
Pennant 会分发在贯穿你的应用程序跟踪功能标志时可能很有用的多种事件。
Laravel\Pennant\Events\特性已获取此事件在每当 功能被检查时 都会被分派。此事件可用于创建和跟踪应用程序中功能标志的使用情况指标。
Laravel\Pennant\事件\特性已解析此事件在首次为特定范围解析某个特性值时被分派。
Laravel\Pennant\事件\未知特性已解析此事件在首次为特定范围解析未知特性时被分派。监听此事件可能会很有用,如果你本打算移除一个功能标志,却意外地在你的应用程序中留下了对其的零散引用:
<?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\事件\动态注册特性类此事件在 基于类的功能 在请求期间首次被动态检查时被分派。
Laravel\Pennant\Events\意外的空作用域遇到此事件在 null 作用域被传递给一个不支持 null 的特性定义时被派发。
这种情况会得到优雅处理,并且该功能将返回 false。但是,如果你希望选择退出此功能的默认优雅行为,你可以在应用的 AppServiceProvider 的 boot 方法中注册此事件的监听器:
use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));
}Laravel\Pennant\Events\特性更新此事件在为某个作用域更新特性时被分派,通常通过调用 activate 或 deactivate。
Laravel\Pennant\事件\特性已针对所有作用域更新此事件在更新针对所有范围的功能时被分派,通常通过调用 activateForEveryone 或 deactivateForEveryone。
Laravel\Pennant\事件\特性已删除当为一个范围删除一个特性时,此事件会被派发,通常通过调用 forget。
此事件在清除特定功能时被派发。
Laravel\Pennant\事件\所有特性已清除此事件在清除所有功能时被派发。