diff --git a/examples/phase5_5/examples.phase5_5.md b/examples/phase5_5/examples.phase5_5.md new file mode 100644 index 0000000..d8959ec --- /dev/null +++ b/examples/phase5_5/examples.phase5_5.md @@ -0,0 +1,38 @@ +# Phase 5.5 Examples + +These examples demonstrate the correct usage of the Rate Limiter library as defined in Phase 5. +They mimic the behavior tested in `tests/` and the public API contract. + +## Native PHP Examples + +These scripts are standalone and require `vendor/autoload.php`. + +### 1. Global Limiter Flow +Demonstrates how the `EnforcingRateLimiter` enforces the Global Rate Limit before checking the specific action limit. +- **File:** `native/global_limiter_flow.php` +- **Key Concept:** If the global limit is exceeded, the action limit is not checked. + +### 2. Action Limiter Flow +Demonstrates the standard flow where the Global Limit passes, but the specific Action Limit is exceeded. +- **File:** `native/action_limiter_flow.php` +- **Key Concept:** `EnforcingRateLimiter` ensures granularity by checking specific actions after global checks. + +### 3. Exponential Backoff Flow +Shows how `ExponentialBackoffPolicy` calculates delays when a limit is exceeded. +- **File:** `native/exponential_backoff_flow.php` +- **Key Concept:** The `TooManyRequestsException` contains an enhanced `RateLimitStatusDTO` with `retryAfter` and `backoffSeconds`. + +### 4. DTO Serialization +Examples of creating, accessing, and serializing the `RateLimitStatusDTO`. +- **File:** `native/dto_serialization.php` + +### 5. Exception Flow +Demonstrates catching `TooManyRequestsException` and retrieving the underlying `RateLimitStatusDTO` for client responses. +- **File:** `native/exception_flow.php` +- **Key Concept:** The `TooManyRequestsException` exposes the `RateLimitStatusDTO` via the public readonly property `$status`. There is no `getStatus()` method. + +## Important Implementation Notes + +- **Source of Truth:** These examples are derived strictly from `src/` and `tests/`. +- **Backoff & Source:** The `EnforcingRateLimiter` is the sole authority for applying backoff logic (via `BackoffPolicyInterface`) and assigning the `source` property ('global' or 'action') to the DTO. Drivers should not attempt to calculate backoff manually. +- **Exception Handling:** When catching `TooManyRequestsException`, always access the DTO via `$e->status`. diff --git a/examples/phase5_5/native/action_limiter_flow.php b/examples/phase5_5/native/action_limiter_flow.php new file mode 100644 index 0000000..c871c29 --- /dev/null +++ b/examples/phase5_5/native/action_limiter_flow.php @@ -0,0 +1,74 @@ +value() === 'global') { + return new RateLimitStatusDTO(1000, 999, 3600); + } + + // Fail Specific Action + if ($action->value() === 'upload') { + throw new TooManyRequestsException('Action limit exceeded', 429); + } + + return new RateLimitStatusDTO(10, 9, 60); + } + + public function reset(string $key, RateLimitActionInterface $action, PlatformInterface $platform): bool + { + return true; + } + + public function status(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO + { + return new RateLimitStatusDTO(5, 0, 60); + } +}; + +// 3. Setup Resolver +$resolver = new RateLimiterResolver(['memory' => $driver], 'memory'); + +// 4. Resolve and Attempt +$limiter = $resolver->resolve(); + +try { + echo "Attempting upload action...\n"; + $limiter->attempt('user_456', $action, $platform); +} catch (TooManyRequestsException $e) { + echo "Caught expected exception: " . $e->getMessage() . "\n"; + + // Use property access for status, not getStatus() + $status = $e->status; + if ($status) { + echo "Source of block: " . $status->source . "\n"; // Expected: action + } +} diff --git a/examples/phase5_5/native/dto_serialization.php b/examples/phase5_5/native/dto_serialization.php new file mode 100644 index 0000000..566b59a --- /dev/null +++ b/examples/phase5_5/native/dto_serialization.php @@ -0,0 +1,34 @@ +limit . "\n"; +echo "Remaining: " . $dto->remaining . "\n"; + +// 3. Serialize to Array +$array = $dto->toArray(); +echo "Array Export: " . print_r($array, true); + +// 4. Re-create from Array +$newDto = RateLimitStatusDTO::fromArray($array); +echo "Re-created Source: " . $newDto->source . "\n"; + +// 5. Serialize to JSON +echo "JSON: " . json_encode($array) . "\n"; diff --git a/examples/phase5_5/native/exception_flow.php b/examples/phase5_5/native/exception_flow.php new file mode 100644 index 0000000..6be5d9b --- /dev/null +++ b/examples/phase5_5/native/exception_flow.php @@ -0,0 +1,35 @@ +getMessage() . "\n"; + echo "Code: " . $e->getCode() . "\n"; + + // Access property directly + $attachedStatus = $e->status; + if ($attachedStatus) { + echo "Attached Status Remaining: " . $attachedStatus->remaining . "\n"; + echo "Attached Status RetryAfter: " . $attachedStatus->retryAfter . "\n"; + } +} diff --git a/examples/phase5_5/native/exponential_backoff_flow.php b/examples/phase5_5/native/exponential_backoff_flow.php new file mode 100644 index 0000000..d01f8de --- /dev/null +++ b/examples/phase5_5/native/exponential_backoff_flow.php @@ -0,0 +1,83 @@ + limit, remaining is negative. +// This matches real driver behavior seen in src/Drivers/RedisRateLimiter.php. +$driver = new class implements RateLimiterInterface { + public function attempt(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO + { + if ($action->value() === 'global') { + return new RateLimitStatusDTO(1000, 999, 3600); + } + + // Simulate exhaustion + // Limit 10, Current 12 -> Remaining -2 + // Used = Limit - Remaining = 10 - (-2) = 12 + // Over = Used - Limit = 12 - 10 = 2 + // Backoff Base 2 => 2^2 = 4 seconds delay + $status = new RateLimitStatusDTO(10, -2, 60); + throw new TooManyRequestsException('Rate limit exceeded', 429, $status); + } + + public function reset(string $key, RateLimitActionInterface $action, PlatformInterface $platform): bool + { + return true; + } + + public function status(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO + { + return new RateLimitStatusDTO(10, -2, 60); + } +}; + +// 3. Setup Resolver with ExponentialBackoffPolicy +// Base 2 seconds, Max 60 seconds +$backoffPolicy = new ExponentialBackoffPolicy(2, 60); +$resolver = new RateLimiterResolver(['memory' => $driver], 'memory', $backoffPolicy); +$limiter = $resolver->resolve(); + +// 4. Trigger Backoff +try { + echo "Attempting SMS send (simulating 12th attempt on limit 10)...\n"; + $limiter->attempt('user_789', $action, $platform); +} catch (TooManyRequestsException $e) { + // Access the status property directly + $status = $e->status; + + // EnforcingRateLimiter (via applyBackoff) ensures $status is not null here in real usage + if ($status === null) { + exit("Error: Status should be enriched by EnforcingRateLimiter\n"); + } + + echo "Blocked: " . ($status->blocked ? 'Yes' : 'No') . "\n"; + echo "Retry After: " . $status->retryAfter . " seconds\n"; // Expected: 4 + echo "Backoff Seconds: " . $status->backoffSeconds . "\n"; + echo "Next Allowed At: " . $status->nextAllowedAt . "\n"; +} diff --git a/examples/phase5_5/native/global_limiter_flow.php b/examples/phase5_5/native/global_limiter_flow.php new file mode 100644 index 0000000..e22cdf4 --- /dev/null +++ b/examples/phase5_5/native/global_limiter_flow.php @@ -0,0 +1,71 @@ +value() === 'global') { + throw new TooManyRequestsException('Global limit exceeded', 429); + } + + // Should not be reached in this example + return new RateLimitStatusDTO(10, 9, 60); + } + + public function reset(string $key, RateLimitActionInterface $action, PlatformInterface $platform): bool + { + return true; + } + + public function status(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO + { + return new RateLimitStatusDTO(100, 0, 60); + } +}; + +// 3. Setup Resolver +$resolver = new RateLimiterResolver(['memory' => $driver], 'memory'); + +// 4. Resolve and Attempt +$limiter = $resolver->resolve(); + +try { + echo "Attempting action...\n"; + $limiter->attempt('user_123', $action, $platform); +} catch (TooManyRequestsException $e) { + echo "Caught expected exception: " . $e->getMessage() . "\n"; + + // Use property access for status, not getStatus() + $status = $e->status; + if ($status) { + echo "Source of block: " . $status->source . "\n"; // Expected: global + } +}