ez-php / rate-limiter
Rate limiter module for the ez-php framework — array, Redis, and cache-backed drivers with ThrottleMiddleware
Requires
- php: ^8.5
- ez-php/cache: ^2.0
- ez-php/contracts: ^2.0
- ez-php/http: ^2.0
Requires (Dev)
- ez-php/docker: ^2.0
- ez-php/testing-application: ^2.0
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.9
- 2.3.8
- 2.3.7
- 2.3.6
- 2.3.5
- 2.3.4
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.1
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.14.0
- 1.13.1
- 1.13.0
- 1.12.2
- 1.12.1
- 1.12.0
- 1.11.2
- 1.11.1
- 1.11.0
- 1.10.0
- 1.9.2
- 1.9.1
- 1.9.0
- 1.8.0
- 1.7.1
- 1.7.0
- 1.6.1
- 1.6.0
- 1.5.1
- 1.5.0
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.0
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.1
- 1.0.0
- 0.9.3
- 0.9.2
- 0.9.1
- 0.9.0
- 0.8.6
- 0.8.5
- 0.8.4
- 0.8.3
- 0.8.2
- 0.8.1
- 0.8.0
- 0.7.0
- 0.6.0
- 0.5.0
This package is auto-updated.
Last update: 2026-09-16 14:36:14 UTC
README
Request throttling for ez-php applications — three backends, a unified interface, and a plug-in ThrottleMiddleware.
Installation
composer require ez-php/rate-limiter
Drivers
| Driver | Persistence | External requirement | Concurrency-safe |
|---|---|---|---|
ArrayDriver |
In-process (lost on restart) | None | No — single-process/test use only |
FileDriver |
Files on disk | None | Yes — flock(LOCK_EX), single host |
RedisDriver |
Redis | ext-redis |
Yes — atomic INCR |
CacheDriver |
Delegates to ez-php/cache |
Any configured cache driver | Driver-dependent |
Warning:
ArrayDriveruses a plain PHP array without atomic operations. Concurrent requests (e.g. PHP-FPM workers) can race and both be allowed through simultaneously. UseFileDriver,RedisDriverorCacheDriverin production.
FileDriver
For single-host deployments that have no Redis. Counters persist across requests and
process restarts, and the whole read-modify-write in attempt() runs under an
exclusive flock(), so concurrent PHP-FPM workers cannot both slip past the limit.
use EzPhp\RateLimiter\FileDriver; $limiter = new FileDriver('/var/www/storage/rate-limiter'); $limiter->attempt('login:1.2.3.4', 5, 60);
Or via config:
// config/rate_limiter.php return [ 'driver' => 'file', 'file' => ['path' => __DIR__ . '/../storage/rate-limiter'], ];
-
One file per key; the key is
sha1()-hashed, so a key containing/or..is safe. -
Single host only. Locking is filesystem-level — a shared network mount across hosts is not supported. Use
RedisDriverfor multi-host deployments. -
Counter files are not swept automatically. A key is reclaimed when it is next read, but keys that stop being used (e.g. one per client IP) leave files behind. Call
prune()from cron or a scheduled command if the endpoint is exposed to untrusted traffic:$deleted = $limiter->prune(); // removes expired counters, returns how many
Live counters are left untouched.
prune()is only onFileDriver, not onRateLimiterInterface— the other drivers expire their own keys.
Basic usage
use EzPhp\RateLimiter\ArrayDriver; $limiter = new ArrayDriver(); if (!$limiter->attempt('login:' . $ip, maxAttempts: 5, decaySeconds: 60)) { // Too many attempts — respond with 429 } $limiter->remainingAttempts('login:' . $ip, 5); // how many hits are still allowed $limiter->resetAttempts('login:' . $ip); // clear the counter (e.g. on success)
Using the facade
RateLimiter mirrors RateLimiterInterface as static methods, backed by a managed
singleton set during RateLimiterServiceProvider::boot(). Without a service provider
it falls back to an in-memory ArrayDriver, so it's safe to call in code paths that
run before the provider boots (e.g. early tests).
use EzPhp\RateLimiter\RateLimiter; if (!RateLimiter::attempt('login:' . $ip, maxAttempts: 5, decaySeconds: 60)) { $retryIn = RateLimiter::availableIn('login:' . $ip); // respond 429, e.g. with a Retry-After: $retryIn header } RateLimiter::tooManyAttempts('login:' . $ip, 5); RateLimiter::remainingAttempts('login:' . $ip, 5); RateLimiter::resetAttempts('login:' . $ip);
In tests, call RateLimiter::resetInstance() in tearDown() to clear the static
singleton between test cases.
ThrottleMiddleware
Plug into the framework middleware pipeline for per-IP global or per-route throttling:
// Global — in AppServiceProvider::boot() $app->middleware(new ThrottleMiddleware($limiter, maxAttempts: 60, decaySeconds: 60)); // Per-route $router->get('/login', [LoginController::class, 'store']) ->middleware(new ThrottleMiddleware($limiter, maxAttempts: 5, decaySeconds: 60));
The middleware:
- Resolves the client IP from
X-Forwarded-For(first value) or falls back toREMOTE_ADDR. - Returns HTTP 429 with body
Too Many Requestswhen the limit is exceeded. - Adds
X-RateLimit-LimitandX-RateLimit-Remainingheaders on every passing response.
Service provider
Register RateLimiterServiceProvider in provider/modules.php:
\EzPhp\RateLimiter\RateLimiterServiceProvider::class,
Create config/rate_limiter.php:
<?php return [ 'driver' => env('RATE_LIMITER_DRIVER', 'array'), // array | redis | cache 'redis' => [ 'host' => env('REDIS_HOST', '127.0.0.1'), 'port' => (int) env('REDIS_PORT', 6379), 'database' => (int) env('REDIS_RATE_LIMITER_DB', 0), ], ];
Interface
interface RateLimiterInterface { public function attempt(string $key, int $maxAttempts, int $decaySeconds): bool; public function tooManyAttempts(string $key, int $maxAttempts): bool; public function remainingAttempts(string $key, int $maxAttempts): int; public function resetAttempts(string $key): void; public function availableIn(string $key): int; // seconds until the window resets; 0 if expired/absent }
License
MIT