diff --git a/examples/phase5_5/examples.phase5_5.md b/examples/phase5_5/examples.phase5_5.md new file mode 100644 index 0000000..3981ee7 --- /dev/null +++ b/examples/phase5_5/examples.phase5_5.md @@ -0,0 +1,40 @@ +# Phase 5.5: Native PHP Examples + +This directory contains native PHP examples demonstrating the behavior of the Rate Limiter library using the **Resolver** as the entry point. +These examples rely only on `vendor/autoload.php` and use a dummy in-memory driver to replicate real-world scenarios. + +## Core Concepts + +1. **Entry Point**: `RateLimiterResolver::resolve()` is the only public way to obtain a rate limiter instance. `EnforcingRateLimiter` is an internal implementation detail. +2. **Global vs Action**: The system automatically enforces a "Global" limit before checking the specific "Action" limit. +3. **Drivers**: Drivers are "dumb" counters. They do not calculate backoff or know about global rules. +4. **Exceptions**: A `TooManyRequestsException` carries a `RateLimitStatusDTO` with full context (retry time, source, etc.). + +## Examples + +### 1. Basic Flow (`01_basic_flow.php`) +Demonstrates the standard usage: +- Create `RateLimiterResolver`. +- Resolve a driver. +- Call `attempt()`. +- Inspect the returned `RateLimitStatusDTO`. + +### 2. Global Blocking (`02_global_blocking.php`) +Simulates a scenario where the **Global** rate limit is exceeded. +- The system checks the global limit *before* the action limit. +- If the global limit is exceeded, `TooManyRequestsException` is thrown with `source: 'global'`. + +### 3. Action Blocking (`03_action_blocking.php`) +Simulates a scenario where the Global limit passes, but the specific **Action** limit is exceeded. +- If the global limit passes, the specific action limit is checked. +- If exceeded, `TooManyRequestsException` is thrown with `source: 'action'`. + +### 4. Backoff Flow (`04_backoff_flow.php`) +Demonstrates how the library calculates exponential backoff. +- Repeated failures result in increased `retryAfter` and `backoffSeconds`. +- This calculation is handled internally by the `EnforcingRateLimiter`, not the driver. + +### 5. Exception Handling (`05_exception_handling.php`) +Detailed inspection of the `TooManyRequestsException`. +- Shows how to access the `RateLimitStatusDTO` via `$e->status`. +- Prints all relevant properties for client response construction. diff --git a/examples/phase5_5/native/01_basic_flow.php b/examples/phase5_5/native/01_basic_flow.php new file mode 100644 index 0000000..62b671a --- /dev/null +++ b/examples/phase5_5/native/01_basic_flow.php @@ -0,0 +1,39 @@ + $driver], 'memory'); + +// 3. Resolve the RateLimiter instance +$rateLimiter = $resolver->resolve(); + +// 4. Define the context +$key = 'user_01'; +$action = new ExampleAction('login'); +$platform = new ExamplePlatform('web'); + +// 5. Perform an attempt +try { + $status = $rateLimiter->attempt($key, $action, $platform); + + echo "Attempt successful!\n"; + echo "Limit: {$status->limit}\n"; + echo "Remaining: {$status->remaining}\n"; + echo "Source: {$status->source}\n"; // Should be 'action' or 'global' (defaults to action if global passes) + +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} diff --git a/examples/phase5_5/native/02_global_blocking.php b/examples/phase5_5/native/02_global_blocking.php new file mode 100644 index 0000000..6311fc1 --- /dev/null +++ b/examples/phase5_5/native/02_global_blocking.php @@ -0,0 +1,45 @@ +setLimit('user_02:global:global', 1); + +$resolver = new RateLimiterResolver(['memory' => $driver], 'memory'); +$rateLimiter = $resolver->resolve(); + +$key = 'user_02'; +$action = new ExampleAction('login'); +$platform = new ExamplePlatform('web'); + +echo "1. First attempt (Global count: 1/1) - Should Pass\n"; +$rateLimiter->attempt($key, $action, $platform); +echo " Success.\n\n"; + +echo "2. Second attempt (Global count: 2/1) - Should Fail\n"; +try { + $rateLimiter->attempt($key, $action, $platform); +} catch (TooManyRequestsException $e) { + echo " Caught TooManyRequestsException!\n"; + + // Access the Status DTO from the exception + $status = $e->status; + + if ($status) { + echo " Source: {$status->source}\n"; // Expected: 'global' + echo " Blocked: " . ($status->blocked ? 'Yes' : 'No') . "\n"; + } +} diff --git a/examples/phase5_5/native/03_action_blocking.php b/examples/phase5_5/native/03_action_blocking.php new file mode 100644 index 0000000..b309525 --- /dev/null +++ b/examples/phase5_5/native/03_action_blocking.php @@ -0,0 +1,45 @@ +setLimit('user_03:global:global', 10); + +// Action limit is strict (1). +// Key format: key:action_name:platform_name +$driver->setLimit('user_03:checkout:api', 1); + +$resolver = new RateLimiterResolver(['memory' => $driver], 'memory'); +$rateLimiter = $resolver->resolve(); + +$key = 'user_03'; +$action = new ExampleAction('checkout'); +$platform = new ExamplePlatform('api'); + +echo "1. First attempt (Action count: 1/1) - Should Pass\n"; +$rateLimiter->attempt($key, $action, $platform); +echo " Success.\n\n"; + +echo "2. Second attempt (Action count: 2/1) - Should Fail\n"; +try { + $rateLimiter->attempt($key, $action, $platform); +} catch (TooManyRequestsException $e) { + echo " Caught TooManyRequestsException!\n"; + + $status = $e->status; + if ($status) { + echo " Source: {$status->source}\n"; // Expected: 'action' + echo " Blocked: " . ($status->blocked ? 'Yes' : 'No') . "\n"; + } +} diff --git a/examples/phase5_5/native/04_backoff_flow.php b/examples/phase5_5/native/04_backoff_flow.php new file mode 100644 index 0000000..fea5b8d --- /dev/null +++ b/examples/phase5_5/native/04_backoff_flow.php @@ -0,0 +1,46 @@ +setLimit('user_04:search:api', 1); + +$resolver = new RateLimiterResolver(['memory' => $driver], 'memory'); +$rateLimiter = $resolver->resolve(); + +$key = 'user_04'; +$action = new ExampleAction('search'); +$platform = new ExamplePlatform('api'); + +// 1. First attempt (Pass) +$rateLimiter->attempt($key, $action, $platform); + +// 2. Second attempt (Fail) +try { + $rateLimiter->attempt($key, $action, $platform); +} catch (TooManyRequestsException $e) { + echo "First Block:\n"; + echo " Retry After: {$e->status->retryAfter}s\n"; + echo " Backoff: {$e->status->backoffSeconds}s\n"; +} + +// 3. Third attempt (Fail with exponential backoff) +// Note: In a real scenario, time would pass. Here we just trigger another failure. +// The default backoff policy increases delay based on attempts over limit. +try { + $rateLimiter->attempt($key, $action, $platform); +} catch (TooManyRequestsException $e) { + echo "Second Block:\n"; + echo " Retry After: {$e->status->retryAfter}s\n"; + echo " Backoff: {$e->status->backoffSeconds}s\n"; +} diff --git a/examples/phase5_5/native/05_exception_handling.php b/examples/phase5_5/native/05_exception_handling.php new file mode 100644 index 0000000..1c44ad1 --- /dev/null +++ b/examples/phase5_5/native/05_exception_handling.php @@ -0,0 +1,36 @@ +setLimit('user_05:api:web', 0); // Always fail + +$resolver = new RateLimiterResolver(['memory' => $driver], 'memory'); +$rateLimiter = $resolver->resolve(); + +try { + $rateLimiter->attempt('user_05', new ExampleAction('api'), new ExamplePlatform('web')); +} catch (TooManyRequestsException $e) { + echo "Exception Caught: " . $e->getMessage() . "\n"; + echo "Code: " . $e->getCode() . "\n"; + + $status = $e->status; + if ($status) { + echo "DTO Status:\n"; + echo " - Remaining: {$status->remaining}\n"; + echo " - Reset After: {$status->resetAfter}\n"; + echo " - Retry After: {$status->retryAfter}\n"; + echo " - Blocked: " . ($status->blocked ? 'Yes' : 'No') . "\n"; + echo " - Source: {$status->source}\n"; + echo " - Next Allowed: {$status->nextAllowedAt}\n"; + } +} diff --git a/examples/phase5_5/native/InMemoryDriver.php b/examples/phase5_5/native/InMemoryDriver.php new file mode 100644 index 0000000..bf0444a --- /dev/null +++ b/examples/phase5_5/native/InMemoryDriver.php @@ -0,0 +1,99 @@ +name; + } +} + +final class ExamplePlatform implements PlatformInterface +{ + public function __construct(private string $name = 'test_platform') + { + } + + public function value(): string + { + return $this->name; + } +} + +/** + * A dummy in-memory driver for demonstration purposes. + * It mimics the behavior of a real storage driver (Redis/MySQL). + */ +final class InMemoryDriver implements RateLimiterInterface +{ + /** @var array */ + private array $counts = []; + + /** + * @param array $limits Map of storage key to limit. + */ + public function __construct( + private array $limits = [] + ) { + } + + public function setLimit(string $key, int $limit): void + { + $this->limits[$key] = $limit; + } + + public function attempt(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO + { + $storageKey = $this->buildKey($key, $action, $platform); + $limit = $this->limits[$storageKey] ?? 10; // Default limit 10 + $current = ($this->counts[$storageKey] ?? 0) + 1; + $this->counts[$storageKey] = $current; + + $remaining = $limit - $current; + $dto = new RateLimitStatusDTO($limit, $remaining, 60); + + if ($current > $limit) { + throw new TooManyRequestsException('Rate limit exceeded', 429, $dto); + } + + return $dto; + } + + public function reset(string $key, RateLimitActionInterface $action, PlatformInterface $platform): bool + { + $storageKey = $this->buildKey($key, $action, $platform); + unset($this->counts[$storageKey]); + + return true; + } + + public function status(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO + { + $storageKey = $this->buildKey($key, $action, $platform); + $limit = $this->limits[$storageKey] ?? 10; + $current = $this->counts[$storageKey] ?? 0; + $remaining = $limit - $current; + + return new RateLimitStatusDTO($limit, $remaining, 60); + } + + private function buildKey(string $key, RateLimitActionInterface $action, PlatformInterface $platform): string + { + // Simple composite key simulation: key:action:platform + return sprintf('%s:%s:%s', $key, $action->value(), $platform->value()); + } +}