Completed
Pull Request — master (#49)
by
unknown
09:13
created

Psr16RateLimiter   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 108
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 12
lcom 1
cbo 3
dl 0
loc 108
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A limit() 0 12 2
A limitSilently() 0 13 1
A key() 0 4 1
A getCurrentCount() 0 6 1
A getCurrentStoredCounter() 0 16 5
A updateCounter() 0 18 1
1
<?php
2
/**
3
 * Implementation of nikolaposa/rate-limit to allow using a PSR-16 cache for storage.
4
 *
5
 * @see https://www.php-fig.org/psr/psr-16/
6
 *
7
 * @license    MIT
8
 * @author     BrianHenryIE <[email protected]>
9
 */
10
11
declare(strict_types=1);
12
13
namespace RateLimit;
14
15
use Psr\SimpleCache\CacheInterface;
16
use Psr\SimpleCache\InvalidArgumentException;
17
use RateLimit\Exception\LimitExceeded;
18
use function time;
19
20
class Psr16RateLimiter implements RateLimiter, SilentRateLimiter
21
{
22
23
    /** @var CacheInterface */
24
    protected $psrCache;
25
26
    /** @var string */
27
    protected $keyPrefix;
28
29
    public function __construct(CacheInterface $psrCache, string $keyPrefix = '')
30
    {
31
        $this->keyPrefix = $keyPrefix;
32
        $this->psrCache  = $psrCache;
33
    }
34
35
    public function limit(string $identifier, Rate $rate): void
36
    {
37
        $key = $this->key($identifier, $rate->getInterval());
38
39
        $current = $this->getCurrentCount($key);
40
41
        if ($current >= $rate->getOperations()) {
42
            throw LimitExceeded::for($identifier, $rate);
43
        }
44
45
        $this->updateCounter($key, $rate->getInterval());
46
    }
47
48
    public function limitSilently(string $identifier, Rate $rate): Status
49
    {
50
        $key = $this->key($identifier, $rate->getInterval());
51
52
        $current = $this->updateCounter($key, $rate->getInterval());
53
54
        return Status::from(
55
            $identifier,
56
            $current,
57
            $rate->getOperations(),
58
            time() + $rate->getInterval()
59
        );
60
    }
61
62
    /**
63
     * The key includes the interval so multiple intervals:incidents can be counted against the one identifier.
64
     * e.g. it can happen five times in one minute but no more then ten times in one hour.
65
     *
66
     * @param string $identifier
67
     * @param int    $interval
68
     * @return string
69
     */
70
    protected function key(string $identifier, int $interval): string
71
    {
72
        return "{$this->keyPrefix}{$identifier}:$interval";
73
    }
74
75
    /**
76
     * Return a count of unexpired records for the key.
77
     *
78
     * @param string $key
79
     * @return int
80
     */
81
    protected function getCurrentCount(string $key): int
82
    {
83
        $stored_values = $this->getCurrentStoredCounter($key);
84
85
        return count($stored_values);
86
    }
87
88
    /**
89
     * @param string $key
90
     * @return array<int, array{key: string, created_at: int, expires_at: int, interval :int}>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
91
     */
92
    protected function getCurrentStoredCounter(string $key): array
93
    {
94
        try {
95
            $stored_values = $this->psrCache->get($key, []);
96
        } catch (InvalidArgumentException $e) {
97
            $stored_values = [];
98
        }
99
100
        foreach ($stored_values as $created_time => $value) {
101
            if (isset($value['expires_at']) && $value['expires_at'] < time()) {
102
                unset($stored_values[ $created_time ]);
103
            }
104
        }
105
106
        return $stored_values;
107
    }
108
109
    protected function updateCounter(string $key, int $interval): int
110
    {
111
        $stored_values = $this->getCurrentStoredCounter($key);
112
113
        $created_time = time();
114
        $expires_at   = $created_time + $interval;
115
116
        $stored_values[ $created_time ] = [
117
            'key'          => $key,
118
            'created_time' => $created_time,
119
            'expires_at'   => $expires_at,
120
            'interval'     => $interval,
121
        ];
122
123
        $this->psrCache->set($key, $stored_values, $interval);
124
125
        return count($stored_values);
126
    }
127
}
128