はじめに

Laravel Pennant は、シンプルで軽量なフィーチャーフラグパッケージです - 不要なものはありません。フィーチャーフラグを使用すると、新しいアプリケーション機能を自信を持って段階的に展開したり、新しいインターフェースデザインをA/Bテストしたり、トランクベースの開発戦略を補完したり、その他多くのことが可能になります。

インストール

まず、Composerパッケージマネージャーを使用してPennantをプロジェクトにインストールします:

  1. composer require laravel/pennant

次に、vendor:publish Artisanコマンドを使用してPennantの設定ファイルとマイグレーションファイルを公開する必要があります:

  1. php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"

最後に、アプリケーションのデータベースマイグレーションを実行する必要があります。これにより、Pennantがそのdatabaseドライバーを動かすために使用するfeaturesテーブルが作成されます:

  1. php artisan migrate

設定

Pennantのアセットを公開した後、その設定ファイルはconfig/pennant.phpにあります。この設定ファイルでは、Pennantが解決されたフィーチャーフラグの値を保存するために使用するデフォルトのストレージメカニズムを指定できます。

Pennantは、arrayドライバーを介してメモリ内配列に解決されたフィーチャーフラグの値を保存するサポートを含んでいます。また、Pennantは、databaseドライバーを介してリレーショナルデータベースに解決されたフィーチャーフラグの値を永続的に保存することもできます。これは、Pennantが使用するデフォルトのストレージメカニズムです。

フィーチャーの定義

フィーチャーを定義するには、defineメソッドを使用することができます。フィーチャーの名前と、フィーチャーの初期値を解決するために呼び出されるクロージャを提供する必要があります。

通常、フィーチャーはFeatureファサードを使用してサービスプロバイダー内で定義されます。クロージャは、フィーチャーチェックの「スコープ」を受け取ります。最も一般的には、スコープは現在認証されているユーザーです。この例では、アプリケーションのユーザーに新しいAPIを段階的に展開するためのフィーチャーを定義します:

  1. <?php
  2. namespace App\Providers;
  3. use App\Models\User;
  4. use Illuminate\Support\Lottery;
  5. use Illuminate\Support\ServiceProvider;
  6. use Laravel\Pennant\Feature;
  7. class AppServiceProvider extends ServiceProvider
  8. {
  9. /**
  10. * Bootstrap any application services.
  11. */
  12. public function boot(): void
  13. {
  14. Feature::define('new-api', fn (User $user) => match (true) {
  15. $user->isInternalTeamMember() => true,
  16. $user->isHighTrafficCustomer() => false,
  17. default => Lottery::odds(1 / 100),
  18. });
  19. }
  20. }

