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);
特性类通过 容器 解析,因此您可以在需要时将依赖项注入特性类的构造函数中。
自定义存储的特性名称
默认情况下,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']);
在 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
,并且功能的定义不支持通过可空类型或在联合类型中包含 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) {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 内置的 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
门面的 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
使用丰富的值时,重要的是要知道当功能具有除 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();
更新值
首次解析功能的值时,底层驱动程序会将结果存储在存储中。这通常是必要的,以确保您的用户在请求之间获得一致的体验。但是,有时您可能希望手动更新功能的存储值。
为此,您可以使用 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');
这只会更新 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
。
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
。但是,如果您想选择退出此功能的默认优雅行为,可以在应用程序的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\FeatureUpdated
更新范围的功能时(通常通过调用activate
或deactivate
)会分发此事件。
Laravel\Pennant\Events\FeatureUpdatedForAllScopes
更新所有范围的功能时(通常通过调用activateForEveryone
或deactivateForEveryone
)会分发此事件。
Laravel\Pennant\Events\FeatureDeleted
删除范围的功能时(通常通过调用forget
)会分发此事件。
Laravel\Pennant\Events\FeaturesPurged
清除特定功能时,会分发此事件。
Laravel\Pennant\Events\AllFeaturesPurged
清除所有功能时,会分发此事件。