コンテンツへスキップ

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);
lightbulb

機能クラスはコンテナを介して解決されるため、必要に応じて機能クラスのコンストラクターに依存関係を注入できます。

保存された機能名のカスタマイズ

デフォルトでは、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']);
lightbulb

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モデルを期待していることに気付くでしょう。ユーザーのチームでこの機能がアクティブかどうかを判断するには、TeamFeatureファサードの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
lightbulb

リッチ値を使用する場合、機能は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');
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には、拡張機能を格納するためのディレクトリは含まれていません。どこに配置しても自由です。この例では、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を返します。ただし、このフィーチャのデフォルトの正常な動作をオプトアウトする場合は、アプリケーションの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

このイベントは、通常activateまたはdeactivateを呼び出すことによって、スコープのフィーチャを更新したときにディスパッチされます。

Laravel\Pennant\Events\FeatureUpdatedForAllScopes

このイベントは、通常activateForEveryoneまたはdeactivateForEveryoneを呼び出すことによって、すべてのスコープのフィーチャを更新したときにディスパッチされます。

Laravel\Pennant\Events\FeatureDeleted

このイベントは、通常forgetを呼び出すことによって、スコープのフィーチャを削除したときにディスパッチされます。

Laravel\Pennant\Events\FeaturesPurged

このイベントは、特定のフィーチャをパージしたときにディスパッチされます。

Laravel\Pennant\Events\AllFeaturesPurged

このイベントは、すべてのフィーチャをパージしたときにディスパッチされます。