メインコンテンツまでスキップ

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 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),
};
}
}
注記

格納されるフィーチャー名のカスタマイズ

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

HTTPコンテキスト外で Pennant を使用する場合(たとえば、Artisanコマンドやキューに入れられたジョブなど)、通常は明示的にフィーチャーのスコープを指定する必要があります。または、認証された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 メソッドを使用して、フィーチャーがアクティブな場合に与えられたクロージャを流暢に実行することができます。さらに、フィーチャーが非アクティブな場合には、第二のクロージャを提供して実行されます。

    <?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 ディレクティブを提供しています:

@feature('site-redesign')
<!-- 'site-redesign' is active -->
@else
<!-- 'site-redesign' is inactive -->
@endfeature

ミドルウェア

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 メソッドを使用できます。通常、このメソッドは、アプリケーションのサービスプロバイダーの 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);
}
);

// ...
}

インメモリキャッシュ

機能をチェックする際、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 モデルを想定しています。ユーザーのチームに対してこの機能がアクティブかどうかを判断するには、Feature ファサードが提供する for メソッドにチームを渡す必要があります:

if (Feature::for($user->team)->active('billing-v2')) {
return redirect()->to('/billing/v2');
}

// ...

デフォルトスコープ

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

<?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');

Nullable Scope

機能をチェックする際に提供するスコープが null であり、かつ機能の定義が nullable 型をサポートしていないか、nullable 型またはユニオン型で null を含んでいない場合、Pennant は自動的に機能の結果値として false を返します。

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

常に スコープを明示的に指定しない場合 は、スコープの型が "nullable" であることを確認し、機能の定義ロジック内で null スコープ値を処理してください:

use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('new-api', fn (User $user) => match (true) {// [tl! remove]
Feature::define('new-api', fn (User|null $user) => match (true) {// [tl! add]
$user === null => true,// [tl! add]
$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 morph map を使用している場合、Pennant がモーフマップも使用するように選択することができ、保存された機能をアプリケーションの構造から切り離すことができます。

これを実現するために、サービスプロバイダーで Eloquent morph map を定義した後、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',
]));

purchase-button 機能の値を取得するには、value メソッドを使用できます:

$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
注記

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

値の更新

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

以下の方法で、機能を「オン」または「オフ」に切り替えるために 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 メソッドに第二引数を提供することで、機能のリッチな値を手動で設定することも可能です:

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');
注記

機能の削除

時々、ストレージから機能全体を削除することが役立つ場合があります。これは、アプリケーションから機能を削除した場合や、機能の定義を調整してすべてのユーザーに展開したい場合に通常必要とされます。

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 インスタンスを返している場合、便利な テストヘルパー がいくつか利用可能です。

ストアの設定

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

<?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は、アプリケーション全体でフィーチャーフラグを追跡する際に役立つさまざまなイベントをディスパッチします。

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

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