Passed
Push — develop ( cc6e0d...eb2865 )
by nguereza
03:00
created

RateLimit   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 134
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 40
c 1
b 0
f 0
dl 0
loc 134
rs 10
wmc 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A limit() 0 35 4
A getTimeKey() 0 6 1
A getRemainingAttempts() 0 11 2
A getAllowKey() 0 6 1
A purge() 0 4 1
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant
7
 * PHP Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file RateLimit.php
34
 *
35
 *  The Rate Limit base class
36
 *
37
 *  @package    Platine\Framework\Http\RateLimit
38
 *  @author Platine Developers team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Framework\Http\RateLimit;
49
50
use Platine\Framework\Http\RateLimit\Exception\LimitExceededException;
51
52
53
/**
54
 * @class RateLimit
55
 * @package Platine\Framework\Http\RateLimit
56
 */
57
class RateLimit
58
{
59
    /**
60
     * The storage to used
61
     * @var RateLimitStorageInterface
62
     */
63
    protected RateLimitStorageInterface $storage;
64
65
    /**
66
     * The rate to use
67
     * @var Rate
68
     */
69
    protected Rate $rate;
70
71
    /**
72
     * prefix used in storage keys.
73
     * @var string
74
     */
75
    protected string $name;
76
77
78
    /**
79
     * Create new instance
80
     * @param RateLimitStorageInterface $storage
81
     * @param Rate $rate
82
     * @param string $name
83
     */
84
    public function __construct(
85
        RateLimitStorageInterface $storage,
86
        Rate $rate,
87
        string $name = 'api'
88
    ) {
89
        $this->storage = $storage;
90
        $this->rate = $rate;
91
        $this->name = $name;
92
    }
93
94
    /**
95
     * Rate Limiting
96
     * @param string $id
97
     * @param float $used
98
     * @return void
99
     */
100
    public function limit(string $id, float $used = 1.0): void
101
    {
102
        $rate = $this->rate->getQuota() / $this->rate->getInterval();
103
        $allowKey = $this->getAllowKey($id);
104
        $timeKey = $this->getTimeKey($id);
105
106
        if ($this->storage->exists($timeKey) === false) {
107
            // first hit; setup storage; allow.
108
            $this->storage->set($timeKey, time(), $this->rate->getInterval());
109
            $this->storage->set($allowKey, ($this->rate->getQuota() - $used), $this->rate->getInterval());
110
111
            return;
112
        }
113
114
        $currentTime = time();
115
        $timePassed = $currentTime - $this->storage->get($timeKey);
116
        $this->storage->set($timeKey, $currentTime, $this->rate->getInterval());
117
118
        $quota = $this->storage->get($allowKey);
119
        $quota += $timePassed * $rate;
120
121
122
123
        if ($quota > $this->rate->getQuota()) {
124
            $quota = $this->rate->getQuota(); // throttle
125
        }
126
127
        if ($quota < $used) {
128
            // need to wait for more 'tokens' to be in the bucket.
129
            $this->storage->set($allowKey, $quota, $this->rate->getInterval());
130
131
            throw LimitExceededException::create($id, $this->rate);
132
        }
133
134
        $this->storage->set($allowKey, $quota - $used, $this->rate->getInterval());
135
    }
136
137
    /**
138
     * Return the remaining quota to be used
139
     * @param string $id
140
     * @return int the number of operation that can be made before hitting a limit.
141
     */
142
    public function getRemainingAttempts(string $id): int
143
    {
144
        $this->limit($id, 0.0);
145
146
        $allowKey = $this->getAllowKey($id);
147
148
        if ($this->storage->exists($allowKey) === false) {
149
            return $this->rate->getQuota();
150
        }
151
152
        return (int) max(0, floor($this->storage->get($allowKey)));
153
    }
154
155
    /**
156
     * Purge rate limit record for the given key
157
     * @param string $id
158
     * @return void
159
     */
160
    public function purge(string $id): void
161
    {
162
        $this->storage->delete($this->getAllowKey($id));
163
        $this->storage->delete($this->getTimeKey($id));
164
    }
165
166
    /**
167
     * Return the storage key used for time of the given identifier
168
     * @param string $id
169
     * @return string
170
     */
171
    protected function getTimeKey(string $id): string
172
    {
173
        return sprintf(
174
            '%s:%s:time',
175
            $this->name,
176
            $id
177
        );
178
    }
179
180
    /**
181
     * Return the storage key used for allow of the given identifier
182
     * @param string $id
183
     * @return string
184
     */
185
    protected function getAllowKey(string $id): string
186
    {
187
        return sprintf(
188
            '%s:%s:allow',
189
            $this->name,
190
            $id
191
        );
192
    }
193
}
194