Laravel 10.x 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ドライバをサポートしています。もしくは、算出済み機能フラグ値を、リレーショナルデータベースに永続的に保存する、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),
        });
    }
}

ご覧の通り、この機能では以下のようなルールを設けています。

最初に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我用意しているarraydatabaseストレージドライバは、すべての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つの新しい色をテストする場合を考えてみましょう。機能定義からtruefalseを返す代わりに、文字列を返せます。

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

値の更新

機能の値を初めて解決するとき、裏で動作しているドライバはその結果をストレージへ保存します。これは、リクエスト間で一貫したユーザー体験を保証するために、多くの場合必要です。しかし、時には、保存している機能の値を手作業で更新したい場合もあるでしょう。

このために、activatedeactivateメソッドを使用して、機能の"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

このイベントは、クラスベースの機能をリクエスト中に、初めて動的にチェックするときに発行します。

ドキュメント章別ページ

ヘッダー項目移動

注目:アイコン:ページ内リンク設置(リンクがないヘッダーへの移動では、リンクがある以前のヘッダーのハッシュをURLへ付加します。

移動

クリックで即時移動します。

設定

適用ボタンクリック後に、全項目まとめて適用されます。

カラーテーマ
和文指定 Pagination
和文指定 Scaffold
Largeスクリーン表示幅
インデント
本文フォント
コードフォント
フォント適用確認

フォントの指定フィールドから、フォーカスが外れると、当ブロックの内容に反映されます。EnglishのDisplayもPreviewしてください。

フォント設定時、表示に不具合が出た場合、当サイトのクッキーを削除してください。

バックスラッシュを含むインライン\Code\Blockの例です。

以下はコードブロックの例です。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * ユーザに関連する電話レコードを取得
     */
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }
}

設定を保存する前に、表示が乱れないか必ず確認してください。CSSによるフォントファミリー指定の知識がない場合は、フォントを変更しないほうが良いでしょう。

キーボード・ショートカット

オープン操作

PDC

ページ(章)移動の左オフキャンバスオープン

HA

ヘッダー移動モーダルオープン

MS

移動/設定の右オフキャンバスオープン

ヘッダー移動

T

最初のヘッダーへ移動

E

最後のヘッダーへ移動

NJ

次ヘッダー(H2〜H4)へ移動

BK

前ヘッダー(H2〜H4)へ移動

その他

?

このヘルプページ表示
閉じる