Lines 100.00% 34 / 34
Methods 100.00% 4 / 4
Classes 100.00% 1 / 1
Covered by tests of size
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
28final 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}