Completed
Push — master ( 63e3c2...273cff )
by Nikola
03:54
created

RequestsPerWindowRateLimiter::isLimitExceeded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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\Options\RequestsPerWindowOptions;
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 IdentityGeneratorInterface
40
     */
41
    private $identityGenerator;
42
43
    /**
44
     * @var RequestsPerWindowOptions
45
     */
46
    private $options;
47
48
    /**
49
     * @var array
50
     */
51
    private $rateLimit;
52
53 6
    public function __construct(StorageInterface $storage, IdentityGeneratorInterface $identityGenerator, RequestsPerWindowOptions $options)
54
    {
55 6
        $this->storage = $storage;
56 6
        $this->identityGenerator = $identityGenerator;
57 6
        $this->options = $options;
58 6
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 6
    public function __invoke(RequestInterface $request, ResponseInterface $response, callable $out = null)
64
    {
65 6
        $key = $this->identityGenerator->getIdentity($request);
66
67 6
        $this->initRateLimit($key);
68
69 6
        if ($this->shouldResetRateLimit()) {
70 1
            $this->resetRateLimit();
71 6
        } elseif ($this->isLimitExceeded()) {
72 3
            return $this->onLimitExceeded($request, $response);
73
        } else {
74 6
            $this->updateRateLimit();
75
        }
76
77 6
        $this->storage->set($key, $this->rateLimit);
78
79 6
        return $this->onBelowLimit($request, $response, $out);
80
    }
81
82 6
    private function initRateLimit(string $key)
83
    {
84
        try {
85 6
            $rateLimit = $this->storage->get($key);
86 6
        } catch (StorageRecordNotExistException $ex) {
87
            $rateLimit = [
88 6
                'remaining' => $this->options->getLimit(),
89 6
                'reset' => time() + $this->options->getWindow(),
90
            ];
91
        }
92
93 6
        $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...
94 6
    }
95
96 6
    private function isLimitExceeded() : bool
97
    {
98 6
        return $this->rateLimit['remaining'] <= 0;
99
    }
100
101 6
    private function updateRateLimit()
102
    {
103 6
        $this->rateLimit['remaining']--;
104 6
    }
105
106 6
    private function shouldResetRateLimit() : bool
107
    {
108 6
        return time() >= $this->rateLimit['reset'];
109
    }
110
111 1
    private function resetRateLimit()
112
    {
113 1
        $this->rateLimit = [
114 1
            'remaining' => $this->options->getLimit(),
115 1
            'reset' => time() + $this->options->getWindow(),
116
        ];
117 1
    }
118
119 3
    private function onLimitExceeded(RequestInterface $request, ResponseInterface $response) : ResponseInterface
120
    {
121
        $response = $this
122 3
            ->setRateLimitHeaders($response)
123 3
            ->withStatus(self::LIMIT_EXCEEDED_HTTP_STATUS_CODE)
124
        ;
125
126 3
        $limitExceededHandler = $this->options->getLimitExceededHandler();
127 3
        $response = $limitExceededHandler($request, $response);
128
129 3
        return $response;
130
    }
131
132 6
    private function onBelowLimit(RequestInterface $request, ResponseInterface $response, callable $out = null) : ResponseInterface
133
    {
134 6
        $response = $this->setRateLimitHeaders($response);
135
136 6
        return $out ? $out($request, $response) : $response;
137
    }
138
139 6
    private function setRateLimitHeaders(ResponseInterface $response) : ResponseInterface
140
    {
141
        return $response
142 6
            ->withHeader(self::HEADER_LIMIT, (string) $this->options->getLimit())
143 6
            ->withHeader(self::HEADER_REMAINING, (string) $this->rateLimit['remaining'])
144 6
            ->withHeader(self::HEADER_RESET, (string) $this->rateLimit['reset'])
145
        ;
146
    }
147
}
148