diff --git a/README.md b/README.md index 689e608..f0b55de 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ You can create a rate limiter to limit per second or per minute. RateLimiterMiddleware::perSecond(3); // Max. 3 requests per second RateLimiterMiddleware::perMinute(5); // Max. 5 requests per minute + +RateLimiterMiddleware::custom(3, 30, RateLimiter::TIME_FRAME_SECOND); // Max. 3 requests per 30 seconds + +RateLimiterMiddleware::custom(7, 15, RateLimiter::TIME_FRAME_MINUTE); // Max. 7 requests per 15 minutes ``` ## Custom stores diff --git a/src/RateLimiter.php b/src/RateLimiter.php index eda2072..c67118b 100644 --- a/src/RateLimiter.php +++ b/src/RateLimiter.php @@ -4,14 +4,19 @@ class RateLimiter { + const TIME_FRAME_DAY = 'day'; + const TIME_FRAME_HOUR = 'hour'; const TIME_FRAME_MINUTE = 'minute'; const TIME_FRAME_SECOND = 'second'; /** @var int */ protected $limit; + /** @var int */ + protected $timeInterval; + /** @var string */ - protected $timeFrame; + protected $timeUnit; /** @var \Spatie\RateLimiter\Store */ protected $store; @@ -21,12 +26,14 @@ class RateLimiter public function __construct( int $limit, - string $timeFrame, + int $timeInterval, + string $timeUnit, Store $store, Deferrer $deferrer ) { $this->limit = $limit; - $this->timeFrame = $timeFrame; + $this->timeInterval = $timeInterval; + $this->timeUnit = $timeUnit; $this->store = $store; $this->deferrer = $deferrer; } @@ -70,10 +77,17 @@ function (int $timestamp) use ($currentTimeFrameStart) { protected function timeFrameLengthInMilliseconds(): int { - if ($this->timeFrame === self::TIME_FRAME_MINUTE) { - return 60 * 1000; + $unitsInMilliseconds = [ + 'second' => 1000, + 'minute' => 60 * 1000, + 'hour' => 60 * 60 * 1000, + 'day' => 24 * 60 * 60 * 1000, + ]; + + if (! isset($unitsInMilliseconds[$this->timeUnit])) { + throw new Exception("Invalid time unit provided: $this->timeUnit"); } - return 1000; + return $this->timeInterval * $unitsInMilliseconds[$this->timeUnit]; } } diff --git a/src/RateLimiterMiddleware.php b/src/RateLimiterMiddleware.php index e646597..6118db0 100755 --- a/src/RateLimiterMiddleware.php +++ b/src/RateLimiterMiddleware.php @@ -18,6 +18,7 @@ public static function perSecond(int $limit, Store $store = null, Deferrer $defe { $rateLimiter = new RateLimiter( $limit, + 1, RateLimiter::TIME_FRAME_SECOND, $store ?? new InMemoryStore(), $deferrer ?? new SleepDeferrer() @@ -30,6 +31,7 @@ public static function perMinute(int $limit, Store $store = null, Deferrer $defe { $rateLimiter = new RateLimiter( $limit, + 1, RateLimiter::TIME_FRAME_MINUTE, $store ?? new InMemoryStore(), $deferrer ?? new SleepDeferrer() @@ -38,6 +40,19 @@ public static function perMinute(int $limit, Store $store = null, Deferrer $defe return new static($rateLimiter); } + public static function custom(int $limit, int $timeInterval, string $timeUnit, Store $store = null, Deferrer $deferrer = null): RateLimiterMiddleware + { + $rateLimiter = new RateLimiter( + $limit, + $timeInterval, + $timeUnit, + $store ?? new InMemoryStore(), + $deferrer ?? new SleepDeferrer() + ); + + return new static($rateLimiter); + } + public function __invoke(callable $handler) { return function (RequestInterface $request, array $options) use ($handler) { diff --git a/tests/RateLimiterTest.php b/tests/RateLimiterTest.php index 02bf7c6..691931b 100644 --- a/tests/RateLimiterTest.php +++ b/tests/RateLimiterTest.php @@ -9,7 +9,7 @@ class RateLimiterTest extends TestCase /** @test */ public function it_execute_actions_below_a_limit_in_seconds() { - $rateLimiter = $this->createRateLimiter(3, RateLimiter::TIME_FRAME_SECOND); + $rateLimiter = $this->createRateLimiter(3, 1, RateLimiter::TIME_FRAME_SECOND); $this->assertEquals(0, $this->deferrer->getCurrentTime()); @@ -49,7 +49,7 @@ public function it_execute_actions_below_a_limit_in_seconds() /** @test */ public function it_defers_actions_when_it_reaches_a_limit_in_seconds() { - $rateLimiter = $this->createRateLimiter(3, RateLimiter::TIME_FRAME_SECOND); + $rateLimiter = $this->createRateLimiter(3, 1, RateLimiter::TIME_FRAME_SECOND); $this->assertEquals(0, $this->deferrer->getCurrentTime()); @@ -77,7 +77,7 @@ public function it_defers_actions_when_it_reaches_a_limit_in_seconds() /** @test */ public function it_execute_actions_below_a_limit_in_minutes() { - $rateLimiter = $this->createRateLimiter(3, RateLimiter::TIME_FRAME_MINUTE); + $rateLimiter = $this->createRateLimiter(3, 1, RateLimiter::TIME_FRAME_MINUTE); $this->assertEquals(0, $this->deferrer->getCurrentTime()); @@ -111,7 +111,7 @@ public function it_execute_actions_below_a_limit_in_minutes() /** @test */ public function it_defers_actions_when_it_reaches_a_limit_in_minutes() { - $rateLimiter = $this->createRateLimiter(3, RateLimiter::TIME_FRAME_MINUTE); + $rateLimiter = $this->createRateLimiter(3, 1, RateLimiter::TIME_FRAME_MINUTE); $this->assertEquals(0, $this->deferrer->getCurrentTime()); @@ -135,4 +135,66 @@ public function it_defers_actions_when_it_reaches_a_limit_in_minutes() $this->assertEquals(60000, $this->deferrer->getCurrentTime()); } + + /** @test */ + public function it_execute_actions_below_a_custom_limit() + { + $rateLimiter = $this->createRateLimiter(2, 6, RateLimiter::TIME_FRAME_SECOND); + + $this->assertEquals(0, $this->deferrer->getCurrentTime()); + + $rateLimiter->handle(function () { + $this->deferrer->sleep(100); + }); + + $this->assertEquals(100, $this->deferrer->getCurrentTime()); + + $rateLimiter->handle(function () { + $this->deferrer->sleep(100); + }); + + $this->assertEquals(200, $this->deferrer->getCurrentTime()); + + $this->deferrer->sleep(5800); + + $rateLimiter->handle(function () { + $this->deferrer->sleep(100); + }); + + $this->assertEquals(6100, $this->deferrer->getCurrentTime()); + + $rateLimiter->handle(function () { + $this->deferrer->sleep(100); + }); + + $this->assertEquals(6200, $this->deferrer->getCurrentTime()); + } + + /** @test */ + public function it_defers_actions_when_it_reaches_a_custom_limit() + { + $rateLimiter = $this->createRateLimiter(2, 6, RateLimiter::TIME_FRAME_SECOND); + + $this->assertEquals(0, $this->deferrer->getCurrentTime()); + + $rateLimiter->handle(function () { + }); + + $this->assertEquals(0, $this->deferrer->getCurrentTime()); + + $rateLimiter->handle(function () { + }); + + $this->assertEquals(0, $this->deferrer->getCurrentTime()); + + $rateLimiter->handle(function () { + }); + + $this->assertEquals(6000, $this->deferrer->getCurrentTime()); + + $rateLimiter->handle(function () { + }); + + $this->assertEquals(6000, $this->deferrer->getCurrentTime()); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index a57dfa3..0894018 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -18,8 +18,8 @@ protected function setUp(): void $this->deferrer = new TestDeferrer(); } - public function createRateLimiter(int $limit, string $timeFrame): RateLimiter + public function createRateLimiter(int $limit, int $timeInterval, string $timeUnit): RateLimiter { - return new RateLimiter($limit, $timeFrame, new InMemoryStore(), $this->deferrer); + return new RateLimiter($limit, $timeInterval, $timeUnit, new InMemoryStore(), $this->deferrer); } }