跳轉至內容

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

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