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
38 changes: 38 additions & 0 deletions examples/phase5_5/examples.phase5_5.md
Original file line number Diff line number Diff line change
@@ -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`.
74 changes: 74 additions & 0 deletions examples/phase5_5/native/action_limiter_flow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/../../../vendor/autoload.php';

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;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

// 1. Define Dummy Classes
$action = new class implements RateLimitActionInterface {
public function value(): string
{
return 'upload';
}
};

$platform = new class implements PlatformInterface {
public function value(): string
{
return 'api';
}
};

// 2. Define a Dummy Driver that passes Global but fails Action
$driver = new class implements RateLimiterInterface {
public function attempt(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO
{
// Pass Global
if ($action->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
}
}
34 changes: 34 additions & 0 deletions examples/phase5_5/native/dto_serialization.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/../../../vendor/autoload.php';

use Maatify\RateLimiter\DTO\RateLimitStatusDTO;

// 1. Create a DTO manually
$dto = new RateLimitStatusDTO(
limit: 100,
remaining: 50,
resetAfter: 30,
retryAfter: null,
blocked: false,
backoffSeconds: null,
nextAllowedAt: null,
source: 'redis'
);

// 2. Access Properties
echo "Limit: " . $dto->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";
35 changes: 35 additions & 0 deletions examples/phase5_5/native/exception_flow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/../../../vendor/autoload.php';

use Maatify\RateLimiter\DTO\RateLimitStatusDTO;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;

// 1. Create Exception with DTO
$status = new RateLimitStatusDTO(
limit: 60,
remaining: 0,
resetAfter: 120,
retryAfter: 120,
blocked: true,
backoffSeconds: 120,
nextAllowedAt: '2025-01-01 12:00:00',
source: 'mysql'
);

try {
throw new TooManyRequestsException('Rate limit exceeded', 429, $status);
} catch (TooManyRequestsException $e) {
// 2. Catch and Inspect
echo "Message: " . $e->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";
}
}
83 changes: 83 additions & 0 deletions examples/phase5_5/native/exponential_backoff_flow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/../../../vendor/autoload.php';

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;
use Maatify\RateLimiter\Resolver\ExponentialBackoffPolicy;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

// 1. Define Dummy Classes
$action = new class implements RateLimitActionInterface {
public function value(): string
{
return 'sms_send';
}
};

$platform = new class implements PlatformInterface {
public function value(): string
{
return 'api';
}
};

// 2. Define Driver that throws Exception with negative remaining to simulate overage
// In RedisRateLimiter, remaining = limit - current. If current > 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";
}
71 changes: 71 additions & 0 deletions examples/phase5_5/native/global_limiter_flow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/../../../vendor/autoload.php';

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;
use Maatify\RateLimiter\Resolver\RateLimiterResolver;

// 1. Define Dummy Classes for Demonstration
$action = new class implements RateLimitActionInterface {
public function value(): string
{
return 'login';
}
};

$platform = new class implements PlatformInterface {
public function value(): string
{
return 'web';
}
};

// 2. Define a Dummy Driver that fails on Global Check
// This simulates a scenario where the Global Rate Limit is exhausted.
$driver = new class implements RateLimiterInterface {
public function attempt(string $key, RateLimitActionInterface $action, PlatformInterface $platform): RateLimitStatusDTO
{
// EnforcingRateLimiter calls attempt('key', globalAction, globalPlatform) first.
if ($action->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
}
}