Lines
100.00%
34 / 34
Methods
100.00%
4 / 4
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | ||
| check | 100.00% | 25 / 25 | 100.00% | 1 / 1 | 2 | ||
| peek | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 1 | ||
| getHeaders | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| 28 | final class RateLimiter | |
| 29 | { | |
| 30 | /** @var array<string, int|string> */ | |
| 31 | private array $headersStorage = []; | |
| 32 | ||
| 33 | /** @var array<string, int|string> */ | |
| 34 | public array $headers { | |
| 35 | get => $this->headersStorage; | |
| 36 | } | |
| 37 | ||
| 38 | private readonly StorageInterface $storage; | |
| 39 | ||
| 40 | public function __construct(?CacheItemPoolInterface $cache = null, ?StorageInterface $storage = null) | |
| 41 | { | |
| 42 | $this->storage = $storage ?? ($cache !== null ? new CacheStorage($cache) : new InMemoryStorage()); | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * Checks if a request should be allowed for the given key. | |
| 47 | */ | |
| 48 | public function check(string $key, int $limit, int $intervalSeconds): bool | |
| 49 | { | |
| 50 | $limit = max(1, $limit); | |
| 51 | $intervalSeconds = max(1, $intervalSeconds); | |
| 52 | ||
| 53 | $factory = new RateLimiterFactory(config: [ | |
| 54 | 'id' => 'api', | |
| 55 | 'policy' => 'fixed_window', | |
| 56 | 'limit' => $limit, | |
| 57 | 'interval' => $intervalSeconds . ' seconds', | |
| 58 | ], storage: $this->storage); | |
| 59 | ||
| 60 | $limiter = $factory->create($key); | |
| 61 | $rateLimit = $limiter->consume(1); | |
| 62 | ||
| 63 | $resetTime = $rateLimit->getRetryAfter()->getTimestamp(); | |
| 64 | ||
| 65 | if ($rateLimit->isAccepted()) { | |
| 66 | $this->headersStorage = [ | |
| 67 | 'X-RateLimit-Limit' => $limit, | |
| 68 | 'X-RateLimit-Remaining' => $rateLimit->getRemainingTokens(), | |
| 69 | 'X-RateLimit-Reset' => $resetTime, | |
| 70 | ]; | |
| 71 | ||
| 72 | return true; | |
| 73 | } | |
| 74 | ||
| 75 | $this->headersStorage = [ | |
| 76 | 'X-RateLimit-Limit' => $limit, | |
| 77 | 'X-RateLimit-Remaining' => 0, | |
| 78 | 'X-RateLimit-Reset' => $resetTime, | |
| 79 | 'Retry-After' => max(1, $resetTime - time()), | |
| 80 | ]; | |
| 81 | ||
| 82 | return false; | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * Reports whether the given key still has budget left, without consuming a token. | |
| 87 | */ | |
| 88 | public function peek(string $key, int $limit, int $intervalSeconds): bool | |
| 89 | { | |
| 90 | $factory = new RateLimiterFactory(config: [ | |
| 91 | 'id' => 'api', | |
| 92 | 'policy' => 'fixed_window', | |
| 93 | 'limit' => max(1, $limit), | |
| 94 | 'interval' => max(1, $intervalSeconds) . ' seconds', | |
| 95 | ], storage: $this->storage); | |
| 96 | ||
| 97 | // consume(0) never rejects, so exhaustion shows up as zero remaining tokens | |
| 98 | return $factory->create($key)->consume(0)->getRemainingTokens() > 0; | |
| 99 | } | |
| 100 | ||
| 101 | /** | |
| 102 | * @return array<string, int|string> | |
| 103 | */ | |
| 104 | public function getHeaders(): array | |
| 105 | { | |
| 106 | return $this->headers; | |
| 107 | } | |
| 108 | } |