イントロダクション
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
ドライバをサポートしています。もしくは、算出済み機能フラグ値を、リレーショナルデータベースに永続的に保存する、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
{
/**
* 全アプリケーションサービスの初期起動処理
*/
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を使用できない。
- それ以外の場合、1/100の確率で、ランダムに機能をアクティブにする。
最初にnew-api
機能を指定したユーザーに対してチェックしたら、クロージャの実行結果をストレージドライバへ保存します。次回、同じユーザーに対しこの機能をチェックするとき、値はストレージから取り出し、クロージャを呼び出しません。
使いやすいように、機能定義が抽選(lottery)を返すだけの場合は、クロージャを完全に省略できます。
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
{
/**
* 機能の初期値を決める
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}
Note: 機能クラスは、コンテナにより依存解決されるため、必要に応じ機能クラスのコンストラクタに依存を注入できます。
機能のチェック
ある機能がアクティブであるかを判断するには、Feature
ファサードのactive
メソッドを使用します。デフォルトで機能は、現在認証しているユーザーを対象にチェックします。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* リソースリストの表示
*/
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はさらに、機能がアクティブかを判断するのに役立つ、便利なメソッドをいくつか用意しています。
// 指定機能がすべてアクティブであることを判断
Feature::allAreActive(['new-api', 'site-redesign']);
// 指定機能のうち、どれかがアクティブであることを判断
Feature::someAreActive(['new-api', 'site-redesign']);
// 特定機能がアクティブではないことを判断
Feature::inactive('new-api');
// 指定機能が全部アクティブではないことを判断
Feature::allAreInactive(['new-api', 'site-redesign']);
// 指摘機能のうち、どれかがアクティブでないことを判断
Feature::someAreInactive(['new-api', 'site-redesign']);
Note: PennantをHTTPコンテキスト外で使う場合、例えば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
{
/**
* リソースリストの表示
*/
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
{
/**
* リソースリストの表示
*/
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),
);
HasFetres
トレイト
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
メソッドは、機能を操作する他の多くの便利なメソッドへのアクセスも提供します。
// 値
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);
// 状態
$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']);
// 条件付き実行
$user->features()->when('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
$user->features()->unless('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
Bladeディレクティブ
Blade内でも機能をシームレスにチェックするため、Pennantは@feature
ディレクティブを提供します。
@feature('site-redesign')
<!-- 'site-redesign'はアクティブ -->
@else
<!-- 'site-redesign'は非アクティブ -->
@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;
/**
* 全アプリケーションサービスの初期起動処理
*/
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)
を毎回呼び出す代わりに、チームをデフォルトのスコープとして指定できます。一般に、これはアプリケーションのいずれかのサービスプロバイダで行う必要があります。
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* 全アプリケーションサービスの初期起動処理
*/
public function boot(): void
{
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
// ...
}
}
これにより、for
メソッドで明示的にスコープを指定しなかった場合、機能チェックでは現在認証しているユーザーのチームをデフォルトのスコープとして使用するようになりました。
Feature::active('billing-v2');
// 上記コードが以下と同じ動作をするようになった
Feature::for($user->team)->active('billing-v2');
NULL許可のスコープ
もし、ある機能をチェックするときに指定したスコープで、nullable型またはnull
を含むunion型により、機能の定義で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
契約を実装することにより、スコープの値を保存用にフォーマットできるようにしました。
例えば、1つのアプリケーションで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
{
/**
* オブジェクトを指定されたドライバの機能スコープ識別子にキャスト
*/
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はリッチな値も格納できます。
例えば、アプリケーションの「Buy
now」ボタンに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
Note: リッチな値を使用する場合、その機能が
false
以外の値なら、「アクティブ」とみなすことを知っておいてください。
条件付きwhen
メソッドを呼び出すと、その機能のリッチな値が最初のクロージャに提供されます。
Feature::when('purchase-button',
fn ($color) => /* ... */,
fn () => /* ... */,
);
同様に、条件付きのunless
メソッドを呼び出すと、その機能のリッチな値がオプションの2番目のクロージャへ渡されます。
Feature::unless('purchase-button',
fn () => /* ... */,
fn ($color) => /* ... */,
);
複数の機能の取得
value
メソッドで、指定スコープに対する複数の機能を取得できます。
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
{
/**
* 全アプリケーションサービスの初期起動処理
*/
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,
// ]
Eagerロード
Pennantは、一つのリクエストで解決したすべての機能のメモリ内キャッシュを保持しますが、それでも性能上の問題が発生する可能性があります。これを軽減するために、Pennantは機能の値をEagerロードする機能を提供しています。
これを理解するため、機能がアクティブであるかをループの中でチェックしていると考えてください。
use Laravel\Pennant\Feature;
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
データベースドライバを使用していると仮定すると、このコードはループ内のすべてのユーザーに対してデータベースクエリを実行することになり、潜在的に数百のクエリを実行することになります。しかし、Pennantのload
メソッドを使えば、ユーザーやスコープのコレクションの値をEagerロードでき、この潜在的なパフォーマンスのボトルネックを取り除けます。
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
メソッドを使用して、機能の"on"と"off"を切り替えます。
use Laravel\Pennant\Feature;
// デフォルトのスコープの機能を有効にする
Feature::activate('new-api');
// 指定のスコープの機能を無効にする
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');
Note: これは、Pennantのストレージドライバにより保存された、解決済みの機能値のみを更新します。アプリケーションの機能定義も更新する必要があります。
機能の削除
時には、ストレージから機能全体を取り除くのが、有用な場合があります。これは、アプリケーションからその機能を削除した場合や、その機能の定義に調整を加え、全ユーザーにロールアウトする状況で、典型的に必要になります。
ある機能に対して保存されているすべての値を削除するには、purge
メソッドを使用します。
// 1機能の削除
Feature::purge('new-api');
// 複数機能の削除
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
テスト
機能フラグを操作するコードをテストする場合、機能フラグの返り値をテストでコントロールする最も簡単な方法は、その機能を単純に再定義することです。たとえば、アプリケーションのサービスプロバイダに次のような機能が定義されているとしましょう。
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;
public function test_it_can_control_feature_values()
{
Feature::define('purchase-button', 'seafoam-green');
$this->assertSame('seafoam-green', Feature::value('purchase-button'));
}
クラスベースの機能でも、同様のアプローチが可能です。
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の既存ストレージドライバが、どれもあなたのアプリケーションのニーズに合わない場合は、独自のストレージドライバを書いてください。カスタムドライバは、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
を見てください。
Note: Laravelは、拡張機能を格納するディレクトリを用意していません。好きな場所に自由に配置できます。この例では、
RedisFeatureDriver
を格納するために、Extensions
ディレクトリを作成しました。
ドライバの登録
ドライバを実装したら、Laravelに登録します。Pennantへドライバを追加するには、Feature
ファサードが提供するextend
メソッドをしようします。extend
メソッドは、アプリケーションの[サービスプロバイダ](providers.html
のboot
メソッドから呼び出す必要があります。
<?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
{
/**
* アプリケーションの全サービスの登録
*/
public function register(): void
{
// ...
}
/**
* 全アプリケーションサービスの初期起動処理
*/
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\RetrievingKnownFeature
このイベントは、特定のスコープに対するリクエスト中に、既知の機能を初めて取得したときに発行します。このイベントは、アプリケーション全体で使用する機能フラグに対するメトリックを作成し、追跡するのに便利です。
Laravel\Pennant\Events\RetrievingUnknownFeature
このイベントは、特定のスコープへのリクエスト中に、未知の機能を初めて取得したときに発行します。このイベントは、ある機能フラグを削除するつもりが、誤ってアプリケーション全体にそのフラグへの参照を残してしまった場合に便利です。
例えば、このイベントをリッスンして、それが発生したときに、report
や例外を投げるのが便利でしょう。
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
use Laravel\Pennant\Events\RetrievingUnknownFeature;
class EventServiceProvider extends ServiceProvider
{
/**
* アプリケーションのその他のイベントの登録
*/
public function boot(): void
{
Event::listen(function (RetrievingUnknownFeature $event) {
report("Resolving unknown feature [{$event->feature}].");
});
}
}
Laravel\Pennant\Events\DynamicallyDefiningFeature
このイベントは、クラスベースの機能をリクエスト中に、初めて動的にチェックするときに発行します。