ご覧のとおり、フィーチャーには次のルールがあります:

  • すべての内部チームメンバーは新しいAPIを使用する必要があります。
  • 高トラフィックの顧客は新しいAPIを使用してはいけません。
  • それ以外の場合、フィーチャーはユーザーに対して1/100の確率でランダムに割り当てられるべきです。
  1. 便利なことに、フィーチャー定義がロッタリーのみを返す場合、クロージャを完全に省略できます:
  2. ``````php
  3. Feature::define('site-redesign', Lottery::odds(1, 1000));
  4. `

クラスベースのフィーチャー

Pennantは、クラスベースのフィーチャーを定義することも可能です。クロージャベースのフィーチャー定義とは異なり、サービスプロバイダーにクラスベースのフィーチャーを登録する必要はありません。クラスベースのフィーチャーを作成するには、pennant:feature Artisanコマンドを呼び出します。デフォルトでは、フィーチャークラスはアプリケーションのapp/Featuresディレクトリに配置されます:

  1. php artisan pennant:feature NewApi

フィーチャークラスを書くときは、特定のスコープに対してフィーチャーの初期値を解決するために呼び出されるresolveメソッドを定義するだけで済みます。再度、スコープは通常、現在認証されているユーザーです:

  1. <?php
  2. namespace App\Features;
  3. use App\Models\User;
  4. use Illuminate\Support\Lottery;
  5. class NewApi
  6. {
  7. /**
  8. * Resolve the feature's initial value.
  9. */
  10. public function resolve(User $user): mixed
  11. {
  12. return match (true) {
  13. $user->isInternalTeamMember() => true,
  14. $user->isHighTrafficCustomer() => false,
  15. default => Lottery::odds(1 / 100),
  16. };
  17. }
  18. }

クラスベースのフィーチャーのインスタンスを手動で解決したい場合は、instanceメソッドをFeatureファサードで呼び出すことができます:

  1. use Illuminate\Support\Facades\Feature;
  2. $instance = Feature::instance(NewApi::class);

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

保存されたフィーチャー名のカスタマイズ

デフォルトでは、Pennantはフィーチャークラスの完全修飾クラス名を保存します。保存されたフィーチャー名をアプリケーションの内部構造から切り離したい場合は、フィーチャークラスに$nameプロパティを指定できます。このプロパティの値は、クラス名の代わりに保存されます:

  1. <?php
  2. namespace App\Features;
  3. class NewApi
  4. {
  5. /**
  6. * The stored name of the feature.
  7. *
  8. * @var string
  9. */
  10. public $name = 'new-api';
  11. // ...
  12. }

フィーチャーのチェック

フィーチャーがアクティブかどうかを判断するには、activeメソッドをFeatureファサードで使用します。デフォルトでは、フィーチャーは現在認証されているユーザーに対してチェックされます:

  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Http\Request;
  4. use Illuminate\Http\Response;
  5. use Laravel\Pennant\Feature;
  6. class PodcastController
  7. {
  8. /**
  9. * Display a listing of the resource.
  10. */
  11. public function index(Request $request): Response
  12. {
  13. return Feature::active('new-api')
  14. ? $this->resolveNewApiResponse($request)
  15. : $this->resolveLegacyApiResponse($request);
  16. }
  17. // ...
  18. }

フィーチャーはデフォルトで現在認証されているユーザーに対してチェックされますが、別のユーザーやスコープに対してフィーチャーを簡単にチェックすることもできます。これを実現するには、forメソッドをFeatureファサードで使用します:

  1. return 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...
  2. Feature::allAreActive(['new-api', 'site-redesign']);
  3. // Determine if any of the given features are active...
  4. Feature::someAreActive(['new-api', 'site-redesign']);
  5. // Determine if a feature is inactive...
  6. Feature::inactive('new-api');
  7. // Determine if all of the given features are inactive...
  8. Feature::allAreInactive(['new-api', 'site-redesign']);
  9. // Determine if any of the given features are inactive...
  10. Feature::someAreInactive(['new-api', 'site-redesign']);

PennantをHTTPコンテキスト外で使用する場合、たとえばArtisanコマンドやキューに入れられたジョブの中で、通常はフィーチャーのスコープを明示的に指定する必要があります。あるいは、認証されたHTTPコンテキストと認証されていないコンテキストの両方を考慮したデフォルトスコープを定義することもできます。

クラスベースのフィーチャーのチェック

クラスベースのフィーチャーの場合、フィーチャーをチェックする際にクラス名を提供する必要があります:

  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Features\NewApi;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Http\Response;
  6. use Laravel\Pennant\Feature;
  7. class PodcastController
  8. {
  9. /**
  10. * Display a listing of the resource.
  11. */
  12. public function index(Request $request): Response
  13. {
  14. return Feature::active(NewApi::class)
  15. ? $this->resolveNewApiResponse($request)
  16. : $this->resolveLegacyApiResponse($request);
  17. }
  18. // ...
  19. }

条件付き実行

フィーチャーがアクティブな場合、whenメソッドを使用して指定されたクロージャを流暢に実行できます。さらに、2つ目のクロージャを提供することもでき、フィーチャーが非アクティブな場合に実行されます:

  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Features\NewApi;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Http\Response;
  6. use Laravel\Pennant\Feature;
  7. class PodcastController
  8. {
  9. /**
  10. * Display a listing of the resource.
  11. */
  12. public function index(Request $request): Response
  13. {
  14. return Feature::when(NewApi::class,
  15. fn () => $this->resolveNewApiResponse($request),
  16. fn () => $this->resolveLegacyApiResponse($request),
  17. );
  18. }
  19. // ...
  20. }
  1. ``````php
  2. return Feature::unless(NewApi::class,
  3. fn () => $this->resolveLegacyApiResponse($request),
  4. fn () => $this->resolveNewApiResponse($request),
  5. );
  6. `

HasFeaturesトレイト

PennantのHasFeaturesトレイトをアプリケーションのUserモデル(またはフィーチャーを持つ他のモデル)に追加することで、モデルから直接フィーチャーをチェックするための流暢で便利な方法を提供します:

  1. <?php
  2. namespace App\Models;
  3. use Illuminate\Foundation\Auth\User as Authenticatable;
  4. use Laravel\Pennant\Concerns\HasFeatures;
  5. class User extends Authenticatable
  6. {
  7. use HasFeatures;
  8. // ...
  9. }

