Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions examples/phase5_5/examples.phase5_5.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 39 additions & 0 deletions examples/phase5_5/native/01_basic_flow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../../../vendor/autoload.php';
require __DIR__ . '/InMemoryDriver.php';

use Maatify\RateLimiter\Examples\Phase5_5\ExampleAction;
use Maatify\RateLimiter\Examples\Phase5_5\ExamplePlatform;
use Maatify\RateLimiter\Examples\Phase5_5\InMemoryDriver;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

// 1. Setup the dummy driver
$driver = new InMemoryDriver();

// 2. Setup the Resolver (Entry Point)
// The Resolver is configured with a map of drivers.
$resolver = new RateLimiterResolver(['memory' => $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";
}
45 changes: 45 additions & 0 deletions examples/phase5_5/native/02_global_blocking.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../../../vendor/autoload.php';
require __DIR__ . '/InMemoryDriver.php';

use Maatify\RateLimiter\Examples\Phase5_5\ExampleAction;
use Maatify\RateLimiter\Examples\Phase5_5\ExamplePlatform;
use Maatify\RateLimiter\Examples\Phase5_5\InMemoryDriver;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

$driver = new InMemoryDriver();

// Configure the driver to limit the GLOBAL scope to 1 request.
// The EnforcingRateLimiter uses 'global' as the action and platform name for global checks.
// Driver Key Format: key:action:platform
$driver->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";
}
}
45 changes: 45 additions & 0 deletions examples/phase5_5/native/03_action_blocking.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../../../vendor/autoload.php';
require __DIR__ . '/InMemoryDriver.php';

use Maatify\RateLimiter\Examples\Phase5_5\ExampleAction;
use Maatify\RateLimiter\Examples\Phase5_5\ExamplePlatform;
use Maatify\RateLimiter\Examples\Phase5_5\InMemoryDriver;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

$driver = new InMemoryDriver();

// Global limit is high (10), so it won't block.
$driver->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";
}
}
46 changes: 46 additions & 0 deletions examples/phase5_5/native/04_backoff_flow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../../../vendor/autoload.php';
require __DIR__ . '/InMemoryDriver.php';

use Maatify\RateLimiter\Examples\Phase5_5\ExampleAction;
use Maatify\RateLimiter\Examples\Phase5_5\ExamplePlatform;
use Maatify\RateLimiter\Examples\Phase5_5\InMemoryDriver;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

$driver = new InMemoryDriver();
// Strict limit of 1
$driver->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";
}
36 changes: 36 additions & 0 deletions examples/phase5_5/native/05_exception_handling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../../../vendor/autoload.php';
require __DIR__ . '/InMemoryDriver.php';

use Maatify\RateLimiter\Examples\Phase5_5\ExampleAction;
use Maatify\RateLimiter\Examples\Phase5_5\ExamplePlatform;
use Maatify\RateLimiter\Examples\Phase5_5\InMemoryDriver;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

$driver = new InMemoryDriver();
$driver->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";
}
}
99 changes: 99 additions & 0 deletions examples/phase5_5/native/InMemoryDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Maatify\RateLimiter\Examples\Phase5_5;

use Maatify\RateLimiter\Contracts\PlatformInterface;
use Maatify\RateLimiter\Contracts\RateLimitActionInterface;
use Maatify\RateLimiter\Contracts\RateLimiterInterface;
use Maatify\RateLimiter\DTO\RateLimitStatusDTO;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;

final class ExampleAction implements RateLimitActionInterface
{
public function __construct(private string $name = 'test_action')
{
}

public function value(): string
{
return $this->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<string, int> */
private array $counts = [];

/**
* @param array<string, int> $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());
}
}