跳至內容

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 facade 提供的 define 方法。您需要為功能提供名稱,以及將被呼叫以解析功能初始值的閉包。

通常,功能是使用 Feature facade 在服務提供者中定義的。閉包將接收功能檢查的「範圍」。最常見的情況是,範圍是目前經過身份驗證的使用者。在此範例中,我們將定義一個功能,以便逐步向應用程式的使用者推出新的 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 facade 上呼叫 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 facade 上的 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 facade 提供的 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 Trait

您可以將 Pennant 的 HasFeatures trait 新增至應用程式的 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 指令

為了使在 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 也包含一個中介層 (middleware),可用於驗證目前已驗證的使用者在路由 (route) 被調用之前是否有權限存取某項功能。您可以將此中介層指派給一個路由,並指定存取該路由所需的功能。如果任何指定的功能對於目前已驗證的使用者處於非啟用狀態,則路由將會返回一個 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 來支援 null,Pennant 將自動返回 false 作為該功能的值。

因此,如果您傳遞給功能的範圍可能為 null,並且您希望調用該功能的值解析器,則應該在該功能的定義中考慮到這一點。如果在 Artisan 命令、排隊作業或未驗證的路由中檢查功能,可能會出現 null 範圍。由於在這些情況下通常沒有已驗證的使用者,因此預設範圍將為 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

測試

在測試與功能標誌互動的程式碼時,在測試中控制功能標誌的傳回值的最簡單方法是簡單地重新定義該功能。例如,假設您在應用程式的某個服務提供者中定義了以下功能。

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 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\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

當清除所有功能時,會分派此事件。