トレイトがモデルに追加されると、featuresメソッドを呼び出すことでフィーチャーを簡単にチェックできます:

  1. if ($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. // State...
  5. $user->features()->active('new-api');
  6. $user->features()->allAreActive(['new-api', 'server-api']);
  7. $user->features()->someAreActive(['new-api', 'server-api']);
  8. $user->features()->inactive('new-api');
  9. $user->features()->allAreInactive(['new-api', 'server-api']);
  10. $user->features()->someAreInactive(['new-api', 'server-api']);
  11. // Conditional execution...
  12. $user->features()->when('new-api',
  13. fn () => /* ... */,
  14. fn () => /* ... */,
  15. );
  16. $user->features()->unless('new-api',
  17. fn () => /* ... */,
  18. fn () => /* ... */,
  19. );

Bladeディレクティブ

Bladeでフィーチャーをチェックするシームレスな体験を提供するために、Pennantは@featureディレクティブを提供します:

  1. @feature('site-redesign')
  2. <!-- 'site-redesign' is active -->
  3. @else
  4. <!-- 'site-redesign' is inactive -->
  5. @endfeature

ミドルウェア

Pennantには、現在認証されているユーザーがルートが呼び出される前にフィーチャーにアクセスできるかどうかを確認するために使用できるミドルウェアも含まれています。ミドルウェアをルートに割り当て、ルートにアクセスするために必要なフィーチャーを指定できます。指定されたフィーチャーのいずれかが現在認証されているユーザーに対して非アクティブな場合、ルートによって400 Bad Request HTTPレスポンスが返されます。複数のフィーチャーを静的usingメソッドに渡すことができます。

  1. use Illuminate\Support\Facades\Route;
  2. use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
  3. Route::get('/api/servers', function () {
  4. // ...
  5. })->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));

レスポンスのカスタマイズ

リストされたフィーチャーのいずれかが非アクティブな場合にミドルウェアによって返されるレスポンスをカスタマイズしたい場合は、whenInactiveメソッドをEnsureFeaturesAreActiveミドルウェアで使用できます。通常、このメソッドはアプリケーションのサービスプロバイダーのbootメソッド内で呼び出されるべきです:

  1. use Illuminate\Http\Request;
  2. use Illuminate\Http\Response;
  3. use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
  4. /**
  5. * Bootstrap any application services.
  6. */
  7. public function boot(): void
  8. {
  9. EnsureFeaturesAreActive::whenInactive(
  10. function (Request $request, array $features) {
  11. return new Response(status: 403);
  12. }
  13. );
  14. // ...
  15. }

フィーチャーチェックのインターセプト

時には、保存されたフィーチャーの値を取得する前にメモリ内でいくつかのチェックを行うことが有用です。フィーチャーフラグの背後に新しいAPIを開発していて、新しいAPIを無効にする能力を持ちつつ、ストレージ内の解決されたフィーチャー値を失いたくないと想像してください。新しいAPIにバグが見つかった場合、内部チームメンバーを除いてすべての人に対してそれを簡単に無効にし、バグを修正し、その後、以前にフィーチャーにアクセスできたユーザーのために新しいAPIを再度有効にすることができます。

これはclass-based featurebeforeメソッドを使用して実現できます。存在する場合、beforeメソッドは常にストレージから値を取得する前にメモリ内で実行されます。メソッドから非null値が返されると、それはリクエストの間、フィーチャーの保存された値の代わりに使用されます:

  1. <?php
  2. namespace App\Features;
  3. use App\Models\User;
  4. use Illuminate\Support\Facades\Config;
  5. use Illuminate\Support\Lottery;
  6. class NewApi
  7. {
  8. /**
  9. * Run an always-in-memory check before the stored value is retrieved.
  10. */
  11. public function before(User $user): mixed
  12. {
  13. if (Config::get('features.new-api.disabled')) {
  14. return $user->isInternalTeamMember();
  15. }
  16. }
  17. /**
  18. * Resolve the feature's initial value.
  19. */
  20. public function resolve(User $user): mixed
  21. {
  22. return match (true) {
  23. $user->isInternalTeamMember() => true,
  24. $user->isHighTrafficCustomer() => false,
  25. default => Lottery::odds(1 / 100),
  26. };
  27. }
  28. }

