Completed
Push — master ( 5c027e...c45ab2 )
by Nikola
03:38
created

MemcachedRateLimiter::limitSilently()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 0
cts 16
cp 0
rs 9.6666
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace RateLimit;
6
7
use Memcached;
8
use RateLimit\Exception\CannotUseRateLimiter;
9
use RateLimit\Exception\LimitExceeded;
10
use function max;
11
use function sprintf;
12
use function time;
13
14
final class MemcachedRateLimiter implements RateLimiter, SilentRateLimiter
15
{
16
    private const MEMCACHED_SECONDS_LIMIT = 2592000; // 30 days in seconds
17
18
    /** @var Memcached */
19
    private $memcached;
20
21
    /** @var string */
22
    private $keyPrefix;
23
24
    public function __construct(Memcached $memcached, string $keyPrefix = '')
25
    {
26
        // @see https://www.php.net/manual/en/memcached.increment.php#111187
27
        if ($memcached->getOption(Memcached::OPT_BINARY_PROTOCOL) !== 1) {
28
            throw new CannotUseRateLimiter('Memcached "OPT_BINARY_PROTOCOL" option should be set to "true".');
29
        }
30
31
        $this->memcached = $memcached;
32
        $this->keyPrefix = $keyPrefix;
33
    }
34
35
    public function limit(string $identifier, Rate $rate): void
36
    {
37
        $limitKey = $this->limitKey($identifier, $rate->getInterval());
38
39
        $current = $this->getCurrent($limitKey);
40
        if ($current >= $rate->getOperations()) {
41
            throw LimitExceeded::for($identifier, $rate);
42
        }
43
44
        $this->updateCounter($limitKey, $rate->getInterval());
45
    }
46
47
    public function limitSilently(string $identifier, Rate $rate): Status
48
    {
49
        $interval = $rate->getInterval();
50
        $limitKey = $this->limitKey($identifier, $interval);
51
        $timeKey = $this->timeKey($identifier, $interval);
52
53
        $current = $this->getCurrent($limitKey);
54
        if ($current <= $rate->getOperations()) {
55
            $current = $this->updateCounterAndTime($limitKey, $timeKey, $interval);
56
        }
57
58
        return Status::from(
59
            $identifier,
60
            $current,
61
            $rate->getOperations(),
62
            time() + max(0, $interval - $this->getElapsedTime($timeKey))
63
        );
64
    }
65
66
    private function limitKey(string $identifier, int $interval): string
67
    {
68
        return sprintf('%s%s:%d', $this->keyPrefix, $identifier, $interval);
69
    }
70
71
    private function timeKey(string $identifier, int $interval): string
72
    {
73
        return sprintf('%s%s:%d:time', $this->keyPrefix, $identifier, $interval);
74
    }
75
76
    private function getCurrent(string $limitKey): int
77
    {
78
        return (int) $this->memcached->get($limitKey);
79
    }
80
81
    private function updateCounterAndTime(string $limitKey, string $timeKey, int $interval): int
82
    {
83
        $current = $this->updateCounter($limitKey, $interval);
84
85
        if ($current === 1) {
86
            $this->memcached->add($timeKey, time(), $this->intervalToMemcachedTime($interval));
87
        }
88
89
        return $current;
90
    }
91
92
    private function updateCounter(string $limitKey, int $interval): int
93
    {
94
        $current = $this->memcached->increment($limitKey, 1, 1, $this->intervalToMemcachedTime($interval));
95
96
        return $current === false ? 1 : $current;
97
    }
98
99
    private function getElapsedTime(string $timeKey): int
100
    {
101
        return time() - (int) $this->memcached->get($timeKey);
102
    }
103
104
    /**
105
     * Interval to Memcached expiration time.
106
     *
107
     * @see https://www.php.net/manual/en/memcached.expiration.php
108
     *
109
     * @param int $interval
110
     * @return int
111
     */
112
    private function intervalToMemcachedTime(int $interval): int
113
    {
114
        return $interval <= self::MEMCACHED_SECONDS_LIMIT ? $interval : time() + $interval;
115
    }
116
}
117