サイトを更新しています。 数日間、レイアウトや翻訳に不具合が出ることがあります。ドキュメントは引き続きご利用いただけます。表示が崩れている場合は、後ほど再読み込みしてください。

ホーム Laravel 10.x Laravel Pennant

Laravel Pennant

10.x 2026年3月7日

#はじめに

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 は、array ドライバーを使ってインメモリの配列に機能フラグの値を保存することをサポートしています。また、デフォルトのストレージ機構として、リレーショナルデータベースに永続的に保存する database ドライバーも利用できます。

#機能の定義

機能を定義するには、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 機能が特定のユーザーに対して初めてチェックされるとき、クロージャの結果がストレージドライバーに保存されます。次回以降はストレージから値が取得され、クロージャは呼び出されません。

便利なことに、機能定義が単にロトの結果を返すだけの場合は、クロージャを省略できます:

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),
        };
    }
}
Примечание

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

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

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

<?php

namespace App\Features;

class NewApi
{
    /**
     * 保存される機能名。
     *
     * @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
{
    /**
     * リソースの一覧を表示する。
     */
    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']);
Примечание

Artisan コマンドやキュージョブなど HTTP コンテキスト外で Pennant を使う場合は、通常 機能のスコープを明示的に指定するべきです。あるいは、認証済みの HTTP コンテキストと未認証のコンテキストの両方を考慮した デフォルトスコープ を定義してもよいでしょう。

#クラスベースの機能の確認

クラスベースの機能をチェックする場合は、機能名の代わりにクラス名を指定します。

<?php

namespace App\Http\Controllers;

use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;

class PodcastController
{
    /**
     * リソースの一覧を表示する。
     */
    public function index(Request $request): Response
    {
        return Feature::active(NewApi::class)
                ? $this->resolveNewApiResponse($request)
                : $this->resolveLegacyApiResponse($request);
    }

    // ...
}

#条件付き実行

when メソッドは、機能が有効な場合に指定したクロージャを流暢に実行できます。さらに、機能が無効な場合に実行される第2のクロージャも指定できます。

<?php

namespace App\Http\Controllers;

use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;

class PodcastController
{
    /**
     * Display a listing of the resource.
     */
    public function index(Request $request): Response
    {
        return Feature::when(NewApi::class,
            fn () => $this->resolveNewApiResponse($request),
            fn () => $this->resolveLegacyApiResponse($request),
        );
    }

    // ...
}

unless メソッドは when メソッドの逆で、機能が無効な場合に最初のクロージャを実行します。

return Feature::unless(NewApi::class,
    fn () => $this->resolveLegacyApiResponse($request),
    fn () => $this->resolveNewApiResponse($request),
);

#HasFeatures トレイト

Pennant の HasFeatures トレイトは、アプリケーションの User モデル(または機能を持つ他のモデル)に追加できます。これにより、モデルから直接機能を簡単にチェックできるようになります。

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;

class User extends Authenticatable
{
    use HasFeatures;

    // ...
}

トレイトをモデルに追加すると、features メソッドを呼び出して簡単に機能をチェックできます。

if ($user->features()->active('new-api')) {
    // ...
}

もちろん、features メソッドは機能とやり取りするための多くの便利なメソッドも提供しています。

// 値の取得...
$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');

#Nullable スコープ

機能をチェックする際に渡したスコープが null で、かつ機能の定義が nullable 型やユニオン型に null を含めることで 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) {
Feature::define('new-api', fn (User|null $user) => match (true) {
    $user === null => true,
    $user->isInternalTeamMember() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 100),
});

#スコープの識別

Pennant の組み込み arraydatabase ストレージドライバーは、すべての PHP データ型および Eloquent モデルのスコープ識別子を正しく保存できます。しかし、サードパーティ製の Pennant ドライバーを使用している場合、そのドライバーは Eloquent モデルやアプリケーションのカスタム型の識別子を正しく保存できないことがあります。

このため、Pennant はアプリケーション内で Pennant スコープとして使うオブジェクトに FeatureScopeable インターフェイスを実装することで、スコープ値の保存形式をカスタマイズできるようにしています。

例えば、アプリケーションで組み込みの 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 morph map を使っている場合は、Pennant にも morph map を使わせて、保存された機能をアプリケーション構造から切り離すことができます。

これを実現するには、サービスプロバイダーで 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' が有効 -->
@elsefeature('purchase-button', 'seafoam-green')
    <!-- 'seafoam-green' が有効 -->
@elsefeature('purchase-button', 'tart-orange')
    <!-- 'tart-orange' が有効 -->
@endfeature
Примечание

リッチな値を使う場合、機能は false 以外の値を持つとき「有効」とみなされることに注意してください。

条件付きの when メソッドを呼ぶと、機能のリッチな値が最初のクロージャに渡されます。

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

同様に、条件付きの unless メソッドを呼ぶと、機能のリッチな値がオプションの第二クロージャに渡されます。

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

#複数機能の取得

values メソッドを使うと、指定したスコープに対する複数の機能を取得できます。

Feature::values(['billing-v2', 'purchase-button']);

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]

また、all メソッドを使うと、指定したスコープに対するすべての定義済み機能の値を取得できます。

Feature::all();

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
//     'site-redesign' => true,
// ]

ただし、クラスベースの機能は動的に登録され、明示的にチェックされるまで Pennant に認識されません。つまり、現在のリクエスト中にチェックされていない場合、all メソッドの結果にクラスベースの機能が含まれないことがあります。

all メソッドで常に機能クラスを含めたい場合は、Pennant の機能ディスカバリー機能を使えます。始めるには、アプリケーションのサービスプロバイダーのいずれかで discover メソッドを呼び出します。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    /**
     * アプリケーションのサービスをブートストラップします。
     */
    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 メソッドを使えば、ユーザーのコレクションやスコープに対して機能フラグの値を 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',
]);

#値の更新

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

これを実現するには、activatedeactivate メソッドを使って機能を「オン」または「オフ」に切り替えられます。

use Laravel\Pennant\Feature;

// デフォルトスコープに対して機能を有効化...
Feature::activate('new-api');

// 指定したスコープに対して機能を無効化...
Feature::for($user->team)->deactivate('billing-v2');

activate メソッドに第二引数を渡すことで、機能にリッチな値を手動で設定することも可能です。

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

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

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

#一括更新

保存された機能値を一括で更新するには、activateForEveryonedeactivateForEveryone メソッドを使います。

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

use Laravel\Pennant\Feature;

Feature::activateForEveryone('new-api');

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

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

Feature::deactivateForEveryone('new-api');
Примечание

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

#フィーチャーのパージ

ストレージからフィーチャー全体をパージすることが役立つ場合があります。これは通常、アプリケーションからフィーチャーを削除した場合や、フィーチャー定義を変更して全ユーザーに展開したい場合に必要です。

purge メソッドを使って、フィーチャーの保存されたすべての値を削除できます。

// 単一のフィーチャーをパージする...
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

特定のフィーチャーリストを除いてすべてのフィーチャーをパージすることも可能です。例えば、「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;

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

#ストアの設定

テスト時に 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 メソッドを使います。extend メソッドはアプリケーションの サービスプロバイダー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

このイベントは、クラスベースのフィーチャーがリクエスト中に初めて動的にチェックされる際に発行されます。