このフィーチャーを使用して、以前にフィーチャーフラグの背後にあったフィーチャーのグローバル展開をスケジュールすることもできます:

  1. <?php
  2. namespace App\Features;
  3. use Illuminate\Support\Carbon;
  4. use Illuminate\Support\Facades\Config;
  5. class NewApi
  6. {
  7. /**
  8. * Run an always-in-memory check before the stored value is retrieved.
  9. */
  10. public function before(User $user): mixed
  11. {
  12. if (Config::get('features.new-api.disabled')) {
  13. return $user->isInternalTeamMember();
  14. }
  15. if (Carbon::parse(Config::get('features.new-api.rollout-date'))->isPast()) {
  16. return true;
  17. }
  18. }
  19. // ...
  20. }

メモリ内キャッシュ

フィーチャーをチェックする際、Pennantは結果のメモリ内キャッシュを作成します。databaseドライバーを使用している場合、これは、単一のリクエスト内で同じフィーチャーフラグを再チェックしても追加のデータベースクエリがトリガーされないことを意味します。これにより、リクエストの間、フィーチャーが一貫した結果を持つことが保証されます。

メモリ内キャッシュを手動でフラッシュする必要がある場合は、flushCacheメソッドをFeatureファサードで使用できます:

  1. Feature::flushCache();

スコープ

スコープの指定

前述のように、フィーチャーは通常、現在認証されているユーザーに対してチェックされます。しかし、これは常にニーズに合うとは限りません。したがって、特定のフィーチャーをチェックしたいスコープをFeatureファサードのforメソッドを介して指定することが可能です:

  1. return Feature::for($user)->active('new-api')
  2. ? $this->resolveNewApiResponse($request)
  3. : $this->resolveLegacyApiResponse($request);

もちろん、フィーチャースコープは「ユーザー」に限定されません。「新しい請求体験」を構築していて、個々のユーザーではなく、チーム全体に展開していると想像してください。おそらく、古いチームが新しいチームよりも遅い展開を持つことを望んでいるでしょう。フィーチャー解決クロージャは次のようになります:

  1. use App\Models\Team;
  2. use Carbon\Carbon;
  3. use Illuminate\Support\Lottery;
  4. use Laravel\Pennant\Feature;
  5. Feature::define('billing-v2', function (Team $team) {
  6. if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
  7. return true;
  8. }
  9. if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
  10. return Lottery::odds(1 / 100);
  11. }
  12. return Lottery::odds(1 / 1000);
  13. });

定義したクロージャはUserを期待していないことに気付くでしょうが、Teamモデルを期待しています。このフィーチャーがユーザーのチームに対してアクティブかどうかを判断するには、forファサードで提供されるFeatureメソッドにチームを渡す必要があります:

  1. if (Feature::for($user->team)->active('billing-v2')) {
  2. return redirect('/billing/v2');
  3. }
  4. // ...

デフォルトスコープ

