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

# Lottery 類別

> 說明如何使用 Illuminate\Support\Lottery 優雅地實作機率式操作。同時介紹套件開發中的使用情境與測試時的固定化。

## 什麼是 Lottery 類別

`Illuminate\Support\Lottery` 是可將機率式操作以流暢 API 表達的工具類別。可以簡潔地寫出如「每 100 個請求執行 1 次處理」、「僅對部分請求記錄詳細日誌」等模式。

<Info>
  實作位於 `src/Illuminate/Support/Lottery.php`。Laravel 亦於 Session GC、Cache Lock 的 prune 等框架內部活用此類別。
</Info>

```mermaid theme={null}
flowchart LR
  A["Lottery::odds(1, 100)"] --> B{"隨機判定"}
  B -->|"中選 (1%)"| C["執行 winner callback"]
  B -->|"未中 (99%)"| D["執行 loser callback"]
```

## 基本用法

### 以整數比例指定機率

以 `Lottery::odds($chances, $outOf)` 指定「$outOf 次中 $chances 次中選」的機率。

```php theme={null}
use Illuminate\Support\Lottery;

Lottery::odds(1, 100)           // 每 100 次 1 次
    ->winner(fn () => $this->runMaintenance())
    ->loser(fn () => null)
    ->choose();
```

### 以小數指定機率

省略 `$outOf` 並傳入 `0.0`〜`1.0` 的小數，則會直接作為機率使用。

```php theme={null}
Lottery::odds(0.01)             // 1% 的機率
    ->winner(fn () => $this->sample())
    ->choose();
```

<Warning>
  小數指定時，值若超過 `1.0` 會拋出 `RuntimeException`。
</Warning>

### 不帶 callback 回傳布林值

若未設定 winner / loser，`choose()` 中選時回傳 `true`，未中時回傳 `false`。

```php theme={null}
$shouldSample = Lottery::odds(1, 50)->choose(); // bool
```

### 執行多次

若對 `choose($times)` 傳入次數，會回傳結果的陣列。

```php theme={null}
$results = Lottery::odds(1, 2)
    ->winner(fn () => 'win')
    ->loser(fn () => 'lose')
    ->choose(10);

// 例：['win', 'lose', 'win', 'win', 'lose', ...]
```

### 作為 Callable 傳入

`Lottery` 實例實作了 `__invoke`，可直接傳入接受 callable 的 API。

```php theme={null}
// 作為 DB::whenQueryingForLongerThan 的第 2 個引數傳入的範例
DB::whenQueryingForLongerThan(
    Interval::seconds(5),
    Lottery::odds(1, 5)->winner(function ($connection) {
        // 偵測到慢查詢時以 1/5 機率送出警示
        Alert::send("Slow query on {$connection->getName()}");
    })
);
```

## 實務使用情境

### 1. Cache 的 prune（每 100 次僅執行 1 次）

適合刪除過期記錄等無需每次執行的維護處理。

```php theme={null}
Lottery::odds(1, 100)
    ->winner(fn () => Cache::store('database')->flush())
    ->choose();
```

### 2. 遙測、取樣（僅部分請求記錄詳細日誌）

若將所有請求都記錄成本過高，可用於取樣。

```php theme={null}
Lottery::odds(1, 20)
    ->winner(function () use ($request) {
        Log::channel('telemetry')->info('Request sampled', [
            'url'      => $request->url(),
            'duration' => microtime(true) - LARAVEL_START,
            'memory'   => memory_get_peak_usage(true),
        ]);
    })
    ->choose();
```

### 3. 類似 A/B 測試的行為

以機率將使用者分派至 2 條程式碼路徑。

```php theme={null}
$result = Lottery::odds(1, 2)
    ->winner(fn ($user) => $this->newCheckoutFlow($user))
    ->loser(fn ($user) => $this->legacyCheckoutFlow($user))
    ->choose();
```

### 4. 作為 Scheduler 的輔助，隨機執行定期任務

當希望在多台伺服器上避免重複執行且隨機執行某任務時可用。

```php theme={null}
// app/Console/Kernel.php
$schedule->call(function () {
    Lottery::odds(1, 3)
        ->winner(fn () => Artisan::call('cache:prune-stale-tags'))
        ->choose();
})->everyMinute();
```

## Laravel 框架內的機率模式

Laravel 也於框架內部廣泛使用機率式維護處理。部分實作因為在 `Lottery` 類別新增之前撰寫，直接使用 `random_int()`，但思維相同。

<Steps>
  <Step title="Session：垃圾回收">
    `Illuminate\Session\Middleware\StartSession::configHitsLottery()` 使用 `config/session.php` 的 `lottery` 設定，以 `random_int` 進行機率判定並執行 GC。

    ```php theme={null}
    // config/session.php
    'lottery' => [2, 100], // 每 100 個請求 2 次

    // StartSession 內部（直接使用 random_int）
    protected function configHitsLottery(array $config): bool
    {
        return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
    }
    ```
  </Step>

  <Step title="DatabaseLock：過期 Lock 的 prune">
    `Illuminate\Cache\DatabaseLock::acquire()` 於每次取得 Lock 時以相同比例模式刪除過期 Lock。

    ```php theme={null}
    // config/cache.php（database driver）
    'lock_lottery' => [2, 100], // 每 100 次執行 2 次 prune

    // DatabaseLock::acquire() 內部（直接使用 random_int）
    if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
        $this->pruneExpiredLocks();
    }
    ```
  </Step>

  <Step title="DB::whenQueryingForLongerThan — 傳入 Lottery 類別的範例">
    因 `Lottery` 實例可作為 callable 傳入，可直接用於慢查詢偵測的 callback。

    ```php theme={null}
    DB::whenQueryingForLongerThan(
        Interval::seconds(5),
        Lottery::odds(1, 5)->winner(function ($connection) {
            Log::warning("Slow query on {$connection->getName()}");
        })
    );
    ```
  </Step>
