Passed
Pull Request — master (#204)
by Alexander
01:56
created

Counter::incrementAndGetResult()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 0
dl 0
loc 17
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\RateLimiter;
6
7
use Psr\SimpleCache\CacheInterface;
8
9
/**
10
 * Counter implements generic сell rate limit algorithm (GCRA) that ensures that after reaching the limit futher
11
 * requests are distributed equally.
12
 *
13
 * @link https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm
14
 */
15
final class Counter implements CounterInterface
16
{
17
    public const ID_PREFIX = 'rate-limiter-';
18
19
    private const MILLISECONDS_PER_SECOND = 1000;
20
21
    private int $period;
22
23
    private int $limit;
24
25
    private ?string $id = null;
26
27
    private CacheInterface $storage;
28
29
    private int $arrivalTime;
30
31
    public function __construct(int $limit, int $period, CacheInterface $storage)
32
    {
33
        if ($limit < 1) {
34
            throw new \InvalidArgumentException('The limit must be a positive value.');
35
        }
36
37
        if ($period < 1) {
38
            throw new \InvalidArgumentException('The period must be a positive value.');
39
        }
40
41
        $this->limit = $limit;
42
        $this->period = $period * self::MILLISECONDS_PER_SECOND;
43
        $this->storage = $storage;
44
    }
45
46
    public function setId(string $id): void
47
    {
48
        $this->id = self::ID_PREFIX . $id;
49
    }
50
51
    public function incrementAndGetResult(): CounterStatistics
52
    {
53
        if ($this->id === null) {
54
            throw new \LogicException('The counter id not set');
55
        }
56
57
        $this->arrivalTime = $this->getArrivalTime();
58
        $theoreticalArrivalTime = $this->calculateTheoreticalArrivalTime($this->getStorageValue());
59
        $remaining = $this->getRemaining($theoreticalArrivalTime);
60
61
        if ($remaining < 1) {
62
            return new CounterStatistics($this->limit, 0, $this->getResetAfter($theoreticalArrivalTime));
63
        }
64
65
        $this->setStorageValue($theoreticalArrivalTime);
66
67
        return new CounterStatistics($this->limit, (int)$remaining, $this->getResetAfter($theoreticalArrivalTime));
68
    }
69
70
    private function getEmissionInterval(): float
71
    {
72
        return (float)($this->period / $this->limit);
73
    }
74
75
    private function calculateTheoreticalArrivalTime(float $theoreticalArrivalTime): float
76
    {
77
        return max($this->arrivalTime, $theoreticalArrivalTime) + $this->getEmissionInterval();
78
    }
79
80
    private function getRemaining(float $theoreticalArrivalTime): float
81
    {
82
        $allowAt = $theoreticalArrivalTime - $this->period;
83
84
        return (floor($this->arrivalTime - $allowAt) / $this->getEmissionInterval()) + 0.5;
85
    }
86
87
    private function getStorageValue(): float
88
    {
89
        return $this->storage->get($this->id, (float)$this->arrivalTime);
90
    }
91
92
    private function setStorageValue(float $theoreticalArrivalTime): void
93
    {
94
        $this->storage->set($this->id, $theoreticalArrivalTime);
95
    }
96
97
    private function getArrivalTime(): int
98
    {
99
        return time() * self::MILLISECONDS_PER_SECOND;
100
    }
101
102
    private function getResetAfter(float $theoreticalArrivalTime): int
103
    {
104
        return (int)($theoreticalArrivalTime - $this->arrivalTime);
105
    }
106
}
107