Pennantがフィーチャーをチェックするために使用するデフォルトスコープをカスタマイズすることも可能です。たとえば、すべてのフィーチャーが現在認証されているユーザーのチームに対してチェックされる場合、Feature::for($user->team)を毎回呼び出す代わりに、チームをデフォルトスコープとして指定できます。通常、これはアプリケーションのサービスプロバイダーの1つで行うべきです:

  1. <?php
  2. namespace App\Providers;
  3. use Illuminate\Support\Facades\Auth;
  4. use Illuminate\Support\ServiceProvider;
  5. use Laravel\Pennant\Feature;
  6. class AppServiceProvider extends ServiceProvider
  7. {
  8. /**
  9. * Bootstrap any application services.
  10. */
  11. public function boot(): void
  12. {
  13. Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
  14. // ...
  15. }
  16. }
  1. ``````php
  2. Feature::active('billing-v2');
  3. // Is now equivalent to...
  4. Feature::for($user->team)->active('billing-v2');
  5. `

ヌラブルスコープ

フィーチャーをチェックする際に提供するスコープがnullであり、フィーチャーの定義がヌラブルタイプを介してnullをサポートしていない場合、またはnullをユニオンタイプに含めていない場合、Pennantは自動的にfalseをフィーチャーの結果値として返します。

したがって、フィーチャーに渡すスコープが潜在的にnullであり、フィーチャーの値解決者を呼び出したい場合は、フィーチャーの定義でそれを考慮する必要があります。nullスコープは、Artisanコマンド、キューに入れられたジョブ、または認証されていないルート内でフィーチャーをチェックする場合に発生する可能性があります。これらのコンテキストには通常、認証されたユーザーが存在しないため、デフォルトスコープはnullになります。

フィーチャースコープを常に明示的に指定しない場合は、スコープのタイプが「ヌラブル」であることを確認し、フィーチャー定義ロジック内でnullスコープ値を処理する必要があります:

  1. use App\Models\User;
  2. use Illuminate\Support\Lottery;
  3. use Laravel\Pennant\Feature;
  4. Feature::define('new-api', fn (User $user) => match (true) {
  5. Feature::define('new-api', fn (User|null $user) => match (true) {
  6. $user === null => true,
  7. $user->isInternalTeamMember() => true,
  8. $user->isHighTrafficCustomer() => false,
  9. default => Lottery::odds(1 / 100),
  10. });

スコープの識別

Pennantの組み込みarrayおよびdatabaseストレージドライバーは、すべてのPHPデータ型およびEloquentモデルのスコープ識別子を適切に保存する方法を知っています。しかし、アプリケーションがサードパーティのPennantドライバーを利用している場合、そのドライバーはEloquentモデルやアプリケーション内の他のカスタムタイプの識別子を適切に保存する方法を知らない可能性があります。

このため、Pennantは、アプリケーション内でPennantスコープとして使用されるオブジェクトにFeatureScopeable契約を実装することで、ストレージ用のスコープ値をフォーマットすることを許可します。

たとえば、単一のアプリケーションで2つの異なるフィーチャードライバーを使用していると想像してください: 組み込みのdatabaseドライバーとサードパーティの「Flag Rocket」ドライバー。「Flag Rocket」ドライバーはEloquentモデルを適切に保存する方法を知りません。代わりに、FlagRocketUserインスタンスが必要です。toFeatureIdentifier契約によって定義されたFeatureScopeableを実装することで、アプリケーションで使用される各ドライバーに提供されるストレージ可能なスコープ値をカスタマイズできます:

  1. <?php
  2. namespace App\Models;
  3. use FlagRocket\FlagRocketUser;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Laravel\Pennant\Contracts\FeatureScopeable;
  6. class User extends Model implements FeatureScopeable
  7. {
  8. /**
  9. * Cast the object to a feature scope identifier for the given driver.
  10. */
  11. public function toFeatureIdentifier(string $driver): mixed
  12. {
  13. return match($driver) {
  14. 'database' => $this,
  15. 'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
  16. };
  17. }
  18. }

スコープのシリアライズ

デフォルトでは、PennantはEloquentモデルに関連付けられたフィーチャーを保存する際に完全修飾クラス名を使用します。すでにEloquentモーフマップを使用している場合、Pennantがモーフマップを使用して保存されたフィーチャーをアプリケーション構造から切り離すことを選択できます。

これを実現するには、サービスプロバイダーでEloquentモーフマップを定義した後、FeatureファサードのuseMorphMapメソッドを呼び出します:

  1. use Illuminate\Database\Eloquent\Relations\Relation;
  2. use Laravel\Pennant\Feature;
  3. Relation::enforceMorphMap([
  4. 'post' => 'App\Models\Post',
  5. 'video' => 'App\Models\Video',
  6. ]);
  7. Feature::useMorphMap();

リッチフィーチャー値

これまで、フィーチャーはバイナリ状態であると示されてきました。つまり、フィーチャーは「アクティブ」または「非アクティブ」のいずれかですが、Pennantはリッチな値を保存することも可能です。

たとえば、アプリケーションの「今すぐ購入」ボタンのために3つの新しい色をテストしていると想像してください。フィーチャー定義からtrueまたはfalseを返す代わりに、文字列を返すことができます:

  1. use Illuminate\Support\Arr;
  2. use Laravel\Pennant\Feature;
  3. Feature::define('purchase-button', fn (User $user) => Arr::random([
  4. 'blue-sapphire',
  5. 'seafoam-green',
  6. 'tart-orange',
  7. ]));
  1. ``````php
  2. $color = Feature::value('purchase-button');
  3. `

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メソッドを呼び出すと、フィーチャーのリッチ値が最初のクロージャに提供されます:

  1. Feature::when('purchase-button',
  2. fn ($color) => /* ... */,
  3. fn () => /* ... */,
  4. );

