Explica cómo implementar operaciones probabilísticas de forma elegante con Illuminate\Support\Lottery. Presenta casos de uso en desarrollo de paquetes y cómo fijar el resultado en tests.
Illuminate\Support\Lottery es una utilidad que expresa operaciones basadas en probabilidad con una API fluida. Permite describir de forma sencilla patrones como «ejecuta este proceso solo 1 vez cada 100 peticiones» o «registra logs detallados solo en una fracción de las peticiones».
La implementación está en src/Illuminate/Support/Lottery.php. Laravel también la utiliza internamente, por ejemplo en el GC de sesiones o para podar locks de caché.
Con Lottery::odds($chances, $outOf) indicas la probabilidad «gana $chances de cada $outOf intentos».
use Illuminate\Support\Lottery;Lottery::odds(1, 100) // 1 vez de cada 100 ->winner(fn () => $this->runMaintenance()) ->loser(fn () => null) ->choose();
Como la instancia de Lottery implementa __invoke, la puedes pasar directamente a APIs que reciban un callable.
// Ejemplo pasándolo como segundo argumento de DB::whenQueryingForLongerThanDB::whenQueryingForLongerThan( Interval::seconds(5), Lottery::odds(1, 5)->winner(function ($connection) { // Cuando se detecta una consulta lenta, envía alerta con probabilidad 1/5 Alert::send("Slow query on {$connection->getName()}"); }));
Patrones probabilísticos dentro del framework Laravel
Laravel utiliza ampliamente el patrón probabilístico para tareas de mantenimiento internas. Parte de esas implementaciones se escribieron antes de que existiera la clase Lottery, así que usan random_int() directamente, pero comparten la misma idea.
1
Session: garbage collection
Illuminate\Session\Middleware\StartSession::configHitsLottery() utiliza el ajuste lottery de config/session.php y decide con random_int si ejecutar el GC.
// config/session.php'lottery' => [2, 100], // 2 veces por cada 100 peticiones// Interior de StartSession (usa random_int directamente)protected function configHitsLottery(array $config): bool{ return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];}
2
DatabaseLock: poda de locks caducados
Illuminate\Cache\DatabaseLock::acquire() aplica el mismo patrón para eliminar locks expirados en cada adquisición.
// config/cache.php (driver database)'lock_lottery' => [2, 100], // ejecuta la poda 2 veces por cada 100// Interior de DatabaseLock::acquire() (usa random_int directamente)if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) { $this->pruneExpiredLocks();}
3
DB::whenQueryingForLongerThan — ejemplo pasando una Lottery
Como una instancia de Lottery es un callable, se puede pasar directamente como callback de detección de consultas lentas.
Frente a Session y DatabaseLock, que usan random_int() directamente, la clase Lottery te permite controlar el resultado durante los tests con alwaysWin() / alwaysLose() / fix(). En el desarrollo de paquetes, optar por Lottery mejora la testabilidad.
Lottery::fix() — fija los resultados con una secuencia
Puedes controlar el resultado de varias llamadas con un array 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(); // restablece tras el test}
alwaysWin() / alwaysLose() / fix() alteran una propiedad estática global. Llama siempre a Lottery::determineResultNormally() en el tearDown() de tu test.
protected function tearDown(): void{ Lottery::determineResultNormally(); parent::tearDown();}
Lottery::setResultFactory() — inyectar una factory personalizada
Si necesitas un control más fino, usa una factory personalizada.
Lottery::setResultFactory(function ($chances, $outOf) { // Lógica personalizada que gana siempre return true;});// Restablece después del testLottery::determineResultNormally();