tobento / app-rate-limiter
App rate limiter support.
Requires
- php: >=8.0
- symfony/rate-limiter: ^6.0
- tobento/app: ^1.0.7
- tobento/app-cache: ^1.0.1
- tobento/app-http: ^1.0 || ^1.1
- tobento/app-migration: ^1.0
Requires (Dev)
- nyholm/psr7: ^1.4
- phpunit/phpunit: ^9.5
- tobento/app-event: ^1.0.1
- tobento/app-user: ^1.0
- tobento/service-filesystem: ^1.0.5
- vimeo/psalm: ^4.0
README
Rate limiter support for the app using the Symfony - Rate Limiter component as default implementation.
Table of Contents
Getting Started
Add the latest version of the app rate limiter project running this command.
composer require tobento/app-rate-limiter
Requirements
- PHP 8.0 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
Rate Limiter Boot
The rate limiter boot does the following:
- installs and loads rate limiter config file
- implements rate limiter interfaces
use Tobento\App\AppFactory; use Tobento\App\RateLimiter\RateLimiterCreatorInterface; use Tobento\App\RateLimiter\RegistriesInterface; use Tobento\App\RateLimiter\FingerprintInterface; // Create the app $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\RateLimiter\Boot\RateLimiter::class); $app->booting(); // Implemented interfaces: $limiterCreator = $app->get(RateLimiterCreatorInterface::class); $registries = $app->get(RegistriesInterface::class); $fingerprint = $app->get(FingerprintInterface::class); // Run the app $app->run();
Rate Limiter Config
The configuration for the rate limiter is located in the app/config/rate_limiter.php
file at the default App Skeleton config location where you can specify named rate limiters for your application.
Basic Usage
Using The Rate Limiter Creator
After having booted the rate limiter, inject it in any service or controller. You may consider booting the App Http - Routing Boot as well in order to get HTTP and Routing support.
use Tobento\App\AppFactory; use Tobento\App\RateLimiter\RateLimiterCreatorInterface; use Tobento\App\RateLimiter\Symfony\Registry\SlidingWindow; use Tobento\App\Http\Exception\TooManyRequestsException; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\ErrorHandler::class); $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\RateLimiter\Boot\RateLimiter::class); $app->booting(); // Routes: $app->route('POST', 'login', function(ServerRequestInterface $request, RateLimiterCreatorInterface $limiterCreator) { // create a rate limiter: $limiter = $limiterCreator->createFromRegistry( // a unique identifier of the client: id: $request->getServerParams()['REMOTE_ADDR'] ?? null, // define the rate limiter to use: registry: new SlidingWindow(limit: 5, interval: '5 minutes'), ); // next hit the limiter and check if attempts exceeded: if ($limiter->hit()->isAttemptsExceeded()) { throw new TooManyRequestsException( retryAfter: $limiter->availableIn(), message: sprintf('Too Many Requests. Please retry after %d seconds.', $limiter->availableIn()), headers: [ 'X-RateLimit-Limit' => $limiter->maxAttempts(), 'X-RateLimit-Remaining' => $limiter->remainingAttempts(), 'X-RateLimit-Reset' => $limiter->availableAt()->getTimestamp(), ], ); } // you may reset the attempts: // $limiter->reset(); return 'response'; }); // Run the app: $app->run();
Check out the Available Rate Limiter Registries for its available limiter registries.
Using The RateLimitRequests Middleware
Use the RateLimitRequests::class
middleware to rate limit routes easily.
use Tobento\App\AppFactory; use Tobento\App\RateLimiter\Middleware\RateLimitRequests; use Tobento\App\RateLimiter\Symfony\Registry\SlidingWindow; use Tobento\App\Http\Exception\TooManyRequestsException; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Http\Boot\ErrorHandler::class); $app->boot(\Tobento\App\Http\Boot\Routing::class); $app->boot(\Tobento\App\Http\Boot\RequesterResponser::class); // for redirection support $app->boot(\Tobento\App\RateLimiter\Boot\RateLimiter::class); $app->booting(); // Routes: $app->route('POST', 'login', function() { // being rate limited! return 'response'; })->middleware([ RateLimitRequests::class, // define the rate limiter to use: 'registry' => new SlidingWindow(limit: 5, interval: '5 minutes'), // or by named rate limiter: //'registry' => 'login', ]); $app->route('POST', 'register', function() { // being rate limited! return 'response'; })->middleware([ RateLimitRequests::class, 'registry' => new SlidingWindow(limit: 5, interval: '5 minutes'), // You may specify a redirect uri or route // which will redirect instead of throwing a TooManyRequestsException: 'redirectUri' => '/register', 'redirectRoute' => 'register', // You may specify a redirect message to be flashed. // The :seconds parameter value will be set by the middleware. 'message' => 'Too many attempts. Please retry after :seconds seconds.', 'messageLevel' => 'error', // default 'messageKey' => 'email', // or null default ]); // Run the app: $app->run();
Available Rate Limiter Registries
Factory
The Factory
registry may be used to create a rate limiter from any factory:
use Tobento\App\RateLimiter\Registry\Factory; use Tobento\App\RateLimiter\Symfony\RateLimiterFactory; $limiter = $limiterCreator->createFromRegistry( id: 'a-unique-identifier', registry: new Factory( factory: RateLimiterFactory::class, config: [ 'policy' => 'sliding_window', 'limit' => 5, 'interval' => '5 Minutes', ], ), );
Fixed Window
The FixedWindow
registry creates a Symfony fixed window rate limiter:
use Tobento\App\RateLimiter\Symfony\Registry\FixedWindow; use Tobento\App\RateLimiter\Symfony\RateLimiterFactory; $limiter = $limiterCreator->createFromRegistry( id: 'a-unique-identifier', registry: new FixedWindow( limit: 100, interval: '5 Minutes', // you may specify an id prefix: id: 'api', // you may change the storage used: storage: 'inmemory', // 'cache' is default // you may change the cache used if using the cache storage: cache: 'api-ratelimiter', // 'ratelimiter' is default ), );
You may define a cache for the name ratelimiter
(default) or api-ratelimiter
(custom) in the App Cache - Config, otherwise the primary cache is used as default.
Named
The Named
registry may be used to create a rate limiter from a named rate limiter:
use Tobento\App\RateLimiter\Registry\Named; use Tobento\App\RateLimiter\Symfony\RateLimiterFactory; $limiter = $limiterCreator->createFromRegistry( id: 'a-unique-identifier', registry: new Named('api'), );
Check out the Register Named Rate Limiters section to learn more about it.
No Limit
The NoLimit
registry creates a Symfony no limit rate limiter:
use Tobento\App\RateLimiter\Symfony\Registry\NoLimit; use Tobento\App\RateLimiter\Symfony\RateLimiterFactory; $limiter = $limiterCreator->createFromRegistry( id: 'a-unique-identifier', registry: new NoLimit(), );
Sliding Window
The SlidingWindow
registry creates a Symfony sliding window rate limiter:
use Tobento\App\RateLimiter\Symfony\Registry\SlidingWindow; use Tobento\App\RateLimiter\Symfony\RateLimiterFactory; $limiter = $limiterCreator->createFromRegistry( id: 'a-unique-identifier', registry: new SlidingWindow( limit: 100, interval: '5 Minutes', // you may specify an id prefix: id: 'api', // you may change the storage used: storage: 'inmemory', // 'cache' is default // you may change the cache used if using the cache storage: cache: 'api-ratelimiter', // 'ratelimiter' is default ), );
You may define a cache for the name ratelimiter
(default) or api-ratelimiter
(custom) in the App Cache - Config, otherwise the primary cache is used as default.
Token Bucket
The TokenBucket
registry creates a Symfony token bucket rate limiter:
use Tobento\App\RateLimiter\Symfony\Registry\TokenBucket; use Tobento\App\RateLimiter\Symfony\RateLimiterFactory; $limiter = $limiterCreator->createFromRegistry( id: 'a-unique-identifier', registry: new TokenBucket( limit: 5000, rateAmount: 500, rateInterval: '60 Minutes', // you may specify an id prefix: id: 'api', // you may change the storage used: storage: 'inmemory', // 'cache' is default // you may change the cache used if using the cache storage: cache: 'api-ratelimiter', // 'ratelimiter' is default ), );
You may define a cache for the name ratelimiter
(default) or api-ratelimiter
(custom) in the App Cache - Config, otherwise the primary cache is used as default.
Register Named Rate Limiters
Register Named Rate Limiter via Config
You can register named rate limiters in the config file app/config/rate_limiter.php
:
return [ // ... 'limiters' => [ 'api' => new TokenBucket(limit: 10, rateAmount: 5, rateInterval: '5 Minutes'), 'login' => new FixedWindow(limit: 2, interval: '1 Minutes'), ], ];
Register Named Rate Limiter via Boot
use Tobento\App\Boot; use Tobento\App\RateLimiter\Boot\RateLimiter; use Tobento\App\RateLimiter\RegistriesInterface; use Tobento\App\RateLimiter\Symfony\Registry\FixedWindow; class RateLimitersBoot extends Boot { public const BOOT = [ // you may ensure the rate limiter boot. RateLimiter::class, ]; public function boot() { // you may use the app on method to add only if requested: $app->on( RegistriesInterface::class, static function(RegistriesInterface $registries) { $registries->add( name: 'api', registry: new FixedWindow(limit: 2, interval: '1 Minutes'), ); } ); } }
Events
Available Events
Supporting Events
Simply, install the App Event bundle.