同様に、条件付きunlessメソッドを呼び出すと、フィーチャーのリッチ値がオプションの2番目のクロージャに提供されます:

  1. Feature::unless('purchase-button',
  2. fn () => /* ... */,
  3. fn ($color) => /* ... */,
  4. );

複数のフィーチャーの取得

  1. ``````php
  2. Feature::values(['billing-v2', 'purchase-button']);
  3. // [
  4. // 'billing-v2' => false,
  5. // 'purchase-button' => 'blue-sapphire',
  6. // ]
  7. `

または、allメソッドを使用して、特定のスコープに対して定義されたすべてのフィーチャーの値を取得できます:

  1. Feature::all();
  2. // [
  3. // 'billing-v2' => false,
  4. // 'purchase-button' => 'blue-sapphire',
  5. // 'site-redesign' => true,
  6. // ]

ただし、クラスベースのフィーチャーは動的に登録され、明示的にチェックされるまでPennantには知られていません。これは、アプリケーションのクラスベースのフィーチャーが、現在のリクエスト中にすでにチェックされていない場合、allメソッドによって返される結果に表示されない可能性があることを意味します。

  1. ``````php
  2. <?php
  3. namespace App\Providers;
  4. use Illuminate\Support\ServiceProvider;
  5. use Laravel\Pennant\Feature;
  6. class AppServiceProvider extends ServiceProvider
  7. {
  8. /**
  9. * Bootstrap any application services.
  10. */
  11. public function boot(): void
  12. {
  13. Feature::discover();
  14. // ...
  15. }
  16. }
  17. `
  1. ``````php
  2. Feature::all();
  3. // [
  4. // 'App\Features\NewApi' => true,
  5. // 'billing-v2' => false,
  6. // 'purchase-button' => 'blue-sapphire',
  7. // 'site-redesign' => true,
  8. // ]
  9. `

イagerローディング

Pennantは、単一のリクエストのすべての解決されたフィーチャーのメモリ内キャッシュを保持しますが、パフォーマンスの問題に直面する可能性があります。これを軽減するために、Pennantはフィーチャー値をイagerローディングする機能を提供します。

これを説明するために、フィーチャーがループ内でアクティブかどうかをチェックしていると想像してください:

  1. use Laravel\Pennant\Feature;
  2. foreach ($users as $user) {
  3. if (Feature::for($user)->active('notifications-beta')) {
  4. $user->notify(new RegistrationSuccess);
  5. }
  6. }

データベースドライバーを使用していると仮定すると、このコードはループ内の各ユーザーに対してデータベースクエリを実行します - 数百のクエリを実行する可能性があります。しかし、Pennantのloadメソッドを使用することで、ユーザーやスコープのコレクションのフィーチャー値をイagerローディングすることで、この潜在的なパフォーマンスボトルネックを取り除くことができます:

  1. Feature::for($users)->load(['notifications-beta']);
  2. foreach ($users as $user) {
  3. if (Feature::for($user)->active('notifications-beta')) {
  4. $user->notify(new RegistrationSuccess);
  5. }
  6. }

すでにロードされていない場合にのみフィーチャー値をロードするには、loadMissingメソッドを使用できます:

  1. Feature::for($users)->loadMissing([
  2. 'new-api',
  3. 'purchase-button',
  4. 'notifications-beta',
  5. ]);
  1. ``````php
  2. Feature::for($user)->loadAll();
  3. `

値の更新

フィーチャーの値が初めて解決されると、基盤となるドライバーは結果をストレージに保存します。これは、リクエスト間でユーザーに一貫した体験を保証するためにしばしば必要です。しかし、時にはフィーチャーの保存された値を手動で更新したい場合があります。