</Steps>

<Tip>
  Session 與 DatabaseLock 直接使用 `random_int()`，而使用 `Lottery` 類別的優點是可透過 `alwaysWin()` / `alwaysLose()` / `fix()` 於測試中控制結果。套件開發選擇 `Lottery` 類別可提升可測試性。
</Tip>

## 測試時的使用

具備隨機性的程式碼測試中，可使用 `Lottery` 提供的測試 API。

### `Lottery::alwaysWin()` — 恆使中選

```php theme={null}
public function test_maintenance_runs_on_win(): void
{
    $ranMaintenance = false;

    Lottery::alwaysWin(function () use (&$ranMaintenance) {
        Lottery::odds(1, 100)
            ->winner(function () use (&$ranMaintenance) {
                $ranMaintenance = true;
            })
            ->choose();
    });

    $this->assertTrue($ranMaintenance);
}
```

### `Lottery::alwaysLose()` — 恆使未中

```php theme={null}
public function test_maintenance_skipped_on_lose(): void
{
    $ranMaintenance = false;

    Lottery::alwaysLose(function () use (&$ranMaintenance) {
        Lottery::odds(1, 100)
            ->winner(function () use (&$ranMaintenance) {
                $ranMaintenance = true;
            })
            ->choose();
    });

    $this->assertFalse($ranMaintenance);
}
```

### `Lottery::fix()` — 以序列固定結果

可以 `true`/`false` 的陣列控制多次呼叫的結果。

```php theme={null}
public function test_alternating_results(): void
{
    Lottery::fix([true, false, true, false]);

    $results = Lottery::odds(1, 100)
        ->winner(fn () => 'winner')
        ->loser(fn () => 'loser')
        ->choose(4);

    $this->assertSame(['winner', 'loser', 'winner', 'loser'], $results);

    Lottery::determineResultNormally(); // 測試後務必還原
}
```

<Warning>
  `alwaysWin()` / `alwaysLose()` / `fix()` 會變更全域靜態屬性。請務必於測試的 `tearDown()` 呼叫 `Lottery::determineResultNormally()`。
</Warning>

```php theme={null}
protected function tearDown(): void
{
    Lottery::determineResultNormally();

    parent::tearDown();
}
```

### `Lottery::setResultFactory()` — 注入自訂 factory

若需更精細的控制，可使用自訂 factory。

```php theme={null}
Lottery::setResultFactory(function ($chances, $outOf) {
    // 恆使中選的自訂邏輯
    return true;
});

// 測試後重置
Lottery::determineResultNormally();
```

## 套件開發的活用

### 於服務提供者註冊

若於套件的服務提供者中組入維護處理，可透過 `Lottery` 分散負載。

```php theme={null}
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;

class AcmeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->booted(function () {
            Lottery::odds(1, 100)
                ->winner(fn () => $this->pruneExpiredRecords())
                ->choose();
        });
    }

    protected function pruneExpiredRecords(): void
    {
        $this->app['db']->table('acme_logs')
            ->where('expires_at', '<', now())
            ->delete();
    }
}
```

### 從設定值讀取 odds

若允許使用者從設定檔變更機率，會更方便調整。

```php theme={null}
$lottery = config('acme.prune_lottery', [1, 100]);

Lottery::odds(...$lottery)
    ->winner(fn () => $this->pruneExpiredRecords())
    ->choose();
```

```php theme={null}
// config/acme.php
return [
    // [中選數, 嘗試數] = 每 100 個請求執行 1 次 prune
    'prune_lottery' => [1, 100],
];
```

### 於 Middleware 進行取樣

```php theme={null}
use Illuminate\Support\Lottery;

class SampleTelemetryMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        Lottery::odds(1, 50)
            ->winner(fn () => $this->recordTelemetry($request, $response))
            ->choose();

        return $response;
    }
}
```

## API 參考

| 方法                                    | 說明                           |
| ------------------------------------- | ---------------------------- |
| `Lottery::odds($chances, $outOf)`     | 產生 Lottery 實例的靜態 factory     |
| `->winner(callable $callback)`        | 設定中選時的 callback              |
| `->loser(callable $callback)`         | 設定未中時的 callback              |
| `->choose($times = null)`             | 執行 Lottery。指定 `$times` 則回傳陣列 |
| `Lottery::alwaysWin($callback)`       | 測試用：恆使中選                     |
| `Lottery::alwaysLose($callback)`      | 測試用：恆使未中                     |
| `Lottery::fix($sequence)`             | 測試用：以序列固定結果                  |
| `Lottery::determineResultNormally()`  | 重置測試用固定                      |
| `Lottery::setResultFactory(callable)` | 注入自訂判定邏輯                     |

## 相關頁面

<Columns cols={2}>
  <Card title="Macroable trait" icon="puzzle-piece" href="/zh-TW/advanced/macroable">
    學習為既有類別新增方法的擴充模式。
  </Card>

  <Card title="Conditionable trait" icon="git-branch" href="/zh-TW/advanced/conditionable">
    學習透過 `when()` / `unless()` 設計條件分支鏈。
  </Card>
</Columns>


## Related topics

- [Fluent 類別](/zh-TW/advanced/fluent.md)
- [字串操作（Str 類別）](/zh-TW/strings.md)
- [通知（Notifications）](/zh-TW/notifications.md)
- [Eloquent 的自訂 Cast](/zh-TW/advanced/eloquent-casts.md)
- [Laravel Pennant](/zh-TW/pennant.md)
