> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel Pennant 実践ユースケース

> Laravel Pennantの基本を超えた実践的なパターンを解説します。チーム単位のスコープ、緊急キルスイッチ、ダークローンチ、管理コマンドなど、公式ドキュメントだけでは分からないノウハウを紹介します。

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

***

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

個人ユーザーではなくチーム(テナント)単位でフラグを管理したい場合は、デフォルトスコープをチームに変更します。

```php theme={null}
// AppServiceProvider
Feature::resolveScopeUsing(fn () => Auth::user()?->currentTeam);
```

これだけで `Feature::active('billing-v2')` が自動的に現在のチームを対象にします。チームメンバーが誰であっても同じチームに属する限り同じ結果が返るため、UI の一貫性が保たれます。

チームのサインアップ日に応じた段階的ロールアウトの例です。

```php theme={null}
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);
});
```

特定チームのみ有効化したい場合（エンタープライズ顧客への先行提供など）:

```php theme={null}
// デプロイ後に特定チームだけ手動で有効化
Feature::for($earlyAccessTeam)->activate('new-billing');
```

***

## 緊急キルスイッチ（`before` メソッドの活用）

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

```php theme={null}
<?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を触らずに機能を止められます。デプロイ不要のキルスイッチです。

<Tip>
  `before` が `null` を返すと `resolve()` が実行されます。`false` を返すと即座に非アクティブとして扱われます。緊急時以外は `null` を返しておきましょう。
</Tip>

***

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

特定日時に自動的に全ユーザーへ公開したいケース。`before` メソッドで実装できます。

```php theme={null}
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();
}
```

```ini theme={null}
# .env
FEATURES_NEW_API_ROLLOUT_DATE=2025-04-01
```

これで `2025-04-01` を過ぎると自動的に全ユーザーに公開されます。デプロイなし、DBなし、Artisanコマンドなしで自動化できます。

***

## ダークローンチ（Shadow Mode）

新しいロジックをユーザーに見せずに本番データで動かし、旧ロジックと結果を比較するパターンです。問題がなければフラグをオンにするだけでリリース完了です。

```php theme={null}
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` イベントはフィーチャーの値が**初めて**解決されたときのみ発火します。これを使ってユーザーのバリアント割り当てを記録します。

```php theme={null}
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,
    ]);
});
```

コンバージョン発生時に別途記録すれば、バリアントごとのコンバージョン率を集計できます。

```php theme={null}
// 購入完了時
Event::dispatch(new PurchaseCompleted($user, $product));
```

```php theme={null}
// PurchaseCompleted リスナー
analytics()->track('purchase_completed', [
    'user_id'              => $user->id,
    'ab_purchase_button'   => Feature::for($user)->value('purchase-button'),
]);
```

<Info>
  `FeatureResolved` と `FeatureRetrieved` の違い: `FeatureResolved` は初回評価時のみ発火、`FeatureRetrieved` は毎回のチェックで発火します。割り当て記録には `FeatureResolved`、ページビュー追跡には `FeatureRetrieved` が適しています。
</Info>

***

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

キューのジョブでは認証済みユーザーが存在しないため、フィーチャーチェックが想定外の挙動をすることがあります。ジョブにスコープを明示的に持たせましょう。

```php theme={null}
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);
        }
    }
}
```

ジョブのディスパッチ時にフラグを評価してジョブに渡す方法もあります。

```php theme={null}
// コントローラーで評価してジョブに渡す
$useNewLayout = Feature::for($user)->active('new-digest-layout');

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

***

## Artisan コマンドによる管理 UI

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

```php theme={null}
<?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} スコープで有効");
    }
}
```

```shell theme={null}
php artisan feature activate new-checkout
php artisan feature deactivate new-checkout
php artisan feature status new-checkout
```

***

## フィーチャー名の安全なリファクタリング（`Name` アトリビュート）

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

```php theme={null}
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` アトリビュート         | 安全なクラスリファクタリング  |

<Card title="Laravel Pennant ガイド" icon="flag" href="/jp/pennant">
  インストールから基本的な使い方まではガイドページを参照してください。
</Card>
