Skip to content

[#290] Add first-class route rate limiting with global backend#507

Merged
armanist merged 8 commits into
quantum-php:masterfrom
armanist:issue/290-rate-limiting
May 8, 2026
Merged

[#290] Add first-class route rate limiting with global backend#507
armanist merged 8 commits into
quantum-php:masterfrom
armanist:issue/290-rate-limiting

Conversation

@armanist

@armanist armanist commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #290

Summary by CodeRabbit

  • New Features

    • Route-level rate limiting via fluent DSL (->rateLimit(limit, interval)), applies to single routes and route groups.
    • Framework-level rate-limit enforcement before other middleware, returning 429 "Too Many Requests" with Retry-After.
    • File and Redis rate-limit backends with centralized rate-limit middleware and response headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After).
  • Documentation

    • Updated CHANGELOG entry for v3.0.0 describing breaking refactor scope and rate-limiting features.
  • Tests

    • Added comprehensive unit tests for adapters, middleware, factory, and router integration.

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@armanist has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 26 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 767d1559-617a-4396-9ef2-73c11bfc8696

📥 Commits

Reviewing files that changed from the base of the PR and between 10ad363 and 8c5f88d.

📒 Files selected for processing (1)
  • tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php
📝 Walkthrough

Walkthrough

Adds per-route rate limiting: a RateLimitAdapter contract, file and Redis adapters, RateLimiter + factory, RateLimitMiddleware enforced at a new framework middleware stage, Route/RouteBuilder DSL/storage, unit tests, and CHANGELOG updates.

Changes

Route-Level Rate Limiting with Adapter Architecture

Layer / File(s) Summary
CHANGELOG
CHANGELOG.md
Documents ->rateLimit() DSL, new framework middleware stage, and Rate Limiting package features.
Contracts & Types
src/RateLimit/Contracts/RateLimitAdapterInterface.php, src/RateLimit/Enums/RateLimitType.php
RateLimitAdapterInterface declares hit, reset, retryAfter; RateLimitType adds FILE and REDIS.
Exceptions & Messages
src/RateLimit/Enums/ExceptionMessages.php, src/RateLimit/Exceptions/RateLimitException.php, src/Router/Enums/ExceptionMessages.php, src/Router/Exceptions/RouteException.php
Defines ADAPTER_NOT_SUPPORTED, RATE_LIMIT_OUTSIDE_ROUTE, RateLimitException::adapterNotSupported(), and RouteException::rateLimitOutsideRoute().
File Adapter
src/RateLimit/Adapters/FileRateLimitAdapter.php
Per-key JSON state with per-key file locks; implements hit(), reset(), retryAfter() and handles ttl, path, prefix.
Redis Adapter
src/RateLimit/Adapters/RedisRateLimitAdapter.php
Namespaced keys with prefix, incr + expire logic, reset() via del/setex, retryAfter() via ttl.
Rate Limiter & Factory
src/RateLimit/RateLimiter.php, src/RateLimit/Factories/RateLimiterFactory.php
RateLimiter normalizes keys METHOD:route:ip and delegates to adapter; RateLimiterFactory resolves adapter from rate_limit.default, instantiates adapter, wraps in RateLimiter, and caches per-adapter instances.
Rate Limit Middleware
src/RateLimit/RateLimitMiddleware.php
Reads route rateLimit, calls limiter hit(), returns 429 JSON with X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After when blocked (fallback to interval when adapter retryAfter=0), otherwise forwards and sets X-RateLimit-Limit.
Middleware Manager Integration
src/Middleware/MiddlewareManager.php
Adds framework middleware stage: applyMiddlewares() delegates to applyFrameworkMiddlewares() which conditionally wraps execution with RateLimitMiddleware; module middleware chaining separated; docblock @throws narrowed.
Router: Route & RouteBuilder DSL
src/Router/Route.php, src/Router/RouteBuilder.php
Route stores rateLimit metadata and exposes rateLimit(limit,interval): self and getRateLimit(); RouteBuilder::rateLimit(...) applies to group/current route or throws rateLimitOutsideRoute() when misused; several PHPDoc @throws narrowed.
Test Helpers & Config
tests/Helpers/InMemoryPsrCache.php, tests/_root/shared/config/rate_limit.php
Adds in-memory PSR cache helper and test rate_limit.php config (default => file).
Unit Tests
tests/Unit/RateLimit/*, tests/Unit/Middleware/*, tests/Unit/Router/*, tests/Unit/Middleware/MiddlewareManagerTest.php
Adds tests covering adapters, factory, limiter key normalization/delegation, RateLimitMiddleware responses/headers and fallback, MiddlewareManager prepending, RouteBuilder DSL, Route storage/validation, and exception factories.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, testing

Suggested reviewers

  • andrey-smaelov
  • grigoryanmartin20
  • live-soft

Poem

🐰 A little hop, a rhythmic beat,
I guard the routes so traffic's neat,
File or Redis, counter's fine,
One call allowed — then wait a time,
Headers tell you when to meet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding first-class route rate limiting with global backend selection.
Linked Issues check ✅ Passed All code requirements from issue #290 are met: route DSL, metadata storage, adapter interface, file/redis adapters, centralized enforcement, 429 responses, required headers, and global config-based backend selection.
Out of Scope Changes check ✅ Passed All changes align with PR objectives and issue #290 scope. No unrelated modifications detected—only rate-limiting feature code and corresponding tests added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d6c396f83

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/RateLimit/Adapters/RedisRateLimitAdapter.php Outdated
Comment thread src/RateLimit/RateLimitMiddleware.php Outdated
@codecov

codecov Bot commented May 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.97487% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.81%. Comparing base (0628ce3) to head (8c5f88d).
⚠️ Report is 9 commits behind head on master.

Files with missing lines Patch % Lines
src/RateLimit/Adapters/FileRateLimitAdapter.php 85.00% 9 Missing ⚠️
src/RateLimit/RateLimitMiddleware.php 96.15% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #507      +/-   ##
============================================
+ Coverage     90.70%   90.81%   +0.11%     
- Complexity     2930     2997      +67     
============================================
  Files           256      262       +6     
  Lines          7711     7906     +195     
============================================
+ Hits           6994     7180     +186     
- Misses          717      726       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/Unit/Router/RouteTest.php (1)

128-143: ⚡ Quick win

Add boundary tests for invalid rate-limit values.

These tests cover happy paths, but not invalid inputs (e.g., limit <= 0, interval <= 0). Adding those cases will lock expected behavior and prevent regressions once validation is enforced.

Also applies to: 191-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Router/RouteTest.php` around lines 128 - 143, Add new unit tests
in RouteTest to assert that invalid rate-limit inputs are rejected: create tests
that call Route::rateLimit with limit <= 0 and interval <= 0 (and both invalid)
and assert the expected failure behavior (e.g., exception thrown or null/empty
result depending on project convention). Update or add test methods alongside
testRouteRateLimitConfigurationIsStored and the similar block later to cover
each invalid case separately, referencing Route::rateLimit and
Route::getRateLimit to verify state does not accept invalid values. Ensure the
tests assert the concrete expected outcome your codebase uses for validation
(specific exception class or return value) so future changes will fail the tests
if invalid values are allowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/RateLimit/Adapters/FileRateLimitAdapter.php`:
- Around line 37-54: The current read-modify-write in FileRateLimitAdapter.php
(using $this->cache->get($key) then local ++ then $this->cache->set(...)) is
racy; change it to an atomic update: use your cache client's atomic increment
(e.g. $this->cache->increment or Redis INCR) on $key to bump the count and set
the TTL/expiration for reset_at in the same atomic operation, ensuring $resetAt
is computed once and stored alongside the counter; if the cache client has no
atomic increment, implement a CAS/compare-and-set loop or obtain a short-lived
lock around the increment/reset logic (using the cache's lock facility) so
updates to $key (and fields 'count' and 'reset_at') cannot be lost under
concurrency.
- Around line 34-41: In FileRateLimitAdapter::hit validate inputs at the start
of the method: check that $limit and $interval are integers >= 1 and throw an
InvalidArgumentException (or similar) if either is < 1 so the method fails fast
instead of producing fallback windows; update the hit(string $key, int $limit,
int $interval) implementation to perform these checks before computing $now,
reading $data, or setting $resetAt.

In `@src/RateLimit/Adapters/RedisRateLimitAdapter.php`:
- Around line 34-57: The hit method in RedisRateLimitAdapter currently does a
non-atomic get→modify→set sequence (in hit), which allows lost increments under
concurrency; change it to perform an atomic increment and set the TTL only when
the key is first created by using Redis primitives (INCR + EXPIRE or a small Lua
script that INCRs and calls EXPIRE when count==1). If Quantum\Cache\Cache
doesn't expose raw Redis commands, inject a raw Redis client (e.g., \Redis or
\Predis\Client) into RedisRateLimitAdapter and use that client to execute the
atomic INCR and conditional EXPIRE/EVAL; ensure hit returns count <= $limit
based on the atomic result and that the TTL equals the provided $interval.

In `@src/RateLimit/RateLimitMiddleware.php`:
- Line 62: The middleware sets Retry-After using the full window ($interval)
rather than time left; update the rate limiter interface and implementations to
provide the remaining time and use that in RateLimitMiddleware: add a new method
remainingSeconds(string $key): int to RateLimitAdapterInterface (keeping hit():
bool unchanged for compatibility), implement remainingSeconds in
FileRateLimitAdapter and RedisRateLimitAdapter to return max(0, reset_at -
time()), and modify RateLimitMiddleware (the code that currently calls
setHeader('Retry-After', (string) $interval)) to call remainingSeconds($key) and
use that value for the Retry-After header so clients receive the actual
remaining window seconds.

In `@src/Router/Route.php`:
- Around line 155-160: The rateLimit method currently accepts non-positive
integers; update Route::rateLimit to validate that both $limit and $interval are
positive (>0) and throw an InvalidArgumentException (or similar) when either is
non-positive, before assigning $this->rateLimit; include a clear message
referencing the invalid parameter name so callers know which argument failed
validation.

---

Nitpick comments:
In `@tests/Unit/Router/RouteTest.php`:
- Around line 128-143: Add new unit tests in RouteTest to assert that invalid
rate-limit inputs are rejected: create tests that call Route::rateLimit with
limit <= 0 and interval <= 0 (and both invalid) and assert the expected failure
behavior (e.g., exception thrown or null/empty result depending on project
convention). Update or add test methods alongside
testRouteRateLimitConfigurationIsStored and the similar block later to cover
each invalid case separately, referencing Route::rateLimit and
Route::getRateLimit to verify state does not accept invalid values. Ensure the
tests assert the concrete expected outcome your codebase uses for validation
(specific exception class or return value) so future changes will fail the tests
if invalid values are allowed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e677e8c-1ab8-4bc6-9e58-03131dbd6bb3

📥 Commits

Reviewing files that changed from the base of the PR and between 0628ce3 and 4d6c396.

📒 Files selected for processing (27)
  • CHANGELOG.md
  • src/Middleware/MiddlewareManager.php
  • src/RateLimit/Adapters/FileRateLimitAdapter.php
  • src/RateLimit/Adapters/RedisRateLimitAdapter.php
  • src/RateLimit/Contracts/RateLimitAdapterInterface.php
  • src/RateLimit/Enums/ExceptionMessages.php
  • src/RateLimit/Enums/RateLimitType.php
  • src/RateLimit/Exceptions/RateLimitException.php
  • src/RateLimit/Factories/RateLimiterFactory.php
  • src/RateLimit/RateLimitMiddleware.php
  • src/RateLimit/RateLimiter.php
  • src/Router/Enums/ExceptionMessages.php
  • src/Router/Exceptions/RouteException.php
  • src/Router/Route.php
  • src/Router/RouteBuilder.php
  • tests/Helpers/InMemoryPsrCache.php
  • tests/Unit/Middleware/MiddlewareManagerTest.php
  • tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php
  • tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php
  • tests/Unit/RateLimit/Exceptions/RateLimitExceptionTest.php
  • tests/Unit/RateLimit/Factories/RateLimiterFactoryTest.php
  • tests/Unit/RateLimit/RateLimitMiddlewareTest.php
  • tests/Unit/RateLimit/RateLimiterTest.php
  • tests/Unit/Router/Exceptions/RouteExceptionTest.php
  • tests/Unit/Router/RouteBuilderTest.php
  • tests/Unit/Router/RouteTest.php
  • tests/_root/shared/config/rate_limit.php

Comment thread src/RateLimit/Adapters/FileRateLimitAdapter.php Outdated
Comment thread src/RateLimit/Adapters/FileRateLimitAdapter.php Outdated
Comment thread src/RateLimit/Adapters/RedisRateLimitAdapter.php
Comment thread src/RateLimit/RateLimitMiddleware.php Outdated
Comment thread src/Router/Route.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
tests/Unit/RateLimit/RateLimiterTest.php (1)

11-58: ⚡ Quick win

buildKey tests don't cover whitespace trimming for method or ip.

testRateLimiterBuildsNormalizedKey only exercises route trimming (' /api/posts '). Both method and ip are passed without surrounding whitespace, so their trim behavior (which the AI summary and production code apparently implement) is never asserted. A missed trim on either field would silently produce wrong keys (and thus separate buckets per extra space), which is easy to miss.

Consider adding a case alongside the existing normalization test:

 $this->assertSame(
     'POST:/api/posts:127.0.0.1',
     $limiter->buildKey('post', ' /api/posts ', '127.0.0.1')
 );
+
+$this->assertSame(
+    'POST:/api/posts:127.0.0.1',
+    $limiter->buildKey(' post ', ' /api/posts ', ' 127.0.0.1 ')
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/RateLimit/RateLimiterTest.php` around lines 11 - 58, The test
suite misses asserting that RateLimiter::buildKey trims whitespace for the
method and IP parts; update Test class RateLimiterTest by extending
testRateLimiterBuildsNormalizedKey (or add a new small assertion) to call
$limiter->buildKey with padded method and ip (e.g., ' POST ' and ' 127.0.0.1 ')
and assert the normalized result equals 'POST:/api/posts:127.0.0.1', ensuring
buildKey's trimming behavior for method and ip is covered.
tests/Unit/RateLimit/RateLimitMiddlewareTest.php (2)

25-28: ⚖️ Poor tradeoff

Consider using dependency injection instead of reflection.

Mutating the factory's private instances property via reflection couples the test to implementation details and bypasses proper encapsulation. If the factory provides constructor injection or a setter method for testing, prefer that approach. Additionally, consider adding a tearDown() method to reset the factory state between tests to prevent state leakage.

♻️ Alternative approach using factory method or setter

If RateLimiterFactory supports it, inject the adapter directly:

-$factory = Di::get(RateLimiterFactory::class);
-$this->setPrivateProperty($factory, 'instances', [
-    'file' => new RateLimiter(new ToggleAdapter()),
-]);
+// Option 1: If factory supports setting adapter
+$factory = Di::get(RateLimiterFactory::class);
+$factory->setAdapter('file', new ToggleAdapter());
+
+// Option 2: Register custom factory for testing
+Di::register(RateLimiterFactory::class, fn() => 
+    new RateLimiterFactory(['file' => new RateLimiter(new ToggleAdapter())])
+);

And add cleanup:

public function tearDown(): void
{
    // Reset factory state
    parent::tearDown();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/RateLimit/RateLimitMiddlewareTest.php` around lines 25 - 28, Tests
currently mutate RateLimiterFactory::instances via setPrivateProperty which
couples tests to internals; instead, obtain a RateLimiterFactory instance
through the Di container and inject a test instance (RateLimiter with
ToggleAdapter) using a public setter or factory method on RateLimiterFactory (or
register a test binding in Di to return a preconfigured RateLimiterFactory),
replace the setPrivateProperty usage in the test, and add a tearDown() that
resets the factory state or rebinds the original Di registration to avoid state
leakage between tests; refer to RateLimiterFactory, instances, RateLimiter,
ToggleAdapter and the test's tearDown for changes.

31-58: 🏗️ Heavy lift

Test doesn't verify rate-limit key construction.

The ToggleAdapter ignores the $key parameter (lines 65-68), so this test doesn't verify that the middleware constructs keys using the required format: HTTP method + route pattern + client IP. According to the PR objectives, the key shape is fixed and must include all three components.

💡 Suggested enhancement to verify key construction

Modify ToggleAdapter to capture and expose the key:

 class ToggleAdapter implements RateLimitAdapterInterface
 {
     private int $calls = 0;
+    public ?string $lastKey = null;
 
     public function hit(string $key, int $limit, int $interval): bool
     {
+        $this->lastKey = $key;
         $this->calls++;
         return $this->calls === 1;
     }

Then add assertions in the test:

$adapter = new ToggleAdapter();
$factory->setAdapter('file', new RateLimiter($adapter));

// ... apply middleware ...

$this->assertStringContainsString('GET', $adapter->lastKey);
$this->assertStringContainsString('/posts', $adapter->lastKey);
// IP assertion would depend on how request() populates client IP
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/RateLimit/RateLimitMiddlewareTest.php` around lines 31 - 58, The
test currently doesn't verify the constructed rate-limit key because the
ToggleAdapter swallows the $key; modify the ToggleAdapter (used by RateLimiter)
to store the last received key in a public property (e.g., lastKey) whenever its
increment/check methods are called, then in
testRateLimitMiddlewareBlocksWith429AndHeaders instantiate that ToggleAdapter,
register it with the factory via factory->setAdapter('file', new
RateLimiter($adapter)), run the middleware apply calls as before, and add
assertions against $adapter->lastKey to assert it contains the HTTP method
("GET"), the route pattern ("/posts") and the client IP (as provided by
request()), so the test validates the key shape produced by
RateLimitMiddleware/RateLimiter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/Unit/RateLimit/RateLimiterTest.php`:
- Around line 11-58: The test suite misses asserting that RateLimiter::buildKey
trims whitespace for the method and IP parts; update Test class RateLimiterTest
by extending testRateLimiterBuildsNormalizedKey (or add a new small assertion)
to call $limiter->buildKey with padded method and ip (e.g., ' POST ' and '
127.0.0.1 ') and assert the normalized result equals
'POST:/api/posts:127.0.0.1', ensuring buildKey's trimming behavior for method
and ip is covered.

In `@tests/Unit/RateLimit/RateLimitMiddlewareTest.php`:
- Around line 25-28: Tests currently mutate RateLimiterFactory::instances via
setPrivateProperty which couples tests to internals; instead, obtain a
RateLimiterFactory instance through the Di container and inject a test instance
(RateLimiter with ToggleAdapter) using a public setter or factory method on
RateLimiterFactory (or register a test binding in Di to return a preconfigured
RateLimiterFactory), replace the setPrivateProperty usage in the test, and add a
tearDown() that resets the factory state or rebinds the original Di registration
to avoid state leakage between tests; refer to RateLimiterFactory, instances,
RateLimiter, ToggleAdapter and the test's tearDown for changes.
- Around line 31-58: The test currently doesn't verify the constructed
rate-limit key because the ToggleAdapter swallows the $key; modify the
ToggleAdapter (used by RateLimiter) to store the last received key in a public
property (e.g., lastKey) whenever its increment/check methods are called, then
in testRateLimitMiddlewareBlocksWith429AndHeaders instantiate that
ToggleAdapter, register it with the factory via factory->setAdapter('file', new
RateLimiter($adapter)), run the middleware apply calls as before, and add
assertions against $adapter->lastKey to assert it contains the HTTP method
("GET"), the route pattern ("/posts") and the client IP (as provided by
request()), so the test validates the key shape produced by
RateLimitMiddleware/RateLimiter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ecd51d6-f596-4457-bd33-f102772f51f3

📥 Commits

Reviewing files that changed from the base of the PR and between 4d6c396 and 0213c7d.

📒 Files selected for processing (10)
  • src/RateLimit/Adapters/FileRateLimitAdapter.php
  • src/RateLimit/Adapters/RedisRateLimitAdapter.php
  • src/RateLimit/Contracts/RateLimitAdapterInterface.php
  • src/RateLimit/Factories/RateLimiterFactory.php
  • src/RateLimit/RateLimitMiddleware.php
  • src/RateLimit/RateLimiter.php
  • tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php
  • tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php
  • tests/Unit/RateLimit/RateLimitMiddlewareTest.php
  • tests/Unit/RateLimit/RateLimiterTest.php
✅ Files skipped from review due to trivial changes (2)
  • src/RateLimit/RateLimitMiddleware.php
  • tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/RateLimit/Contracts/RateLimitAdapterInterface.php
  • src/RateLimit/RateLimiter.php
  • tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php
  • src/RateLimit/Adapters/RedisRateLimitAdapter.php
  • src/RateLimit/Factories/RateLimiterFactory.php
  • src/RateLimit/Adapters/FileRateLimitAdapter.php

@armanist armanist requested review from a team, Wiltzsu, andrey-smaelov and ematrito May 7, 2026 17:57
@armanist armanist added this to the 3.0.0 milestone May 7, 2026
@armanist armanist added the help wanted Extra attention is needed label May 7, 2026
Comment thread src/Middleware/MiddlewareManager.php
Comment thread src/RateLimit/Adapters/FileRateLimitAdapter.php Outdated
Comment thread src/RateLimit/Factories/RateLimiterFactory.php Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php`:
- Around line 92-95: The test in FileRateLimitAdapterTest writes an invalid JSON
file using a hardcoded filename pattern md5('test' . 'broken') . '.rate' which
mirrors the adapter's current key scheme and can lead to a false positive if the
adapter's naming changes; change the test to stop relying on the adapter's
internal filename: either (A) use a test-only helper on the adapter to obtain
the exact state filename for key "broken" (e.g., call
adapter->stateFileForKey('broken') or similar) and write the invalid JSON to
that path before asserting adapter->retryAfter('broken') === 0, or (B) add an
explicit comment documenting the assumed filename scheme and derive the filename
via a public method or protected accessor exposed only for tests so the test
targets the adapter's actual file naming (referencing FileRateLimitAdapterTest,
retryAfter, and the adapter instance).
- Around line 63-80: The testFileAdapterCreatesStorageDirectoryWhenMissing test
can leak the rate_limit_tests_missing directory if the assertion fails; update
the test to ensure cleanup always runs by wrapping the adapter creation and
assertion in a try/finally block so fs()->removeDirectory($path) is called in
the finally clause; reference the local $path variable, fs() helper and the
FileRateLimitAdapter instantiation so the directory is removed regardless of
assertion outcome.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3ee8e12-b2ef-4b67-953e-ef6631bf3a8f

📥 Commits

Reviewing files that changed from the base of the PR and between 0213c7d and 10ad363.

📒 Files selected for processing (16)
  • src/Middleware/MiddlewareManager.php
  • src/RateLimit/Adapters/FileRateLimitAdapter.php
  • src/RateLimit/Adapters/RedisRateLimitAdapter.php
  • src/RateLimit/Contracts/RateLimitAdapterInterface.php
  • src/RateLimit/Enums/ExceptionMessages.php
  • src/RateLimit/Exceptions/RateLimitException.php
  • src/RateLimit/Factories/RateLimiterFactory.php
  • src/RateLimit/RateLimitMiddleware.php
  • src/RateLimit/RateLimiter.php
  • src/Router/Route.php
  • tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php
  • tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php
  • tests/Unit/RateLimit/RateLimitMiddlewareTest.php
  • tests/Unit/Router/RouteBuilderTest.php
  • tests/Unit/Router/RouteTest.php
  • tests/_root/shared/config/rate_limit.php
✅ Files skipped from review due to trivial changes (3)
  • src/RateLimit/Enums/ExceptionMessages.php
  • src/RateLimit/Contracts/RateLimitAdapterInterface.php
  • src/RateLimit/RateLimiter.php
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/Unit/RateLimit/Adapters/RedisRateLimitAdapterTest.php
  • src/RateLimit/Exceptions/RateLimitException.php
  • src/RateLimit/RateLimitMiddleware.php
  • src/Router/Route.php
  • src/Middleware/MiddlewareManager.php
  • src/RateLimit/Adapters/FileRateLimitAdapter.php
  • src/RateLimit/Adapters/RedisRateLimitAdapter.php

Comment thread tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php
Comment thread tests/Unit/RateLimit/Adapters/FileRateLimitAdapterTest.php Outdated
@armanist armanist requested a review from andrey-smaelov May 8, 2026 12:19
@armanist armanist self-assigned this May 8, 2026
@armanist armanist merged commit 0b4096a into quantum-php:master May 8, 2026
5 checks passed
@armanist armanist deleted the issue/290-rate-limiting branch May 8, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted Extra attention is needed new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Route Rate Limiting Support

2 participants