Passer au contenu principal

Qu’est-ce que la classe Lottery

Illuminate\Support\Lottery est une classe utilitaire qui permet d’exprimer des opérations basées sur des probabilités via une API fluide. Elle permet d’écrire simplement des schémas tels que « exécuter un traitement une fois toutes les 100 requêtes » ou « ne journaliser en détail qu’une partie des requêtes ».
L’implémentation se trouve dans src/Illuminate/Support/Lottery.php. Laravel l’utilise également en interne, par exemple pour le garbage collector de session ou l’élagage des verrous de cache.

Utilisation de base

Spécifier la probabilité avec un ratio entier

Lottery::odds($chances, $outOf) définit la probabilité « chancesgainssurchances gains sur outOf ».
use Illuminate\Support\Lottery;

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

Spécifier la probabilité avec un décimal

Si $outOf est omis et qu’un décimal entre 0.0 et 1.0 est passé, il est utilisé tel quel comme probabilité.
Lottery::odds(0.01)             // 1 % de probabilité
    ->winner(fn () => $this->sample())
    ->choose();
En cas de spécification décimale, une valeur supérieure à 1.0 lève une RuntimeException.

Retourner un booléen sans callback

Sans winner / loser définis, choose() retourne true en cas de gain et false en cas de perte.
$shouldSample = Lottery::odds(1, 50)->choose(); // bool

Exécuter plusieurs fois

Passer un nombre à choose($times) retourne un tableau de résultats.
$results = Lottery::odds(1, 2)
    ->winner(fn () => 'win')
    ->loser(fn () => 'lose')
    ->choose(10);

// ex. : ['win', 'lose', 'win', 'win', 'lose', ...]

Passer comme callable

L’instance Lottery implémente __invoke et peut donc être passée directement à une API attendant un callable.
// Exemple : passé en second argument de DB::whenQueryingForLongerThan
DB::whenQueryingForLongerThan(
    Interval::seconds(5),
    Lottery::odds(1, 5)->winner(function ($connection) {
        // Lorsqu'une requête lente est détectée, envoyer une alerte 1 fois sur 5
        Alert::send("Slow query on {$connection->getName()}");
    })
);

Cas d’utilisation concrets

1. Élagage du cache (une fois sur cent)

Idéal pour les opérations de maintenance sans nécessité d’exécution systématique, comme la suppression d’enregistrements expirés.
Lottery::odds(1, 100)
    ->winner(fn () => Cache::store('database')->flush())
    ->choose();

2. Échantillonnage de télémétrie (log détaillé sur une partie des requêtes)

Utile quand journaliser toutes les requêtes serait coûteux.
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. Comportement de type A/B testing

Répartit les utilisateurs de manière probabiliste entre deux chemins de code.
$result = Lottery::odds(1, 2)
    ->winner(fn ($user) => $this->newCheckoutFlow($user))
    ->loser(fn ($user) => $this->legacyCheckoutFlow($user))
    ->choose();

4. En complément du Scheduler pour exécuter aléatoirement une tâche périodique

Utile pour éviter les exécutions redondantes sur plusieurs serveurs tout en exécutant aléatoirement une tâche.
// app/Console/Kernel.php
$schedule->call(function () {
    Lottery::odds(1, 3)
        ->winner(fn () => Artisan::call('cache:prune-stale-tags'))
        ->choose();
})->everyMinute();

Schémas probabilistes dans le framework Laravel

Laravel utilise abondamment des traitements de maintenance probabilistes en interne. Certaines implémentations, antérieures à la classe Lottery, utilisent directement random_int(), mais reposent sur la même idée.
1

Session : ramasse-miettes

Illuminate\Session\Middleware\StartSession::configHitsLottery() s’appuie sur la configuration lottery de config/session.php et utilise random_int pour décider probabilistiquement d’exécuter le GC.
// config/session.php
'lottery' => [2, 100], // 2 requêtes sur 100

