diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index f6116df4f92a..e1140c070a41 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -2,9 +2,11 @@ namespace Illuminate\Cache; +use BackedEnum; use Closure; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Support\InteractsWithTime; +use UnitEnum; class RateLimiter { @@ -38,13 +40,15 @@ public function __construct(Cache $cache) /** * Register a named limiter configuration. * - * @param string $name + * @param \BackedEnum|\UnitEnum|string $name * @param \Closure $callback * @return $this */ - public function for(string $name, Closure $callback) + public function for($name, Closure $callback) { - $this->limiters[$name] = $callback; + $resolvedName = $this->resolveLimiterName($name); + + $this->limiters[$resolvedName] = $callback; return $this; } @@ -52,12 +56,14 @@ public function for(string $name, Closure $callback) /** * Get the given named rate limiter. * - * @param string $name + * @param \BackedEnum|\UnitEnum|string $name * @return \Closure|null */ - public function limiter(string $name) + public function limiter($name) { - return $this->limiters[$name] ?? null; + $resolvedName = $this->resolveLimiterName($name); + + return $this->limiters[$resolvedName] ?? null; } /** @@ -248,4 +254,19 @@ public function cleanRateLimiterKey($key) { return preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities($key)); } + + /** + * Resolve the rate limiter name + * + * @param \BackedEnum|\UnitEnum|string $name + * @return string + */ + private function resolveLimiterName($name): string + { + return match (true) { + $name instanceof BackedEnum => $name->value, + $name instanceof UnitEnum => $name->name, + default => (string) $name, + }; + } } diff --git a/src/Illuminate/Queue/Middleware/RateLimited.php b/src/Illuminate/Queue/Middleware/RateLimited.php index 4ebdc2677e93..31a9f9c53eb9 100644 --- a/src/Illuminate/Queue/Middleware/RateLimited.php +++ b/src/Illuminate/Queue/Middleware/RateLimited.php @@ -2,10 +2,12 @@ namespace Illuminate\Queue\Middleware; +use BackedEnum; use Illuminate\Cache\RateLimiter; use Illuminate\Cache\RateLimiting\Unlimited; use Illuminate\Container\Container; use Illuminate\Support\Arr; +use UnitEnum; class RateLimited { @@ -33,14 +35,18 @@ class RateLimited /** * Create a new middleware instance. * - * @param string $limiterName + * @param \BackedEnum|\UnitEnum|string $limiterName * @return void */ public function __construct($limiterName) { $this->limiter = Container::getInstance()->make(RateLimiter::class); - $this->limiterName = $limiterName; + $this->limiterName = match (true) { + $limiterName instanceof BackedEnum => $limiterName->value, + $limiterName instanceof UnitEnum => $limiterName->name, + default => (string) $limiterName, + }; } /** diff --git a/tests/Cache/RateLimiterTest.php b/tests/Cache/RateLimiterTest.php new file mode 100644 index 000000000000..6f871d382094 --- /dev/null +++ b/tests/Cache/RateLimiterTest.php @@ -0,0 +1,51 @@ + [BackedEnumNamedRateLimiter::API, 'api'], + 'uses UnitEnum' => [UnitEnumNamedRateLimiter::THIRD_PARTY, 'THIRD_PARTY'], + 'uses normal string' => ['yolo', 'yolo'], + 'uses int' => [100, '100'], + ]; + } + + #[DataProvider('registerNamedRateLimiterDataProvider')] + public function testRegisterNamedRateLimiter(mixed $name, string $expected): void + { + $reflectedLimitersProperty = new ReflectionProperty(RateLimiter::class, 'limiters'); + $reflectedLimitersProperty->setAccessible(true); + + $rateLimiter = new RateLimiter($this->createMock(Cache::class)); + $rateLimiter->for($name, fn () => Limit::perMinute(100)); + + $limiters = $reflectedLimitersProperty->getValue($rateLimiter); + + $this->assertArrayHasKey($expected, $limiters); + + $limiterClosure = $rateLimiter->limiter($name); + + $this->assertNotNull($limiterClosure); + } +} + +enum BackedEnumNamedRateLimiter: string +{ + case API = 'api'; +} + +enum UnitEnumNamedRateLimiter +{ + case THIRD_PARTY; +} diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index d3208b741cfb..de6e0e0f785a 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -32,6 +32,30 @@ public function testUnlimitedJobsAreExecuted() $this->assertJobRanSuccessfully(RateLimitedTestJob::class); } + public function testUnlimitedJobsAreExecutedUsingBackedEnum() + { + $rateLimiter = $this->app->make(RateLimiter::class); + + $rateLimiter->for(BackedEnumNamedRateLimited::FOO, function ($job) { + return Limit::none(); + }); + + $this->assertJobRanSuccessfully(RateLimitedTestJobUsingBackedEnum::class); + $this->assertJobRanSuccessfully(RateLimitedTestJobUsingBackedEnum::class); + } + + public function testUnlimitedJobsAreExecutedUsingUnitEnum() + { + $rateLimiter = $this->app->make(RateLimiter::class); + + $rateLimiter->for(UnitEnumNamedRateLimited::LARAVEL, function ($job) { + return Limit::none(); + }); + + $this->assertJobRanSuccessfully(RateLimitedTestJobUsingUnitEnum::class); + $this->assertJobRanSuccessfully(RateLimitedTestJobUsingUnitEnum::class); + } + public function testRateLimitedJobsAreNotExecutedOnLimitReached2() { $cache = m::mock(Cache::class); @@ -315,3 +339,47 @@ public function middleware() return [(new RateLimited('test'))->dontRelease()]; } } + +enum BackedEnumNamedRateLimited: string +{ + case FOO = 'bar'; +} + +enum UnitEnumNamedRateLimited +{ + case LARAVEL; +} + +class RateLimitedTestJobUsingBackedEnum +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + } + + public function middleware() + { + return [new RateLimited(BackedEnumNamedRateLimited::FOO)]; + } +} + +class RateLimitedTestJobUsingUnitEnum +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + } + + public function middleware() + { + return [new RateLimited(UnitEnumNamedRateLimited::LARAVEL)]; + } +}