Completed
Pull Request — master (#3)
by Nikola
01:37
created

RequestsPerWindowRateLimiter::__invoke()   B

Complexity

Conditions 4
Paths 7

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 12
cts 12
cp 1
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 13
nc 7
nop 3
crap 4
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\IdentityResolverInterface;
19
use RateLimit\Options\RequestsPerWindowOptions;
20
use RateLimit\Exception\StorageValueNotFoundException;
21
22
/**
23
 * @author Nikola Posa <[email protected]>
24
 */
25
final class RequestsPerWindowRateLimiter extends AbstractRateLimiter
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 RequestsPerWindowOptions
35
     */
36
    private $options;
37
38
    /**
39
     * @var string
40
     */
41
    private $identity;
42
43 10
    public function __construct(StorageInterface $storage, IdentityResolverInterface $identityResolver, RequestsPerWindowOptions $options)
44
    {
45 10
        parent::__construct($storage, $identityResolver);
46
        
47 10
        $this->options = $options;
48 10
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53 7
    public function __invoke(RequestInterface $request, ResponseInterface $response, callable $out = null)
54
    {
55 7
        $whitelist = $this->options->getWhitelist();
56
57 7
        if ($whitelist($request)) {
58 1
            return $this->next($request, $response, $out);
59
        }
60
61 6
        $this->resolveIdentity($request);
62
63
        try {
64 6
            $current = $this->getCurrent();
65
66 4
            if ($this->isLimitExceeded($current)) {
67 3
                return $this->onLimitExceeded($request, $response);
68
            }
69
70 1
            $this->hit();
71 6
        } catch (StorageValueNotFoundException $ex) {
72 6
            $this->initialHit();
73
        }
74
75 6
        return $this->onBelowLimit($request, $response, $out);
76
    }
77
78 6
    private function resolveIdentity(RequestInterface $request)
79
    {
80 6
        $this->identity = $this->identityResolver->getIdentity($request);
81 6
    }
82
83 6
    private function getCurrent() : int
84
    {
85 6
        return $this->storage->get($this->identity);
86
    }
87
88 4
    private function isLimitExceeded($current) : bool
89
    {
90 4
        return ($current >= $this->options->getLimit());
91
    }
92
93 6
    private function initialHit()
94
    {
95 6
        $this->storage->set($this->identity, 1, $this->options->getWindow());
96 6
    }
97
98 1
    private function hit()
99
    {
100 1
        $this->storage->increment($this->identity, 1);
101 1
    }
102
103 3
    private function onLimitExceeded(RequestInterface $request, ResponseInterface $response) : ResponseInterface
104
    {
105
        $response = $this
106 3
            ->setRateLimitHeaders($response)
107 3
            ->withStatus(self::LIMIT_EXCEEDED_HTTP_STATUS_CODE)
108
        ;
109
110 3
        $limitExceededHandler = $this->options->getLimitExceededHandler();
111 3
        $response = $limitExceededHandler($request, $response);
112
113 3
        return $response;
114
    }
115
116 6
    private function onBelowLimit(RequestInterface $request, ResponseInterface $response, callable $out = null) : ResponseInterface
117
    {
118 6
        $response = $this->setRateLimitHeaders($response);
119
120 6
        return $this->next($request, $response, $out);
121
    }
122
123 7
    private function next(RequestInterface $request, ResponseInterface $response, callable $out = null)
124
    {
125 7
        return $out ? $out($request, $response) : $response;
126
    }
127
128 6
    private function setRateLimitHeaders(ResponseInterface $response) : ResponseInterface
129
    {
130
        return $response
131 6
            ->withHeader(self::HEADER_LIMIT, (string) $this->options->getLimit())
132 6
            ->withHeader(self::HEADER_REMAINING, (string) ($this->options->getLimit() - $this->getCurrent()))
133 6
            ->withHeader(self::HEADER_RESET, (string) (time() + $this->storage->ttl($this->identity)))
134
        ;
135
    }
136
}
137