コンテンツへスキップ

Laravel Pennant

イントロダクション

Laravel Pennantは、無駄のないシンプルで軽量な機能フラグパッケージです。機能フラグを使用すると、新しいアプリケーション機能を段階的に自信を持って展開したり、新しいインターフェイスデザインのA/Bテストを行ったり、トランクベースの開発戦略を補完したりするなど、さまざまなことが可能になります。

インストール

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

1composer require laravel/pennant

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

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

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

1php artisan migrate

設定

Pennantのアセットを公開すると、その設定ファイルはconfig/pennant.phpに配置されます。この設定ファイルでは、Pennantが解決済みの機能フラグ値を保存するために使用するデフォルトの保存メカニズムを指定できます。

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

機能の定義

機能を定義するには、Featureファサードが提供する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を使用してはなりません。
  • それ以外の場合、機能はユーザーにランダムに割り当てられ、100分の1の確率でアクティブになります。

特定のユーザーに対してnew-api機能が初めてチェックされると、クロージャの結果がストレージドライバによって保存されます。次回、同じユーザーに対して機能がチェックされると、値はストレージから取得され、クロージャは呼び出されません。

便利なことに、機能定義が抽選のみを返す場合は、クロージャを完全に省略できます。

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 
5class NewApi
6{
7 /**
8 * The stored name of the feature.
9 *
10 * @var string
11 */
12 public $name = 'new-api';
13 
14 // ...
15}

機能のチェック

機能がアクティブかどうかを判断するには、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']);

Artisanコマンドやキュー投入されたジョブなど、HTTPコンテキスト外でPennantを使用する場合、通常は機能のスコープを明示的に指定する必要があります。あるいは、認証された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メソッドを使用すると、機能がアクティブな場合に特定のクロージャを流れるように実行できます。さらに、2番目のクロージャを指定することもでき、機能が非アクティブな場合に実行されます。

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トレイト

PennantのHasFeaturesトレイトをアプリケーションの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}

トレイトをモデルに追加すると、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'));

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

リストされている機能の1つが非アクティブな場合にミドルウェアから返されるレスポンスをカスタマイズしたい場合は、EnsureFeaturesAreActiveミドルウェアが提供するwhenInactiveメソッドを使用できます。通常、このメソッドはアプリケーションのサービスプロバイダの1つの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();

スコープ

スコープの指定

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

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

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

1use App\Models\Team;
2use Carbon\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つで行う必要があります。

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許容スコープ

機能のチェック時に提供するスコープがnullであり、機能の定義がnullable型や共用体型にnullを含めることでnullをサポートしていない場合、Pennantは自動的に機能の結果値としてfalseを返します。

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

常に機能スコープを明示的に指定しない場合は、スコープの型が「nullable」であることを確認し、機能定義ロジック内で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に組み込まれているarrayおよびdatabaseストレージドライバは、すべてのPHPデータ型およびEloquentモデルのスコープ識別子を適切に保存する方法を知っています。ただし、アプリケーションがサードパーティのPennantドライバを利用している場合、そのドライバはEloquentモデルまたはアプリケーション内の他のカスタム型の識別子を適切に保存する方法を知らない可能性があります。

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

たとえば、単一のアプリケーションで2つの異なる機能ドライバ(組み込みの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();

リッチな機能値

これまで、主に機能をバイナリ状態(つまり、「アクティブ」か「非アクティブ」か)として示してきましたが、Pennantではリッチな値を保存することもできます。

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

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

purchase-button機能の値は、valueメソッドを使用して取得できます。

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メソッドを呼び出すと、機能のリッチな値がオプションの2番目のクロージャに提供されます。

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の機能検出機能を使用できます。開始するには、アプリケーションのサービスプロバイダの1つで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// ]

Eagerローディング

Pennantは単一のリクエストに対してすべての解決済み機能のインメモリキャッシュを保持しますが、それでもパフォーマンスの問題に遭遇する可能性があります。これを軽減するために、Pennantは機能値をEagerロードする機能を提供します。

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

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メソッドを使用すると、ユーザーまたはスコープのコレクションに対して機能値をEagerロードすることで、この潜在的なパフォーマンスのボトルネックを解消できます。

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

値の更新

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

これを実現するには、activateおよびdeactivateメソッドを使用して、機能を「オン」または「オフ」に切り替えることができます。

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メソッドに2番目の引数を渡すことで、機能のリッチな値を手動で設定することも可能です。

1Feature::activate('purchase-button', 'seafoam-green');

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

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

一括更新

保存された機能値を一括で更新するには、activateForEveryoneおよびdeactivateForEveryoneメソッドを使用できます。

たとえば、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

テスト

機能フラグと対話するコードをテストする場合、テストで機能フラグの戻り値を制御する最も簡単な方法は、単に機能を再定義することです。たとえば、アプリケーションのサービスプロバイダの1つに次の機能が定義されているとします。

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には、拡張機能を含むディレクトリは付属していません。好きな場所に自由に配置できます。この例では、RedisFeatureDriverを格納するためにExtensionsディレクトリを作成しました。

ドライバの登録

ドライバが実装されたら、Laravelに登録する準備が整いました。Pennantに追加のドライバを追加するには、Featureファサードが提供するextendメソッドを使用できます。アプリケーションのサービスプロバイダの1つの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

このイベントは、特定のスコープに対して不明な機能が初めて解決されたときにディスパッチされます。このイベントをリッスンすることは、機能フラグを削除するつもりだったが、誤ってアプリケーション全体に stray references (浮遊参照) を残してしまった場合に役立つ可能性があります。

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

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

Laravel\Pennant\Events\FeatureUpdatedForAllScopes

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

Laravel\Pennant\Events\FeatureDeleted

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

Laravel\Pennant\Events\FeaturesPurged

このイベントは、特定の機能をパージするときにディスパッチされます。

Laravel\Pennant\Events\AllFeaturesPurged

このイベントは、すべての機能をパージするときにディスパッチされます。

Laravelは最も生産的な方法です
ソフトウェアを構築、デプロイ、監視します。