メインコンテンツへスキップ

Documentation Index

Fetch the complete documentation index at: https://kawax.biz/llms.txt

Use this file to discover all available pages before exploring further.

基本的な使い方はガイドページを参照してください。このページでは公式ドキュメントには載っていない実践的なパターンを紹介します。

マルチテナント SaaS でのチームスコープ

個人ユーザーではなくチーム(テナント)単位でフラグを管理したい場合は、デフォルトスコープをチームに変更します。
// AppServiceProvider
Feature::resolveScopeUsing(fn () => Auth::user()?->currentTeam);
これだけで Feature::active('billing-v2') が自動的に現在のチームを対象にします。チームメンバーが誰であっても同じチームに属する限り同じ結果が返るため、UI の一貫性が保たれます。 チームのサインアップ日に応じた段階的ロールアウトの例です。
use App\Models\Team;
use Illuminate\Support\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('new-billing', function (Team $team) {
    // 2024年以降に登録したチームは即時有効
    if ($team->created_at->isAfter(new Carbon('2024-01-01'))) {
        return true;
    }

    // 2022〜2023年登録は10%ずつロールアウト
    if ($team->created_at->isAfter(new Carbon('2022-01-01'))) {
        return Lottery::odds(1 / 10);
    }

    // 古いチームは慎重に1%から
    return Lottery::odds(1 / 100);
});
特定チームのみ有効化したい場合(エンタープライズ顧客への先行提供など):
// デプロイ後に特定チームだけ手動で有効化
Feature::for($earlyAccessTeam)->activate('new-billing');

緊急キルスイッチ(before メソッドの活用)

本番でバグが発覚したとき、コードロールバックなしで即座に機能を無効化できます。クラスベースのフィーチャーに before メソッドを追加すると、ストレージの値より先にチェックが走ります。
<?php

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Facades\Config;

class NewCheckout
{
    /**
     * 本番バグ発生時に config で即時無効化できる緊急スイッチ
     */
    public function before(User $user): mixed
    {
        // config/features.php の値、または env で制御
        if (Config::boolean('features.new_checkout_disabled', false)) {
            return false; // 全員に対して即時無効化
        }

        // 管理者は常に有効(デバッグのため)
        if ($user->isAdmin()) {
            return true;
        }

        return null; // null を返すと通常の resolve() へ進む
    }

    public function resolve(User $user): mixed
    {
        return $user->isPremium() || Lottery::odds(1 / 5);
    }
}
環境変数 FEATURES_NEW_CHECKOUT_DISABLED=true を設定するだけで、DBを触らずに機能を止められます。デプロイ不要のキルスイッチです。
beforenull を返すと resolve() が実行されます。false を返すと即座に非アクティブとして扱われます。緊急時以外は null を返しておきましょう。

スケジュールされたロールアウト

特定日時に自動的に全ユーザーへ公開したいケース。before メソッドで実装できます。
public function before(User $user): mixed
{
    $rolloutDate = Config::get('features.new_api.rollout_date');

    if ($rolloutDate && Carbon::parse($rolloutDate)->isPast()) {
        return true; // ロールアウト日を過ぎたら全員に公開
    }

    return null; // まだの場合は通常の resolve() へ
}

public function resolve(User $user): mixed
{
    // ロールアウト前は社内メンバーのみ
    return $user->isInternalTeamMember();
}
# .env
FEATURES_NEW_API_ROLLOUT_DATE=2025-04-01
これで 2025-04-01 を過ぎると自動的に全ユーザーに公開されます。デプロイなし、DBなし、Artisanコマンドなしで自動化できます。

ダークローンチ(Shadow Mode)

新しいロジックをユーザーに見せずに本番データで動かし、旧ロジックと結果を比較するパターンです。問題がなければフラグをオンにするだけでリリース完了です。
class RecommendationController
{
    public function index(Request $request)
    {
        $legacyResult = $this->getLegacyRecommendations($request->user());

        // shadow モードでは新アルゴリズムを実行して比較するが、表示は旧ロジック
        if (Feature::for($request->user())->active('recommendation-v2-shadow')) {
            try {
                $newResult = $this->getNewRecommendations($request->user());

                // 差異をログに記録(ユーザーには見せない)
                if ($legacyResult !== $newResult) {
                    Log::channel('shadow_mode')->info('recommendation diff', [
                        'user_id' => $request->user()->id,
                        'legacy'  => $legacyResult,
                        'new'     => $newResult,
                    ]);
                }
            } catch (\Throwable $e) {
                Log::error('shadow mode error', ['error' => $e->getMessage()]);
            }
        }

        // レスポンスは常に旧ロジックの結果
        return response()->json($legacyResult);
    }
}
ログを確認して差異がなくなったら、フラグを recommendation-v2 に切り替えるだけです。

イベントを使った A/B テスト結果の収集