これを実現するには、activateおよびdeactivateメソッドを使用してフィーチャーを「オン」または「オフ」に切り替えます:

  1. use Laravel\Pennant\Feature;
  2. // Activate the feature for the default scope...
  3. Feature::activate('new-api');
  4. // Deactivate the feature for the given scope...
  5. Feature::for($user->team)->deactivate('billing-v2');
  1. ``````php
  2. Feature::activate('purchase-button', 'seafoam-green');
  3. `

フィーチャーの保存された値を忘れさせるために、forgetメソッドを使用できます。フィーチャーが再度チェックされると、Pennantはフィーチャーの値をそのフィーチャー定義から解決します:

  1. Feature::forget('purchase-button');

バルク更新

保存されたフィーチャー値を一括で更新するには、activateForEveryoneおよびdeactivateForEveryoneメソッドを使用します。

たとえば、new-apiフィーチャーの安定性に自信を持ち、チェックアウトフローに最適な'purchase-button'色が決まった場合、すべてのユーザーに対して保存された値を更新できます:

  1. use Laravel\Pennant\Feature;
  2. Feature::activateForEveryone('new-api');
  3. Feature::activateForEveryone('purchase-button', 'seafoam-green');

または、すべてのユーザーに対してフィーチャーを無効にすることもできます:

  1. Feature::deactivateForEveryone('new-api');

これは、Pennantのストレージドライバーによって保存された解決されたフィーチャー値のみを更新します。アプリケーション内のフィーチャー定義も更新する必要があります。

フィーチャーの消去

時には、ストレージからフィーチャー全体を消去することが有用です。これは、アプリケーションからフィーチャーを削除した場合や、すべてのユーザーに展開したいフィーチャーの定義を調整した場合に通常必要です。

  1. ``````php
  2. // Purging a single feature...
  3. Feature::purge('new-api');
  4. // Purging multiple features...
  5. Feature::purge(['new-api', 'purchase-button']);
  6. `

すべてのフィーチャーをストレージから消去したい場合は、引数なしでpurgeメソッドを呼び出すことができます:

  1. Feature::purge();

アプリケーションのデプロイメントパイプラインの一部としてフィーチャーを消去することが有用であるため、Pennantには、提供されたフィーチャーをストレージから消去するpennant:purge Artisanコマンドが含まれています:

  1. php artisan pennant:purge new-api
  2. php artisan pennant:purge new-api purchase-button

特定のフィーチャーリストにあるフィーチャーを除いてすべてのフィーチャーを消去することも可能です。たとえば、「new-api」と「purchase-button」フィーチャーの値をストレージに保持しつつ、すべてのフィーチャーを消去したい場合は、これらのフィーチャー名を--exceptオプションに渡すことができます:

  1. php artisan pennant:purge --except=new-api --except=purchase-button

便利なことに、pennant:purgeコマンドは--except-registeredフラグもサポートしています。このフラグは、サービスプロバイダーに明示的に登録されたフィーチャーを除いてすべてのフィーチャーを消去することを示します:

  1. php artisan pennant:purge --except-registered

テスト

フィーチャーフラグと対話するコードをテストする際、テスト内でフィーチャーフラグの返される値を制御する最も簡単な方法は、フィーチャーを再定義することです。たとえば、アプリケーションのサービスプロバイダーの1つで次のフィーチャーが定義されていると想像してください:

  1. use Illuminate\Support\Arr;
  2. use Laravel\Pennant\Feature;
  3. Feature::define('purchase-button', fn () => Arr::random([
  4. 'blue-sapphire',
  5. 'seafoam-green',
  6. 'tart-orange',
  7. ]));

テスト内でフィーチャーの返される値を変更するには、テストの最初にフィーチャーを再定義します。次のテストは常に成功します。Arr::random()実装がサービスプロバイダーにまだ存在していても:

  1. use Laravel\Pennant\Feature;
  2. test('it can control feature values', function () {
  3. Feature::define('purchase-button', 'seafoam-green');
  4. expect(Feature::value('purchase-button'))->toBe('seafoam-green');
  5. });
  1. use Laravel\Pennant\Feature;
  2. public function test_it_can_control_feature_values()
  3. {
  4. Feature::define('purchase-button', 'seafoam-green');
  5. $this->assertSame('seafoam-green', Feature::value('purchase-button'));
  6. }

同じアプローチはクラスベースのフィーチャーにも使用できます:

  1. use Laravel\Pennant\Feature;
  2. test('it can control feature values', function () {
  3. Feature::define(NewApi::class, true);
  4. expect(Feature::value(NewApi::class))->toBeTrue();
  5. });
  1. use App\Features\NewApi;
  2. use Laravel\Pennant\Feature;
  3. public function test_it_can_control_feature_values()
  4. {
  5. Feature::define(NewApi::class, true);
  6. $this->assertTrue(Feature::value(NewApi::class));
  7. }

フィーチャーがLotteryインスタンスを返す場合、役立つテストヘルパーがいくつかあります。

ストア設定

