Laravel Pennant
導入
Laravel Pennant は、余計な機能のないシンプルで軽量な機能フラッグパッケージです。機能フラッグを使用すると、新しいアプリケーション機能を自信を持って段階的にロールアウトしたり、新しいインターフェースデザインをA/Bテストしたり、トランクベースの開発戦略を補完したりすることができます。
インストール
まず、Composerパッケージマネージャーを使用して、Pennantをプロジェクトにインストールします。
composer require laravel/pennant
次に、vendor:publish
Artisanコマンドを使用して、Pennantの設定ファイルとマイグレーションファイルを公開する必要があります。
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
最後に、アプリケーションのデータベースマイグレーションを実行する必要があります。これにより、Pennantがdatabase
ドライバを動かすために使用するfeatures
テーブルが作成されます。
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を使用しないようにする必要があります。
- それ以外の場合は、100分の1の確率でランダムにユーザーに機能が割り当てられます。
特定のユーザーに対して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']);
Artisanコマンドやキュージョブなど、HTTPコンテキスト以外でPennantを使用する場合は、通常は機能のスコープを明示的に指定する必要があります。または、認証された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
メソッドを使用して、機能がアクティブな場合に特定のクロージャーを流暢に実行できます。さらに、2番目のクロージャーを提供でき、機能が非アクティブな場合に実行されます。
<?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
メソッドを使用できます。通常、このメソッドはアプリケーションのサービスプロバイダーの1つの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
モデルを期待していることに気付くでしょう。ユーザーのチームでこの機能がアクティブかどうかを判断するには、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の組み込みarray
およびdatabase
ストレージドライバは、すべてのPHPデータ型とEloquentモデルのスコープ識別子を適切に保存する方法を認識しています。ただし、アプリケーションでサードパーティのPennantドライバを使用している場合、そのドライバはEloquentモデルまたはアプリケーションのカスタム型の識別子を適切に保存する方法を認識しない可能性があります。
このため、Pennantでは、アプリケーションで使用されるPennantスコープとして機能するオブジェクトにFeatureScopeable
コントラクトを実装することで、ストレージ用のスコープ値をフォーマットできます。
たとえば、単一のアプリケーションで2つの異なる機能ドライバ(組み込みの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ではリッチ値も保存できます。
たとえば、アプリケーションの「今すぐ購入」ボタンで3つの新しい色をテストしていると想像してください。機能定義から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
メソッドを呼び出すと、機能のリッチ値がオプションの2番目のクロージャに提供されます。
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
メソッドに2番目の引数を指定することで、機能のリッチ値を手動で設定することもできます。
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
テスト
機能フラグと対話するコードをテストする場合、テストで機能フラグの返された値を制御する最も簡単な方法は、機能を再定義することです。たとえば、アプリケーションのサービスプロバイダーのいずれかに次のように機能が定義されているとします。
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には、拡張機能を格納するためのディレクトリは含まれていません。どこに配置しても自由です。この例では、RedisFeatureDriver
を格納するためのExtensions
ディレクトリを作成しました。
ドライバの登録
ドライバーの実装が完了したら、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の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
を返します。ただし、このフィーチャのデフォルトの正常な動作をオプトアウトする場合は、アプリケーションの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
このイベントは、すべてのフィーチャをパージしたときにディスパッチされます。