| 1 |  |  | <?php | 
            
                                                                                                            
                            
            
                                    
            
            
                | 2 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 3 |  |  | declare(strict_types=1); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 4 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 5 |  |  | namespace Yiisoft\Yii\RateLimiter; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 6 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 7 |  |  | use InvalidArgumentException; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 8 |  |  | use Yiisoft\Yii\RateLimiter\Storage\StorageInterface; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 9 |  |  | use Yiisoft\Yii\RateLimiter\Time\MicrotimeTimer; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 10 |  |  | use Yiisoft\Yii\RateLimiter\Time\TimerInterface; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 11 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 12 |  |  | /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 13 |  |  |  * Counter implements generic cell rate limit algorithm (GCRA) that ensures that after reaching the limit further | 
            
                                                                                                            
                            
            
                                    
            
            
                | 14 |  |  |  * increments are distributed equally. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 15 |  |  |  * | 
            
                                                                                                            
                            
            
                                    
            
            
                | 16 |  |  |  * @link https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm | 
            
                                                                                                            
                            
            
                                    
            
            
                | 17 |  |  |  */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 18 |  |  | final class Counter implements CounterInterface | 
            
                                                                                                            
                            
            
                                    
            
            
                | 19 |  |  | { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 20 |  |  |     private const DEFAULT_TTL = 86400; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 21 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 22 |  |  |     private const ID_PREFIX = 'rate-limiter-'; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 23 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 24 |  |  |     private const MILLISECONDS_PER_SECOND = 1000; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 25 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 26 |  |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 27 |  |  |      * @var int Period to apply limit to. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 28 |  |  |      */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 29 |  |  |     private int $periodInMilliseconds; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 30 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 31 |  |  |     private int $limit; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 32 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 33 |  |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 34 |  |  |      * @var float Maximum interval before next increment. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 35 |  |  |      * In GCRA it is known as emission interval. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 36 |  |  |      */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 37 |  |  |     private float $incrementIntervalInMilliseconds; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 38 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 39 |  |  |     private StorageInterface $storage; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 40 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 41 |  |  |     private int $ttlInSeconds; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 42 |  |  |     private string $cachePrefix; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 43 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 44 |  |  |     private TimerInterface $timer; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 45 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 46 |  |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 47 |  |  |      * @param StorageInterface $storage | 
            
                                                                                                            
                            
            
                                    
            
            
                | 48 | 8 |  |      * @param int $limit Maximum number of increments that could be performed before increments are limited. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 49 |  |  |      * @param int $periodInSeconds Period to apply limit to. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 50 | 8 |  |      * @param int $ttlInSeconds | 
            
                                                                                                            
                            
            
                                    
            
            
                | 51 | 1 |  |      * @param TimerInterface|null $timer | 
            
                                                                                                            
                            
            
                                    
            
            
                | 52 |  |  |      * @param string $cachePrefix | 
            
                                                                                                            
                            
            
                                    
            
            
                | 53 |  |  |      */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 54 | 7 |  |     public function __construct( | 
            
                                                                                                            
                            
            
                                    
            
            
                | 55 | 1 |  |         StorageInterface $storage, | 
            
                                                                                                            
                            
            
                                    
            
            
                | 56 |  |  |         int $limit, | 
            
                                                                                                            
                            
            
                                    
            
            
                | 57 |  |  |         int $periodInSeconds, | 
            
                                                                                                            
                            
            
                                    
            
            
                | 58 | 6 |  |         int $ttlInSeconds = self::DEFAULT_TTL, | 
            
                                                                                                            
                            
            
                                    
            
            
                | 59 | 6 |  |         ?TimerInterface $timer = null, | 
            
                                                                                                            
                            
            
                                    
            
            
                | 60 | 6 |  |         string $cachePrefix = self::ID_PREFIX | 
            
                                                                                                            
                            
            
                                    
            
            
                | 61 |  |  |     ) { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 62 | 6 |  |         if ($limit < 1) { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 63 | 6 |  |             throw new InvalidArgumentException('The limit must be a positive value.'); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 64 |  |  |         } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 65 | 4 |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 66 |  |  |         if ($periodInSeconds < 1) { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 67 | 4 |  |             throw new InvalidArgumentException('The period must be a positive value.'); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 68 | 4 |  |         } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 69 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 70 |  |  |         $this->limit = $limit; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 71 |  |  |         $this->periodInMilliseconds = $periodInSeconds * self::MILLISECONDS_PER_SECOND; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 72 |  |  |         $this->storage = $storage; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 73 |  |  |         $this->ttlInSeconds = $ttlInSeconds; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 74 |  |  |         $this->cachePrefix = $cachePrefix; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 75 | 1 |  |         $this->timer = $timer ?: new MicrotimeTimer(); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 76 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 77 | 1 |  |         $this->incrementIntervalInMilliseconds = (float)($this->periodInMilliseconds / $this->limit); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 78 | 1 |  |     } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 79 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 80 |  |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 81 |  |  |      * {@inheritdoc} | 
            
                                                                                                            
                            
            
                                    
            
            
                | 82 |  |  |      */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 83 | 5 |  |     public function hit(string $id): CounterState | 
            
                                                                                                            
                            
            
                                    
            
            
                | 84 |  |  |     { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 85 | 5 |  |         // Last increment time. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 86 | 1 |  |         // In GCRA it's known as arrival time. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 87 |  |  |         $lastIncrementTimeInMilliseconds = $this->timer->nowInMilliseconds(); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 88 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 89 | 4 |  |         $theoreticalNextIncrementTime = $this->calculateTheoreticalNextIncrementTime( | 
            
                                                                                                            
                            
            
                                    
            
            
                | 90 |  |  |             $lastIncrementTimeInMilliseconds, | 
            
                                                                                                            
                            
            
                                    
            
            
                | 91 |  |  |             $this->getLastStoredTheoreticalNextIncrementTime($id, $lastIncrementTimeInMilliseconds) | 
            
                                                                                                            
                            
            
                                    
            
            
                | 92 | 5 |  |         ); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 93 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 94 | 5 |  |         $remaining = $this->calculateRemaining($lastIncrementTimeInMilliseconds, $theoreticalNextIncrementTime); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 95 | 1 |  |         $resetAfter = $this->calculateResetAfter($theoreticalNextIncrementTime); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 96 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 97 |  |  |         if ($remaining >= 1) { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 98 |  |  |             $this->storeTheoreticalNextIncrementTime($id, $theoreticalNextIncrementTime); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 99 |  |  |         } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 100 | 4 |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 101 |  |  |         return new CounterState($this->limit, $remaining, $resetAfter); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 102 | 4 |  |     } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 103 | 4 |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 104 | 4 |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 105 |  |  |      * @param int $lastIncrementTimeInMilliseconds | 
            
                                                                                                            
                            
            
                                    
            
            
                | 106 | 4 |  |      * @param float $storedTheoreticalNextIncrementTime | 
            
                                                                                                            
                            
            
                                    
            
            
                | 107 | 4 |  |      * | 
            
                                                                                                            
                            
            
                                    
            
            
                | 108 |  |  |      * @return float Theoretical increment time that would be expected from equally spaced increments at exactly rate | 
            
                                                                                                            
                            
            
                                    
            
            
                | 109 | 4 |  |      * limit. In GCRA it is known as TAT, theoretical arrival time. | 
            
                                                                                                            
                                                                
            
                                    
            
            
                | 110 | 3 |  |      */ | 
            
                                                                        
                            
            
                                    
            
            
                | 111 |  |  |     private function calculateTheoreticalNextIncrementTime( | 
            
                                                                        
                            
            
                                    
            
            
                | 112 |  |  |         int $lastIncrementTimeInMilliseconds, | 
            
                                                                        
                            
            
                                    
            
            
                | 113 | 4 |  |         float $storedTheoreticalNextIncrementTime | 
            
                                                                        
                            
            
                                    
            
            
                | 114 |  |  |     ): float { | 
            
                                                                        
                            
            
                                    
            
            
                | 115 |  |  |         return max($lastIncrementTimeInMilliseconds, $storedTheoreticalNextIncrementTime) + | 
            
                                                                        
                            
            
                                    
            
            
                | 116 |  |  |             $this->incrementIntervalInMilliseconds; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 117 |  |  |     } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 118 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 119 |  |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 120 | 4 |  |      * @param int $lastIncrementTimeInMilliseconds | 
            
                                                                                                            
                            
            
                                    
            
            
                | 121 |  |  |      * @param float $theoreticalNextIncrementTime | 
            
                                                                                                            
                            
            
                                    
            
            
                | 122 |  |  |      * | 
            
                                                                                                            
                            
            
                                    
            
            
                | 123 |  |  |      * @return int The number of remaining requests in the current time period. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 124 | 4 |  |      */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 125 | 4 |  |     private function calculateRemaining(int $lastIncrementTimeInMilliseconds, float $theoreticalNextIncrementTime): int | 
            
                                                                                                            
                            
            
                                    
            
            
                | 126 |  |  |     { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 127 |  |  |         $incrementAllowedAt = $theoreticalNextIncrementTime - $this->periodInMilliseconds; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 128 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 129 |  |  |         return (int)( | 
            
                                                                                                            
                            
            
                                    
            
            
                | 130 |  |  |             round($lastIncrementTimeInMilliseconds - $incrementAllowedAt) / | 
            
                                                                                                            
                            
            
                                    
            
            
                | 131 | 4 |  |             $this->incrementIntervalInMilliseconds | 
            
                                                                                                            
                            
            
                                    
            
            
                | 132 |  |  |         ); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 133 | 4 |  |     } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 134 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 135 |  |  |     private function getLastStoredTheoreticalNextIncrementTime(string $id, int $lastIncrementTimeInMilliseconds): float | 
            
                                                                                                            
                            
            
                                    
            
            
                | 136 | 4 |  |     { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 137 | 4 |  |         return (float)$this->storage->get($this->getCacheKey($id), $lastIncrementTimeInMilliseconds); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 138 |  |  |     } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 139 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 140 |  |  |     private function storeTheoreticalNextIncrementTime(string $id, float $theoreticalNextIncrementTime): void | 
            
                                                                                                            
                            
            
                                    
            
            
                | 141 | 4 |  |     { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 142 |  |  |         $this->storage->save($this->getCacheKey($id), $theoreticalNextIncrementTime, $this->ttlInSeconds); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 143 | 4 |  |     } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 144 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 145 |  |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 146 | 3 |  |      * @param float $theoreticalNextIncrementTime | 
            
                                                                                                            
                            
            
                                    
            
            
                | 147 |  |  |      * | 
            
                                                                                                            
                            
            
                                    
            
            
                | 148 | 3 |  |      * @return int Timestamp to wait until the rate limit resets. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 149 | 3 |  |      */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 150 |  |  |     private function calculateResetAfter(float $theoreticalNextIncrementTime): int | 
            
                                                                                                            
                            
            
                                    
            
            
                | 151 |  |  |     { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 152 |  |  |         return (int)($theoreticalNextIncrementTime / self::MILLISECONDS_PER_SECOND); | 
            
                                                                                                            
                            
            
                                    
            
            
                | 153 |  |  |     } | 
            
                                                                                                            
                            
            
                                    
            
            
                | 154 | 4 |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 155 |  |  |     /** | 
            
                                                                                                            
                            
            
                                    
            
            
                | 156 | 4 |  |      * @param string $id | 
            
                                                                                                            
                            
            
                                    
            
            
                | 157 |  |  |      * | 
            
                                                                                                            
                            
            
                                    
            
            
                | 158 |  |  |      * @return string Cache key used to store the next increment time. | 
            
                                                                                                            
                            
            
                                    
            
            
                | 159 | 4 |  |      */ | 
            
                                                                                                            
                            
            
                                    
            
            
                | 160 |  |  |     private function getCacheKey(string $id): string | 
            
                                                                                                            
                            
            
                                    
            
            
                | 161 | 4 |  |     { | 
            
                                                                                                            
                            
            
                                    
            
            
                | 162 |  |  |         return $this->cachePrefix . $id; | 
            
                                                                                                            
                            
            
                                    
            
            
                | 163 |  |  |     } | 
            
                                                                                                            
                                                                
            
                                    
            
            
                | 164 |  |  | } | 
            
                                                        
            
                                    
            
            
                | 165 |  |  |  |