Completed
Push — master ( cb5363...63e3c2 )
by Nikola
02:41
created

RequestsPerWindowRateLimiter::initRateLimit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 13
c 0
b 0
f 0
ccs 7
cts 7
cp 1
rs 9.4285
cc 2
eloc 8
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * This file is part of the Rate Limit package.
4
 *
5
 * Copyright (c) Nikola Posa
6
 *
7
 * For full copyright and license information, please refer to the LICENSE file,
8
 * located at the package root folder.
9
 */
10
11
declare(strict_types=1);
12
13
namespace RateLimit;
14
15
use Psr\Http\Message\RequestInterface;
16
use Psr\Http\Message\ResponseInterface;
17
use RateLimit\Storage\StorageInterface;
18
use RateLimit\Identity\IdentityGeneratorInterface;
19
use RateLimit\Identity\IpAddressIdentityGenerator;
20
use RateLimit\Exception\StorageRecordNotExistException;
21
22
/**
23
 * @author Nikola Posa <[email protected]>
24
 */
25
final class RequestsPerWindowRateLimiter implements RateLimiterInterface
26
{
27
    const LIMIT_EXCEEDED_HTTP_STATUS_CODE = 429; //HTTP 429 "Too Many Requests" (RFC 6585)
28
29
    const HEADER_LIMIT = 'X-RateLimit-Limit';
30
    const HEADER_REMAINING = 'X-RateLimit-Remaining';
31
    const HEADER_RESET = 'X-RateLimit-Reset';
32
33
    /**
34
     * @var StorageInterface
35
     */
36
    private $storage;
37
38
    /**
39
     * @var array
40
     */
41
    private $options;
42
43
    /**
44
     * @var IdentityGeneratorInterface
45
     */
46
    private $identityGenerator;
47
48
    /**
49
     * @var array
50
     */
51
    private $rateLimit;
52
53
    /**
54
     * @var array
55
     */
56
    private static $defaultOptions = [
57
        'limit' => 100,
58
        'window' => 900, //15 minutes
59
        'limitExceededHandler' => null,
60
    ];
61
62 2
    private function __construct(StorageInterface $storage, array $options, IdentityGeneratorInterface $identityGenerator)
63
    {
64 2
        $this->storage = $storage;
65 2
        $this->options = $options;
66 2
        $this->identityGenerator = $identityGenerator;
67 2
    }
68
69 2
    public static function create(StorageInterface $storage, array $options = [], IdentityGeneratorInterface $identityGenerator = null)
70
    {
71 2
        return new self(
72
            $storage,
73 2
            array_merge(self::$defaultOptions, $options),
74 2
            $identityGenerator ?? new IpAddressIdentityGenerator()
75
        );
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 2
    public function __invoke(RequestInterface $request, ResponseInterface $response, callable $out = null)
82
    {
83 2
        $key = $this->identityGenerator->getIdentity($request);
84
85 2
        $this->initRateLimit($key);
86
87 2
        if ($this->isLimitExceeded()) {
88 2
            return $this->onLimitExceeded($request, $response);
89
        }
90
91 2
        if ($this->shouldResetRateLimit()) {
92
            $this->resetRateLimit();
93
        } else {
94 2
            $this->updateRateLimit();
95
        }
96
97 2
        $this->storage->set($key, $this->rateLimit);
98
99 2
        return $this->onBelowLimit($request, $response, $out);
100
    }
101
102 2
    private function initRateLimit(string $key)
103
    {
104
        try {
105 2
            $rateLimit = $this->storage->get($key);
106 2
        } catch (StorageRecordNotExistException $ex) {
107
            $rateLimit = [
108 2
                'remaining' => $this->options['limit'],
109 2
                'reset' => time() + $this->options['window'],
110
            ];
111
        }
112
113 2
        $this->rateLimit = $rateLimit;
0 ignored issues
show
Documentation Bug introduced by
It seems like $rateLimit of type * is incompatible with the declared type array of property $rateLimit.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
114 2
    }
115
116 2
    private function isLimitExceeded() : bool
117
    {
118 2
        return $this->rateLimit['remaining'] <= 0;
119
    }
120
121 2
    private function updateRateLimit()
122
    {
123 2
        $this->rateLimit['remaining']--;
124 2
    }
125
126 2
    private function shouldResetRateLimit() : bool
127
    {
128 2
        return time() >= $this->rateLimit['reset'];
129
    }
130
131
    private function resetRateLimit()
132
    {
133
        $this->rateLimit = [
134
            'remaining' => $this->options['limit'],
135
            'reset' => time() + $this->options['window'],
136
        ];
137
    }
138
139 2
    private function onLimitExceeded(RequestInterface $request, ResponseInterface $response) : ResponseInterface
140
    {
141
        $response = $this
142 2
            ->setRateLimitHeaders($response)
143 2
            ->withStatus(self::LIMIT_EXCEEDED_HTTP_STATUS_CODE)
144
        ;
145
146 2
        $limitExceededHandler = $this->options['limitExceededHandler'];
147
148 2
        if (null !== $limitExceededHandler && is_callable($limitExceededHandler)) {
149
            $response = $limitExceededHandler($request, $response);
150
        }
151
152 2
        return $response;
153
    }
154
155 2
    private function onBelowLimit(RequestInterface $request, ResponseInterface $response, callable $out = null) : ResponseInterface
156
    {
157 2
        $response = $this->setRateLimitHeaders($response);
158
159 2
        return $out ? $out($request, $response) : $response;
160
    }
161
162 2
    private function setRateLimitHeaders(ResponseInterface $response) : ResponseInterface
163
    {
164
        return $response
165 2
            ->withHeader(self::HEADER_LIMIT, (string) $this->options['limit'])
166 2
            ->withHeader(self::HEADER_REMAINING, (string) $this->rateLimit['remaining'])
167 2
            ->withHeader(self::HEADER_RESET, (string) $this->rateLimit['reset'])
168
        ;
169
    }
170
}
171