Pennantがテスト中に使用するストアを構成するには、アプリケーションのphpunit.xmlファイルにPENNANT_STORE環境変数を定義します:

  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. namespace App\Extensions;
  3. use Laravel\Pennant\Contracts\Driver;
  4. class RedisFeatureDriver implements Driver
  5. {
  6. public function define(string $feature, callable $resolver): void {}
  7. public function defined(): array {}
  8. public function getAll(array $features): array {}
  9. public function get(string $feature, mixed $scope): mixed {}
  10. public function set(string $feature, mixed $scope, mixed $value): void {}
  11. public function setForAllScopes(string $feature, mixed $value): void {}
  12. public function delete(string $feature, mixed $scope): void {}
  13. public function purge(array|null $features): void {}
  14. }

これで、Redis接続を使用してこれらのメソッドを実装する必要があります。各メソッドの実装方法の例については、PennantソースコードLaravel\Pennant\Drivers\DatabaseDriverを参照してください。

Laravelには拡張を含むディレクトリが付属していません。好きな場所に配置できます。この例では、Extensionsディレクトリを作成してRedisFeatureDriverを収容しました。

ドライバーの登録

ドライバーが実装されたら、Laravelに登録する準備が整いました。Pennantに追加のドライバーを追加するには、extendメソッドをFeatureファサードで使用します。アプリケーションのサービスプロバイダーbootメソッドからextendメソッドを呼び出す必要があります:

  1. <?php
  2. namespace App\Providers;
  3. use App\Extensions\RedisFeatureDriver;
  4. use Illuminate\Contracts\Foundation\Application;
  5. use Illuminate\Support\ServiceProvider;
  6. use Laravel\Pennant\Feature;
  7. class AppServiceProvider extends ServiceProvider
  8. {
  9. /**
  10. * Register any application services.
  11. */
  12. public function register(): void
  13. {
  14. // ...
  15. }
  16. /**
  17. * Bootstrap any application services.
  18. */
  19. public function boot(): void
  20. {
  21. Feature::extend('redis', function (Application $app) {
  22. return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
  23. });
  24. }
  25. }

ドライバーが登録されると、アプリケーションのconfig/pennant.php設定ファイルでredisドライバーを使用できます:

  1. 'stores' => [
  2. 'redis' => [
  3. 'driver' => 'redis',
  4. 'connection' => null,
  5. ],
  6. // ...
  7. ],

イベント

Pennantは、アプリケーション全体でフィーチャーフラグを追跡する際に役立つさまざまなイベントを発行します。

Laravel\Pennant\Events\FeatureRetrieved

このイベントは、フィーチャーがチェックされるたびに発行されます。このイベントは、アプリケーション全体でフィーチャーフラグの使用に対してメトリックを作成および追跡するのに役立ちます。

Laravel\Pennant\Events\FeatureResolved

このイベントは、特定のスコープに対してフィーチャーの値が初めて解決されるときに発行されます。

Laravel\Pennant\Events\UnknownFeatureResolved

このイベントは、特定のスコープに対して未知のフィーチャーが初めて解決されるときに発行されます。このイベントをリッスンすることは、フィーチャーフラグを削除する意図があったが、アプリケーション全体に残された参照がある場合に役立ちます:

  1. <?php
  2. namespace App\Providers;
  3. use Illuminate\Support\ServiceProvider;
  4. use Illuminate\Support\Facades\Event;
  5. use Illuminate\Support\Facades\Log;
  6. use Laravel\Pennant\Events\UnknownFeatureResolved;
  7. class AppServiceProvider extends ServiceProvider
  8. {
  9. /**
  10. * Bootstrap any application services.
  11. */
  12. public function boot(): void
  13. {
  14. Event::listen(function (UnknownFeatureResolved $event) {
  15. Log::error("Resolving unknown feature [{$event->feature}].");
  16. });
  17. }
  18. }

Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass

このイベントは、リクエスト中にclass based featureが初めて動的にチェックされるときに発行されます。

Laravel\Pennant\Events\UnexpectedNullScopeEncountered

このイベントは、フィーチャー定義にnullスコープが渡されたときに発生しますが、ヌルをサポートしていない場合。

この状況は優雅に処理され、フィーチャーはfalseを返します。ただし、このフィーチャーのデフォルトの優雅な動作をオプトアウトしたい場合は、アプリケーションのAppServiceProviderメソッドでこのイベントのリスナーを登録できます:

  1. use Illuminate\Support\Facades\Log;
  2. use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
  3. /**
  4. * Bootstrap any application services.
  5. */
  6. public function boot(): void
  7. {
  8. Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));
  9. }

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

このイベントは、すべてのフィーチャーを消去するときに発行されます。