This commit is contained in:
team3
2026-06-03 22:05:20 +02:00
parent d7e8df6876
commit 40de56c27b
5119 changed files with 552560 additions and 24 deletions

View File

@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
use Symfony\Component\RateLimiter\Util\TimeUtil;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class FixedWindowLimiter implements LimiterInterface
{
use ResetLimiterTrait;
private int $limit;
private int $interval;
public function __construct(string $id, int $limit, \DateInterval $interval, StorageInterface $storage, ?LockInterface $lock = null)
{
if ($limit < 1) {
throw new \InvalidArgumentException(\sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', __CLASS__));
}
$this->storage = $storage;
$this->lock = $lock;
$this->id = $id;
$this->limit = $limit;
$this->interval = TimeUtil::dateIntervalToSeconds($interval);
}
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
{
if ($tokens > $this->limit) {
throw new \InvalidArgumentException(\sprintf('Cannot reserve more tokens (%d) than the size of the rate limiter (%d).', $tokens, $this->limit));
}
$this->lock?->acquire(true);
try {
$window = $this->storage->fetch($this->id);
if (!$window instanceof Window) {
$window = new Window($this->id, $this->interval, $this->limit);
}
$now = microtime(true);
$availableTokens = $window->getAvailableTokens($now);
if (0 === $tokens) {
$waitDuration = $window->calculateTimeForTokens(1, $now);
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), true, $this->limit));
} elseif ($availableTokens >= $tokens) {
$window->add($tokens, $now);
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
} else {
$waitDuration = $window->calculateTimeForTokens($tokens, $now);
if (null !== $maxTime && $waitDuration > $maxTime) {
// process needs to wait longer than set interval
throw new MaxWaitDurationExceededException(\sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
}
$window->add($tokens, $now);
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
}
if (0 < $tokens) {
$this->storage->save($window);
}
} finally {
$this->lock?->release();
}
return $reservation;
}
public function consume(int $tokens = 1): RateLimit
{
try {
return $this->reserve($tokens, 0)->getRateLimit();
} catch (MaxWaitDurationExceededException $e) {
return $e->getRateLimit();
}
}
public function getAvailableTokens(int $hitCount): int
{
return $this->limit - $hitCount;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;
/**
* Implements a non limiting limiter.
*
* This can be used in cases where an implementation requires a
* limiter, but no rate limit should be enforced.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class NoLimiter implements LimiterInterface
{
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
{
return new Reservation(microtime(true), new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX));
}
public function consume(int $tokens = 1): RateLimit
{
return new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX);
}
public function reset(): void
{
}
}

View File

@@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\RateLimiter\Util\TimeUtil;
/**
* Data object representing the fill rate of a token bucket.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class Rate
{
private \DateInterval $refillTime;
private int $refillAmount;
public function __construct(\DateInterval $refillTime, int $refillAmount = 1)
{
$this->refillTime = $refillTime;
$this->refillAmount = $refillAmount;
}
public static function perSecond(int $rate = 1): self
{
return new static(new \DateInterval('PT1S'), $rate);
}
public static function perMinute(int $rate = 1): self
{
return new static(new \DateInterval('PT1M'), $rate);
}
public static function perHour(int $rate = 1): self
{
return new static(new \DateInterval('PT1H'), $rate);
}
public static function perDay(int $rate = 1): self
{
return new static(new \DateInterval('P1D'), $rate);
}
public static function perMonth(int $rate = 1): self
{
return new static(new \DateInterval('P1M'), $rate);
}
public static function perYear(int $rate = 1): self
{
return new static(new \DateInterval('P1Y'), $rate);
}
/**
* @param string $string using the format: "%interval_spec%-%rate%", {@see DateInterval}
*/
public static function fromString(string $string): self
{
[$interval, $rate] = explode('-', $string, 2);
return new static(new \DateInterval($interval), $rate);
}
/**
* Calculates the time needed to free up the provided number of tokens in seconds.
*/
public function calculateTimeForTokens(int $tokens): int
{
$cyclesRequired = ceil($tokens / $this->refillAmount);
return TimeUtil::dateIntervalToSeconds($this->refillTime) * $cyclesRequired;
}
/**
* Calculates the next moment of token availability.
*/
public function calculateNextTokenAvailability(): \DateTimeImmutable
{
return (new \DateTimeImmutable())->add($this->refillTime);
}
/**
* Calculates the number of new free tokens during $duration.
*
* @param float $duration interval in seconds
*/
public function calculateNewTokensDuringInterval(float $duration): int
{
$cycles = floor($duration / TimeUtil::dateIntervalToSeconds($this->refillTime));
return $cycles * $this->refillAmount;
}
/**
* Calculates total amount in seconds of refill intervals during $duration (for maintain strict refill frequency).
*
* @param float $duration interval in seconds
*/
public function calculateRefillInterval(float $duration): int
{
$cycleTime = TimeUtil::dateIntervalToSeconds($this->refillTime);
return floor($duration / $cycleTime) * $cycleTime;
}
public function __toString(): string
{
return $this->refillTime->format('P%yY%mM%dDT%HH%iM%sS').'-'.$this->refillAmount;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
trait ResetLimiterTrait
{
private ?LockInterface $lock;
private StorageInterface $storage;
private string $id;
public function reset(): void
{
try {
$this->lock?->acquire(true);
$this->storage->delete($this->id);
} finally {
$this->lock?->release();
}
}
}

View File

@@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\RateLimiter\Exception\InvalidIntervalException;
use Symfony\Component\RateLimiter\LimiterStateInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @internal
*/
final class SlidingWindow implements LimiterStateInterface
{
private string $id;
private int $hitCount = 0;
private int $hitCountForLastWindow = 0;
private int $intervalInSeconds;
private float $windowEndAt;
public function __construct(string $id, int $intervalInSeconds)
{
if ($intervalInSeconds < 1) {
throw new InvalidIntervalException(\sprintf('The interval must be positive integer, "%d" given.', $intervalInSeconds));
}
$this->id = $id;
$this->intervalInSeconds = $intervalInSeconds;
$this->windowEndAt = microtime(true) + $intervalInSeconds;
}
public static function createFromPreviousWindow(self $window, int $intervalInSeconds): self
{
$new = new self($window->id, $intervalInSeconds);
$windowEndAt = $window->windowEndAt + $intervalInSeconds;
if (microtime(true) < $windowEndAt) {
$new->hitCountForLastWindow = $window->hitCount;
$new->windowEndAt = $windowEndAt;
}
return $new;
}
public function getId(): string
{
return $this->id;
}
/**
* Returns the remaining of this timeframe and the next one.
*/
public function getExpirationTime(): int
{
return (int) ($this->windowEndAt + $this->intervalInSeconds - microtime(true));
}
public function isExpired(): bool
{
return microtime(true) > $this->windowEndAt;
}
public function add(int $hits = 1): void
{
$this->hitCount += $hits;
}
/**
* Calculates the sliding window number of request.
*/
public function getHitCount(): int
{
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
$percentOfCurrentTimeFrame = min((microtime(true) - $startOfWindow) / $this->intervalInSeconds, 1);
return (int) floor($this->hitCountForLastWindow * (1 - $percentOfCurrentTimeFrame) + $this->hitCount);
}
/**
* @deprecated since Symfony 6.4, use {@see self::calculateTimeForTokens} instead
*/
public function getRetryAfter(): \DateTimeImmutable
{
trigger_deprecation('symfony/ratelimiter', '6.4', 'The "%s()" method is deprecated, use "%s::calculateTimeForTokens" instead.', __METHOD__, self::class);
return \DateTimeImmutable::createFromFormat('U.u', \sprintf('%.6F', microtime(true) + $this->calculateTimeForTokens(max(1, $this->getHitCount()), 1)));
}
public function calculateTimeForTokens(int $maxSize, int $tokens): float
{
$remaining = $maxSize - $this->getHitCount();
if ($remaining >= $tokens) {
return 0;
}
$time = microtime(true);
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
$timePassed = $time - $startOfWindow;
$windowPassed = min($timePassed / $this->intervalInSeconds, 1);
$releasable = max(1, $maxSize - floor($this->hitCountForLastWindow * (1 - $windowPassed)));
$remainingWindow = $this->intervalInSeconds - $timePassed;
$needed = $tokens - $remaining;
if ($releasable >= $needed) {
return $needed * ($remainingWindow / max(1, $releasable));
}
return ($this->windowEndAt - $time) + ($needed - $releasable) * ($this->intervalInSeconds / $maxSize);
}
public function __serialize(): array
{
return [
pack('NNN', $this->hitCount, $this->hitCountForLastWindow, $this->intervalInSeconds).$this->id => $this->windowEndAt,
];
}
public function __unserialize(array $data): void
{
// BC layer for old objects serialized via __sleep
if (5 === \count($data)) {
$data = array_values($data);
$this->id = $data[0];
$this->hitCount = $data[1];
$this->intervalInSeconds = $data[2];
$this->hitCountForLastWindow = $data[3];
$this->windowEndAt = $data[4];
return;
}
$pack = key($data);
$this->windowEndAt = $data[$pack];
['a' => $this->hitCount, 'b' => $this->hitCountForLastWindow, 'c' => $this->intervalInSeconds] = unpack('Na/Nb/Nc', $pack);
$this->id = substr($pack, 12);
}
}

View File

@@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
use Symfony\Component\RateLimiter\Util\TimeUtil;
/**
* The sliding window algorithm will look at your last window and the current one.
* It is good algorithm to reduce bursts.
*
* Example:
* Last time window we did 8 hits. We are currently 25% into
* the current window. We have made 3 hits in the current window so far.
* That means our sliding window hit count is (75% * 8) + 3 = 9.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class SlidingWindowLimiter implements LimiterInterface
{
use ResetLimiterTrait;
private int $limit;
private int $interval;
public function __construct(string $id, int $limit, \DateInterval $interval, StorageInterface $storage, ?LockInterface $lock = null)
{
$this->storage = $storage;
$this->lock = $lock;
$this->id = $id;
$this->limit = $limit;
$this->interval = TimeUtil::dateIntervalToSeconds($interval);
}
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
{
if ($tokens > $this->limit) {
throw new \InvalidArgumentException(\sprintf('Cannot reserve more tokens (%d) than the size of the rate limiter (%d).', $tokens, $this->limit));
}
$this->lock?->acquire(true);
try {
$window = $this->storage->fetch($this->id);
if (!$window instanceof SlidingWindow) {
$window = new SlidingWindow($this->id, $this->interval);
} elseif ($window->isExpired()) {
$window = SlidingWindow::createFromPreviousWindow($window, $this->interval);
}
$now = microtime(true);
$hitCount = $window->getHitCount();
$availableTokens = $this->getAvailableTokens($hitCount);
if (0 === $tokens) {
$resetDuration = $window->calculateTimeForTokens($this->limit, $window->getHitCount());
$resetTime = \DateTimeImmutable::createFromFormat('U', $availableTokens ? floor($now) : floor($now + $resetDuration));
return new Reservation($now, new RateLimit($availableTokens, $resetTime, true, $this->limit));
}
if ($availableTokens >= $tokens) {
$window->add($tokens);
$reservation = new Reservation($now, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
} else {
$waitDuration = $window->calculateTimeForTokens($this->limit, $tokens);
if (null !== $maxTime && $waitDuration > $maxTime) {
// process needs to wait longer than set interval
throw new MaxWaitDurationExceededException(\sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
}
$window->add($tokens);
$reservation = new Reservation($now + $waitDuration, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
}
if (0 < $tokens) {
$this->storage->save($window);
}
} finally {
$this->lock?->release();
}
return $reservation;
}
public function consume(int $tokens = 1): RateLimit
{
try {
return $this->reserve($tokens, 0)->getRateLimit();
} catch (MaxWaitDurationExceededException $e) {
return $e->getRateLimit();
}
}
private function getAvailableTokens(int $hitCount): int
{
return $this->limit - $hitCount;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\RateLimiter\LimiterStateInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @internal
*/
final class TokenBucket implements LimiterStateInterface
{
private string $id;
private Rate $rate;
private int $tokens;
private int $burstSize;
private float $timer;
/**
* @param string $id unique identifier for this bucket
* @param int $initialTokens the initial number of tokens in the bucket (i.e. the max burst size)
* @param Rate $rate the fill rate and time of this bucket
* @param float|null $timer the current timer of the bucket, defaulting to microtime(true)
*/
public function __construct(string $id, int $initialTokens, Rate $rate, ?float $timer = null)
{
if ($initialTokens < 1) {
throw new \InvalidArgumentException(\sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', TokenBucketLimiter::class));
}
$this->id = $id;
$this->tokens = $this->burstSize = $initialTokens;
$this->rate = $rate;
$this->timer = $timer ?? microtime(true);
}
public function getId(): string
{
return $this->id;
}
public function setTimer(float $microtime): void
{
$this->timer = $microtime;
}
public function getTimer(): float
{
return $this->timer;
}
public function setTokens(int $tokens): void
{
$this->tokens = $tokens;
}
public function getAvailableTokens(float $now): int
{
$elapsed = max(0, $now - $this->timer);
$newTokens = $this->rate->calculateNewTokensDuringInterval($elapsed);
if ($newTokens > 0) {
$this->timer += $this->rate->calculateRefillInterval($elapsed);
}
return min($this->burstSize, $this->tokens + $newTokens);
}
public function getExpirationTime(): int
{
return $this->rate->calculateTimeForTokens($this->burstSize);
}
public function __serialize(): array
{
return [
pack('N', $this->burstSize).$this->id => $this->tokens,
(string) $this->rate => $this->timer,
];
}
public function __unserialize(array $data): void
{
// BC layer for old objects serialized via __sleep
if (5 === \count($data)) {
$data = array_values($data);
$this->id = $data[0];
$this->tokens = $data[1];
$this->timer = $data[2];
$this->burstSize = $data[3];
$this->rate = Rate::fromString($data[4]);
return;
}
[$this->tokens, $this->timer] = array_values($data);
[$pack, $rate] = array_keys($data);
$this->rate = Rate::fromString($rate);
$this->burstSize = unpack('Na', $pack)['a'];
$this->id = substr($pack, 4);
}
}

View File

@@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\Reservation;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class TokenBucketLimiter implements LimiterInterface
{
use ResetLimiterTrait;
private int $maxBurst;
private Rate $rate;
public function __construct(string $id, int $maxBurst, Rate $rate, StorageInterface $storage, ?LockInterface $lock = null)
{
$this->id = $id;
$this->maxBurst = $maxBurst;
$this->rate = $rate;
$this->storage = $storage;
$this->lock = $lock;
}
/**
* Waits until the required number of tokens is available.
*
* The reserved tokens will be taken into account when calculating
* future token consumptions. Do not use this method if you intend
* to skip this process.
*
* @param int $tokens the number of tokens required
* @param float|null $maxTime maximum accepted waiting time in seconds
*
* @throws MaxWaitDurationExceededException if $maxTime is set and the process needs to wait longer than its value (in seconds)
* @throws \InvalidArgumentException if $tokens is larger than the maximum burst size
*/
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
{
if ($tokens > $this->maxBurst) {
throw new \InvalidArgumentException(\sprintf('Cannot reserve more tokens (%d) than the burst size of the rate limiter (%d).', $tokens, $this->maxBurst));
}
$this->lock?->acquire(true);
try {
$bucket = $this->storage->fetch($this->id);
if (!$bucket instanceof TokenBucket) {
$bucket = new TokenBucket($this->id, $this->maxBurst, $this->rate);
}
$now = microtime(true);
$availableTokens = $bucket->getAvailableTokens($now);
if ($availableTokens > $this->maxBurst) {
$availableTokens = $this->maxBurst;
}
if ($availableTokens >= $tokens) {
// tokens are now available, update bucket
$bucket->setTokens($availableTokens - $tokens);
if (0 === $availableTokens) {
// This means 0 tokens where consumed (discouraged in most cases).
// Return the first time a new token is available
$waitDuration = $this->rate->calculateTimeForTokens(1);
$waitTime = \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration));
} else {
$waitTime = \DateTimeImmutable::createFromFormat('U', floor($now));
}
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), $waitTime, true, $this->maxBurst));
} else {
$remainingTokens = $tokens - $availableTokens;
$waitDuration = $this->rate->calculateTimeForTokens($remainingTokens);
if (null !== $maxTime && $waitDuration > $maxTime) {
// process needs to wait longer than set interval
$rateLimit = new RateLimit($availableTokens, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst);
throw new MaxWaitDurationExceededException(\sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), $rateLimit);
}
// at $now + $waitDuration all tokens will be reserved for this process,
// so no tokens are left for other processes.
$bucket->setTokens($availableTokens - $tokens);
$reservation = new Reservation($now + $waitDuration, new RateLimit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst));
}
if (0 < $tokens) {
$this->storage->save($bucket);
}
} finally {
$this->lock?->release();
}
return $reservation;
}
public function consume(int $tokens = 1): RateLimit
{
try {
return $this->reserve($tokens, 0)->getRateLimit();
} catch (MaxWaitDurationExceededException $e) {
return $e->getRateLimit();
}
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter\Policy;
use Symfony\Component\RateLimiter\LimiterStateInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @internal
*/
final class Window implements LimiterStateInterface
{
private string $id;
private int $hitCount = 0;
private int $intervalInSeconds;
private int $maxSize;
private float $timer;
public function __construct(string $id, int $intervalInSeconds, int $windowSize, ?float $timer = null)
{
$this->id = $id;
$this->intervalInSeconds = $intervalInSeconds;
$this->maxSize = $windowSize;
$this->timer = $timer ?? microtime(true);
}
public function getId(): string
{
return $this->id;
}
public function getExpirationTime(): ?int
{
return $this->intervalInSeconds;
}
public function add(int $hits = 1, ?float $now = null): void
{
$now ??= microtime(true);
if (($now - $this->timer) > $this->intervalInSeconds) {
// reset window
$this->timer = $now;
$this->hitCount = 0;
}
$this->hitCount += $hits;
}
public function getHitCount(): int
{
return $this->hitCount;
}
public function getAvailableTokens(float $now): int
{
// if now is more than the window interval in the past, all tokens are available
if (($now - $this->timer) > $this->intervalInSeconds) {
return $this->maxSize;
}
return $this->maxSize - $this->hitCount;
}
public function calculateTimeForTokens(int $tokens, float $now): int
{
if (($this->maxSize - $this->hitCount) >= $tokens) {
return 0;
}
return (int) ceil($this->timer + $this->intervalInSeconds - $now);
}
public function __serialize(): array
{
return [
$this->id => $this->timer,
pack('NN', $this->hitCount, $this->intervalInSeconds) => $this->maxSize,
];
}
public function __unserialize(array $data): void
{
// BC layer for old objects serialized via __sleep
if (5 === \count($data)) {
$data = array_values($data);
$this->id = $data[0];
$this->hitCount = $data[1];
$this->intervalInSeconds = $data[2];
$this->maxSize = $data[3];
$this->timer = $data[4];
return;
}
[$this->timer, $this->maxSize] = array_values($data);
[$this->id, $pack] = array_keys($data);
['a' => $this->hitCount, 'b' => $this->intervalInSeconds] = unpack('Na/Nb', $pack);
}
}