// Interne de StartSession (utilisation directe de random_int)
protected function configHitsLottery(array $config): bool
{
    return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
}
2

DatabaseLock : élagage des verrous expirés

Illuminate\Cache\DatabaseLock::acquire() applique le même ratio à chaque acquisition de verrou pour supprimer les verrous expirés.
// config/cache.php (driver database)
'lock_lottery' => [2, 100], // élagage 2 fois sur 100

// Interne de DatabaseLock::acquire() (utilisation directe de random_int)
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
    $this->pruneExpiredLocks();
}
3

DB::whenQueryingForLongerThan — exemple avec la classe Lottery

Comme une instance Lottery peut être passée en tant que callable, elle s’intègre directement au callback de détection de requêtes lentes.
DB::whenQueryingForLongerThan(
    Interval::seconds(5),
    Lottery::odds(1, 5)->winner(function ($connection) {
        Log::warning("Slow query on {$connection->getName()}");
    })
);
Alors que Session et DatabaseLock utilisent directement random_int(), la classe Lottery offre l’avantage de pouvoir maîtriser les résultats en tests avec alwaysWin() / alwaysLose() / fix(). En développement de packages, choisir Lottery améliore la testabilité.

Utilisation pendant les tests

Pour tester du code intégrant de l’aléatoire, servez-vous des API de test fournies par Lottery.

Lottery::alwaysWin() — toujours gagnant

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() — toujours perdant

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() — fixer les résultats par séquence

Vous pouvez contrôler les résultats de plusieurs appels avec un tableau de true/false.
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(); // à toujours restaurer après le test
}
alwaysWin() / alwaysLose() / fix() modifient des propriétés statiques globales. Appelez impérativement Lottery::determineResultNormally() dans le tearDown() du test.
protected function tearDown(): void
{
    Lottery::determineResultNormally();

    parent::tearDown();
}

Lottery::setResultFactory() — injecter une fabrique personnalisée

Pour un contrôle plus fin, utilisez une fabrique personnalisée.
Lottery::setResultFactory(function ($chances, $outOf) {
    // Logique personnalisée : toujours gagner
    return true;
});

// Réinitialiser après le test
Lottery::determineResultNormally();

Utilisation en développement de packages

Enregistrement dans un service provider

Pour intégrer un traitement de maintenance dans le service provider d’un package, Lottery permet d’en répartir la charge.
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();
    }
}

Charger la probabilité depuis la configuration

Rendre la probabilité configurable via un fichier facilite l’ajustement par l’utilisateur.
$lottery = config('acme.prune_lottery', [1, 100]);

Lottery::odds(...$lottery)
    ->winner(fn () => $this->pruneExpiredRecords())
    ->choose();
// config/acme.php
return [
    // [gains, essais] = élagage 1 requête sur 100
    'prune_lottery' => [1, 100],
];

Échantillonnage dans un middleware

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;
    }
}

Référence API

MéthodeDescription
Lottery::odds($chances, $outOf)Fabrique statique qui crée une instance Lottery
->winner(callable $callback)Définit le callback en cas de gain
->loser(callable $callback)Définit le callback en cas de perte
->choose($times = null)Exécute Lottery. Un tableau est retourné si $times est spécifié
Lottery::alwaysWin($callback)Pour les tests : toujours gagner
Lottery::alwaysLose($callback)Pour les tests : toujours perdre
Lottery::fix($sequence)Pour les tests : fixer les résultats en séquence
Lottery::determineResultNormally()Réinitialise la fixation utilisée pour les tests
Lottery::setResultFactory(callable)Injecte une logique de décision personnalisée

Pages associées

Trait Macroable

Découvrez le pattern d’extension permettant d’ajouter des méthodes à une classe existante.

Trait Conditionable

Découvrez la conception de chaînes de branchement conditionnel avec when() / unless().
Dernière modification le 13 juillet 2026