Skip to content

Commit d07c240

Browse files
committed
feat(cache): add BackedEnum and UnitEnum support to RateLimiter
- Add enum support to for() and limiter() methods for named rate limiters - Add resolveLimiterName() helper using enum_value() - Add comprehensive enum tests covering BackedEnum, UnitEnum, and string interoperability Following Laravel's approach where only named limiter methods support enums (for/limiter), not key-based methods (attempt/hit/etc) since those typically use dynamic concatenated keys.
1 parent 63c417e commit d07c240

File tree

2 files changed

+154
-4
lines changed

2 files changed

+154
-4
lines changed

src/cache/src/RateLimiter.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
namespace Hypervel\Cache;
66

7+
use BackedEnum;
78
use Closure;
89
use Hyperf\Support\Traits\InteractsWithTime;
910
use Hypervel\Cache\Contracts\Factory as Cache;
11+
use UnitEnum;
12+
13+
use function Hypervel\Support\enum_value;
1014

1115
class RateLimiter
1216
{
@@ -33,19 +37,27 @@ public function __construct(Cache $cache)
3337
/**
3438
* Register a named limiter configuration.
3539
*/
36-
public function for(string $name, Closure $callback): static
40+
public function for(BackedEnum|UnitEnum|string $name, Closure $callback): static
3741
{
38-
$this->limiters[$name] = $callback;
42+
$this->limiters[$this->resolveLimiterName($name)] = $callback;
3943

4044
return $this;
4145
}
4246

4347
/**
4448
* Get the given named rate limiter.
4549
*/
46-
public function limiter(string $name): ?Closure
50+
public function limiter(BackedEnum|UnitEnum|string $name): ?Closure
51+
{
52+
return $this->limiters[$this->resolveLimiterName($name)] ?? null;
53+
}
54+
55+
/**
56+
* Resolve the rate limiter name.
57+
*/
58+
private function resolveLimiterName(BackedEnum|UnitEnum|string $name): string
4759
{
48-
return $this->limiters[$name] ?? null;
60+
return (string) enum_value($name);
4961
}
5062

5163
/**
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Cache;
6+
7+
use Hypervel\Cache\Contracts\Factory as Cache;
8+
use Hypervel\Cache\RateLimiter;
9+
use Hypervel\Tests\TestCase;
10+
use Mockery as m;
11+
use PHPUnit\Framework\Attributes\DataProvider;
12+
use ReflectionProperty;
13+
14+
enum BackedEnumNamedRateLimiter: string
15+
{
16+
case API = 'api';
17+
case Web = 'web';
18+
}
19+
20+
enum UnitEnumNamedRateLimiter
21+
{
22+
case ThirdParty;
23+
case Internal;
24+
}
25+
26+
/**
27+
* @internal
28+
* @coversNothing
29+
*/
30+
class RateLimiterEnumTest extends TestCase
31+
{
32+
#[DataProvider('registerNamedRateLimiterDataProvider')]
33+
public function testRegisterNamedRateLimiter(mixed $name, string $expected): void
34+
{
35+
$reflectedLimitersProperty = new ReflectionProperty(RateLimiter::class, 'limiters');
36+
37+
$rateLimiter = new RateLimiter(m::mock(Cache::class));
38+
$rateLimiter->for($name, fn () => 'limit');
39+
40+
$limiters = $reflectedLimitersProperty->getValue($rateLimiter);
41+
42+
$this->assertArrayHasKey($expected, $limiters);
43+
44+
$limiterClosure = $rateLimiter->limiter($name);
45+
46+
$this->assertNotNull($limiterClosure);
47+
}
48+
49+
public static function registerNamedRateLimiterDataProvider(): array
50+
{
51+
return [
52+
'uses BackedEnum' => [BackedEnumNamedRateLimiter::API, 'api'],
53+
'uses UnitEnum' => [UnitEnumNamedRateLimiter::ThirdParty, 'ThirdParty'],
54+
'uses normal string' => ['yolo', 'yolo'],
55+
];
56+
}
57+
58+
public function testForWithBackedEnumStoresUnderValue(): void
59+
{
60+
$rateLimiter = new RateLimiter(m::mock(Cache::class));
61+
$rateLimiter->for(BackedEnumNamedRateLimiter::API, fn () => 'api-limit');
62+
63+
// Can retrieve with enum
64+
$this->assertNotNull($rateLimiter->limiter(BackedEnumNamedRateLimiter::API));
65+
66+
// Can also retrieve with string value
67+
$this->assertNotNull($rateLimiter->limiter('api'));
68+
69+
// Closure returns expected value
70+
$this->assertSame('api-limit', $rateLimiter->limiter(BackedEnumNamedRateLimiter::API)());
71+
}
72+
73+
public function testForWithUnitEnumStoresUnderName(): void
74+
{
75+
$rateLimiter = new RateLimiter(m::mock(Cache::class));
76+
$rateLimiter->for(UnitEnumNamedRateLimiter::ThirdParty, fn () => 'third-party-limit');
77+
78+
// Can retrieve with enum
79+
$this->assertNotNull($rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty));
80+
81+
// Can also retrieve with string name (PascalCase)
82+
$this->assertNotNull($rateLimiter->limiter('ThirdParty'));
83+
84+
// Closure returns expected value
85+
$this->assertSame('third-party-limit', $rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty)());
86+
}
87+
88+
public function testLimiterReturnsNullForNonExistentEnum(): void
89+
{
90+
$rateLimiter = new RateLimiter(m::mock(Cache::class));
91+
92+
$this->assertNull($rateLimiter->limiter(BackedEnumNamedRateLimiter::Web));
93+
$this->assertNull($rateLimiter->limiter(UnitEnumNamedRateLimiter::Internal));
94+
}
95+
96+
public function testBackedEnumAndStringInteroperability(): void
97+
{
98+
$rateLimiter = new RateLimiter(m::mock(Cache::class));
99+
100+
// Register with string
101+
$rateLimiter->for('api', fn () => 'string-registered');
102+
103+
// Retrieve with BackedEnum that has same value
104+
$limiter = $rateLimiter->limiter(BackedEnumNamedRateLimiter::API);
105+
106+
$this->assertNotNull($limiter);
107+
$this->assertSame('string-registered', $limiter());
108+
}
109+
110+
public function testUnitEnumAndStringInteroperability(): void
111+
{
112+
$rateLimiter = new RateLimiter(m::mock(Cache::class));
113+
114+
// Register with string (matching UnitEnum name)
115+
$rateLimiter->for('ThirdParty', fn () => 'string-registered');
116+
117+
// Retrieve with UnitEnum
118+
$limiter = $rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty);
119+
120+
$this->assertNotNull($limiter);
121+
$this->assertSame('string-registered', $limiter());
122+
}
123+
124+
public function testMultipleEnumLimitersCanCoexist(): void
125+
{
126+
$rateLimiter = new RateLimiter(m::mock(Cache::class));
127+
128+
$rateLimiter->for(BackedEnumNamedRateLimiter::API, fn () => 'api-limit');
129+
$rateLimiter->for(BackedEnumNamedRateLimiter::Web, fn () => 'web-limit');
130+
$rateLimiter->for(UnitEnumNamedRateLimiter::ThirdParty, fn () => 'third-party-limit');
131+
$rateLimiter->for('custom', fn () => 'custom-limit');
132+
133+
$this->assertSame('api-limit', $rateLimiter->limiter(BackedEnumNamedRateLimiter::API)());
134+
$this->assertSame('web-limit', $rateLimiter->limiter(BackedEnumNamedRateLimiter::Web)());
135+
$this->assertSame('third-party-limit', $rateLimiter->limiter(UnitEnumNamedRateLimiter::ThirdParty)());
136+
$this->assertSame('custom-limit', $rateLimiter->limiter('custom')());
137+
}
138+
}

0 commit comments

Comments
 (0)