公式ドキュメントには FeatureRetrieved イベントの言及はありますが、実際のA/Bテスト集計パターンは示されていません。 FeatureResolved イベントはフィーチャーの値が初めて解決されたときのみ発火します。これを使ってユーザーのバリアント割り当てを記録します。
use Laravel\Pennant\Events\FeatureResolved;
use Illuminate\Support\Facades\Event;

// AppServiceProvider や EventServiceProvider に登録
Event::listen(function (FeatureResolved $event) {
    if ($event->feature !== 'purchase-button') {
        return;
    }

    // ユーザーへのバリアント割り当て時点で記録
    analytics()->identify($event->scope?->id, [
        'ab_purchase_button' => $event->value,
    ]);
});
コンバージョン発生時に別途記録すれば、バリアントごとのコンバージョン率を集計できます。
// 購入完了時
Event::dispatch(new PurchaseCompleted($user, $product));
// PurchaseCompleted リスナー
analytics()->track('purchase_completed', [
    'user_id'              => $user->id,
    'ab_purchase_button'   => Feature::for($user)->value('purchase-button'),
]);
FeatureResolvedFeatureRetrieved の違い: FeatureResolved は初回評価時のみ発火、FeatureRetrieved は毎回のチェックで発火します。割り当て記録には FeatureResolved、ページビュー追跡には FeatureRetrieved が適しています。

キューイングされたジョブでのスコープ

キューのジョブでは認証済みユーザーが存在しないため、フィーチャーチェックが想定外の挙動をすることがあります。ジョブにスコープを明示的に持たせましょう。
class SendWeeklyDigest implements ShouldQueue
{
    public function __construct(
        private readonly User $user
    ) {}

    public function handle(): void
    {
        // NG: 認証ユーザーが存在しないため常に false になる
        // if (Feature::active('new-digest-layout')) { ... }

        // OK: ユーザーを明示的に渡す
        if (Feature::for($this->user)->active('new-digest-layout')) {
            $this->sendNewLayout($this->user);
        } else {
            $this->sendLegacyLayout($this->user);
        }
    }
}
ジョブのディスパッチ時にフラグを評価してジョブに渡す方法もあります。
// コントローラーで評価してジョブに渡す
$useNewLayout = Feature::for($user)->active('new-digest-layout');

SendWeeklyDigest::dispatch($user, $useNewLayout);

Artisan コマンドによる管理 UI

フラグを管理する簡単な Artisan コマンドを作れば、デプロイなしで本番フラグを操作できます。
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Pennant\Feature;

class ManageFeatureFlag extends Command
{
    protected $signature = 'feature {action : activate|deactivate|status} {feature : Feature name}';
    protected $description = '機能フラグを管理する';

    public function handle(): void
    {
        $feature = $this->argument('feature');

        match ($this->argument('action')) {
            'activate'   => $this->activate($feature),
            'deactivate' => $this->deactivate($feature),
            'status'     => $this->status($feature),
            default      => $this->error('不明なアクションです'),
        };
    }

    private function activate(string $feature): void
    {
        Feature::activateForEveryone($feature);
        $this->info("✓ {$feature} を全ユーザーに対して有効化しました");
    }

    private function deactivate(string $feature): void
    {
        Feature::deactivateForEveryone($feature);
        $this->info("✓ {$feature} を全ユーザーに対して無効化しました");
    }

    private function status(string $feature): void
    {
        $count = \DB::table('features')
            ->where('name', $feature)
            ->where('value', json_encode(true))
            ->count();

        $this->line("{$feature}: {$count} スコープで有効");
    }
}
php artisan feature activate new-checkout
php artisan feature deactivate new-checkout
php artisan feature status new-checkout

フィーチャー名の安全なリファクタリング(Name アトリビュート)

クラスベースのフィーチャーをリネームする際、DBに保存済みのフラグ名が変わると全ユーザーのフラグがリセットされてしまいます。Name アトリビュートで保存名を固定しましょう。
use Laravel\Pennant\Attributes\Name;

// 旧クラス名: CheckoutV2 → 新クラス名: NewCheckoutExperience
// DBには常に 'checkout-v2' として保存される
#[Name('checkout-v2')]
class NewCheckoutExperience
{
    public function resolve(User $user): mixed
    {
        return $user->isPremium();
    }
}
これでクラス名を自由にリファクタリングしても、DBのデータはそのまま維持されます。

まとめ

公式ドキュメントの基本を押さえたら、以下のパターンで Pennant の真価が発揮されます。
パターン使いどころ
チームスコープマルチテナント SaaS
before キルスイッチ本番バグの緊急対応
スケジュールロールアウト日時指定のリリース自動化
ダークローンチ安全な新アルゴリズムの本番検証
FeatureResolved イベントA/B テスト結果の正確な収集
ジョブの明示的スコープキュー環境での正確なフラグ評価
管理 Artisan コマンドデプロイなしの本番フラグ操作
Name アトリビュート安全なクラスリファクタリング

Laravel Pennant ガイド

インストールから基本的な使い方まではガイドページを参照してください。
Last modified on May 19, 2026