chore: complete update
This commit is contained in:
64
src/Framework/RateLimit/RateLimitResult.php
Normal file
64
src/Framework/RateLimit/RateLimitResult.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
readonly class RateLimitResult
|
||||
{
|
||||
public function __construct(
|
||||
private bool $allowed,
|
||||
private int $limit,
|
||||
private int $current,
|
||||
private ?int $retryAfter = null
|
||||
) {}
|
||||
|
||||
public static function allowed(int $limit, int $current): self
|
||||
{
|
||||
return new self(
|
||||
allowed: true,
|
||||
limit: $limit,
|
||||
current: $current
|
||||
);
|
||||
}
|
||||
|
||||
public static function exceeded(int $limit, int $current, int $retryAfter): self
|
||||
{
|
||||
return new self(
|
||||
allowed: false,
|
||||
limit: $limit,
|
||||
current: $current,
|
||||
retryAfter: $retryAfter
|
||||
);
|
||||
}
|
||||
|
||||
public function isAllowed(): bool
|
||||
{
|
||||
return $this->allowed;
|
||||
}
|
||||
|
||||
public function isExceeded(): bool
|
||||
{
|
||||
return !$this->allowed;
|
||||
}
|
||||
|
||||
public function getLimit(): int
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
public function getCurrent(): int
|
||||
{
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
public function getRetryAfter(): ?int
|
||||
{
|
||||
return $this->retryAfter;
|
||||
}
|
||||
|
||||
public function getRemainingRequests(): int
|
||||
{
|
||||
return max(0, $this->limit - $this->current);
|
||||
}
|
||||
}
|
||||
88
src/Framework/RateLimit/RateLimiter.php
Normal file
88
src/Framework/RateLimit/RateLimiter.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
use App\Framework\RateLimit\Storage\StorageInterface;
|
||||
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
|
||||
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
|
||||
|
||||
final readonly class RateLimiter
|
||||
{
|
||||
public function __construct(
|
||||
private StorageInterface $storage,
|
||||
private TimeProviderInterface $timeProvider = new SystemTimeProvider()
|
||||
) {}
|
||||
|
||||
public function checkLimit(string $key, int $limit, int $window): RateLimitResult
|
||||
{
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
$windowStart = $now - $window;
|
||||
|
||||
// Sliding Window Log Implementierung
|
||||
$requests = $this->storage->getRequestsInWindow($key, $windowStart, $now);
|
||||
|
||||
if (count($requests) >= $limit) {
|
||||
$oldestRequest = min($requests);
|
||||
$retryAfter = $oldestRequest + $window - $now;
|
||||
|
||||
return RateLimitResult::exceeded($limit, count($requests), max(1, $retryAfter));
|
||||
}
|
||||
|
||||
// Request hinzufügen
|
||||
$this->storage->addRequest($key, $now, $window);
|
||||
|
||||
return RateLimitResult::allowed($limit, count($requests) + 1);
|
||||
}
|
||||
|
||||
public function checkTokenBucket(string $key, int $capacity, int $refillRate, int $tokens = 1): RateLimitResult
|
||||
{
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
$bucket = $this->storage->getTokenBucket($key);
|
||||
|
||||
if ($bucket === null) {
|
||||
$bucket = new TokenBucket($capacity, $capacity, $now);
|
||||
}
|
||||
|
||||
// Token nachfüllen
|
||||
$timePassed = $now - $bucket->lastRefill;
|
||||
$tokensToAdd = min($capacity - $bucket->tokens, intval($timePassed * $refillRate));
|
||||
|
||||
$bucket = new TokenBucket(
|
||||
$bucket->capacity,
|
||||
$bucket->tokens + $tokensToAdd,
|
||||
$now
|
||||
);
|
||||
|
||||
if ($bucket->tokens >= $tokens) {
|
||||
$bucket = new TokenBucket(
|
||||
$bucket->capacity,
|
||||
$bucket->tokens - $tokens,
|
||||
$bucket->lastRefill
|
||||
);
|
||||
|
||||
$this->storage->saveTokenBucket($key, $bucket);
|
||||
|
||||
return RateLimitResult::allowed($capacity, $bucket->tokens);
|
||||
}
|
||||
|
||||
$this->storage->saveTokenBucket($key, $bucket);
|
||||
|
||||
$retryAfter = intval(($tokens - $bucket->tokens) / $refillRate);
|
||||
return RateLimitResult::exceeded($capacity, $bucket->tokens, $retryAfter);
|
||||
}
|
||||
|
||||
public function reset(string $key): void
|
||||
{
|
||||
$this->storage->clear($key);
|
||||
}
|
||||
|
||||
public function getUsage(string $key, int $window): int
|
||||
{
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
$windowStart = $now - $window;
|
||||
|
||||
return count($this->storage->getRequestsInWindow($key, $windowStart, $now));
|
||||
}
|
||||
}
|
||||
68
src/Framework/RateLimit/Storage/CacheStorage.php
Normal file
68
src/Framework/RateLimit/Storage/CacheStorage.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit\Storage;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\RateLimit\TokenBucket;
|
||||
|
||||
final readonly class CacheStorage implements StorageInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $cache
|
||||
) {}
|
||||
|
||||
public function getRequestsInWindow(string $key, int $windowStart, int $windowEnd): array
|
||||
{
|
||||
$requests = $this->cache->get("rate_limit:$key:requests") ?? [];
|
||||
|
||||
// Alte Requests entfernen
|
||||
$requests = array_filter($requests, fn($timestamp) => $timestamp >= $windowStart);
|
||||
|
||||
// Cache aktualisieren
|
||||
$this->cache->set("rate_limit:$key:requests", $requests, 3600);
|
||||
|
||||
return $requests;
|
||||
}
|
||||
|
||||
public function addRequest(string $key, int $timestamp, int $ttl): void
|
||||
{
|
||||
$requests = $this->cache->get("rate_limit:$key:requests") ?? [];
|
||||
$requests[] = $timestamp;
|
||||
|
||||
$this->cache->set("rate_limit:$key:requests", $requests, $ttl);
|
||||
}
|
||||
|
||||
public function getTokenBucket(string $key): ?TokenBucket
|
||||
{
|
||||
$data = $this->cache->get("rate_limit:$key:bucket");
|
||||
|
||||
if ($data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TokenBucket(
|
||||
capacity: $data['capacity'],
|
||||
tokens: $data['tokens'],
|
||||
lastRefill: $data['last_refill']
|
||||
);
|
||||
}
|
||||
|
||||
public function saveTokenBucket(string $key, TokenBucket $bucket): void
|
||||
{
|
||||
$data = [
|
||||
'capacity' => $bucket->capacity,
|
||||
'tokens' => $bucket->tokens,
|
||||
'last_refill' => $bucket->lastRefill
|
||||
];
|
||||
|
||||
$this->cache->set("rate_limit:$key:bucket", $data, 3600);
|
||||
}
|
||||
|
||||
public function clear(string $key): void
|
||||
{
|
||||
$this->cache->forget("rate_limit:$key:requests");
|
||||
$this->cache->forget("rate_limit:$key:bucket");
|
||||
}
|
||||
}
|
||||
16
src/Framework/RateLimit/Storage/StorageInterface.php
Normal file
16
src/Framework/RateLimit/Storage/StorageInterface.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit\Storage;
|
||||
|
||||
use App\Framework\RateLimit\TokenBucket;
|
||||
|
||||
interface StorageInterface
|
||||
{
|
||||
public function getRequestsInWindow(string $key, int $windowStart, int $windowEnd): array;
|
||||
public function addRequest(string $key, int $timestamp, int $ttl): void;
|
||||
public function getTokenBucket(string $key): ?TokenBucket;
|
||||
public function saveTokenBucket(string $key, TokenBucket $bucket): void;
|
||||
public function clear(string $key): void;
|
||||
}
|
||||
13
src/Framework/RateLimit/TimeProvider/SystemTimeProvider.php
Normal file
13
src/Framework/RateLimit/TimeProvider/SystemTimeProvider.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit\TimeProvider;
|
||||
|
||||
final readonly class SystemTimeProvider implements TimeProviderInterface
|
||||
{
|
||||
public function getCurrentTime(): int
|
||||
{
|
||||
return time();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit\TimeProvider;
|
||||
|
||||
interface TimeProviderInterface
|
||||
{
|
||||
public function getCurrentTime(): int;
|
||||
}
|
||||
43
src/Framework/RateLimit/TokenBucket.php
Normal file
43
src/Framework/RateLimit/TokenBucket.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
final readonly class TokenBucket
|
||||
{
|
||||
public function __construct(
|
||||
public int $capacity,
|
||||
public int $tokens,
|
||||
public int $lastRefill
|
||||
) {}
|
||||
|
||||
public function canConsume(int $tokens): bool
|
||||
{
|
||||
return $this->tokens >= $tokens;
|
||||
}
|
||||
|
||||
public function consume(int $tokens): self
|
||||
{
|
||||
if (!$this->canConsume($tokens)) {
|
||||
throw new \InvalidArgumentException('Not enough tokens available');
|
||||
}
|
||||
|
||||
return new self(
|
||||
$this->capacity,
|
||||
$this->tokens - $tokens,
|
||||
$this->lastRefill
|
||||
);
|
||||
}
|
||||
|
||||
public function refill(int $tokens, int $timestamp): self
|
||||
{
|
||||
$newTokens = min($this->capacity, $this->tokens + $tokens);
|
||||
|
||||
return new self(
|
||||
$this->capacity,
|
||||
$newTokens,
|
||||
$timestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user