RateLimiter::__invoke()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 1
nop 1
dl 0
loc 20
ccs 9
cts 9
cp 1
crap 2
rs 9.6
c 0
b 0
f 0
1
<?php
2
3
namespace Concat\Http\Middleware;
4
5
use Psr\Http\Message\ResponseInterface;
6
use Psr\Http\Message\RequestInterface;
7
use Psr\Log\LoggerInterface;
8
use Psr\Log\LogLevel;
9
10
/**
11
 * Guzzle middleware which delays requests if they exceed a rate allowance.
12
 */
13
class RateLimiter
14
{
15
    /**
16
     * @var RateLimitProvider
17
     */
18
    protected $provider;
19
20
    /**
21
     * @var LoggerInterface
22
     */
23
    protected $logger;
24
25
    /**
26
     * @var string|callable Constant or callable that accepts a Response.
27
     */
28
    protected $logLevel;
29
30
    /**
31
     * Creates a callable middleware rate limiter.
32
     *
33
     * @param RateLimitProvider $provider A rate data provider.
34
     * @param LoggerInterface   $logger
35
     */
36 7
    public function __construct(
37
        RateLimitProvider $provider,
38
        LoggerInterface $logger = null
39
    ) {
40 7
        $this->provider = $provider;
41 7
        $this->logger = $logger;
42 7
    }
43
44
    /**
45
     * Delays and logs the request then sets the allowance for the next request.
46
     *
47
     * @param callable $handler
48
     * @return \Closure
49
     */
50 3
    public function __invoke(callable $handler)
51
    {
52
        return function (RequestInterface $request, $options) use ($handler) {
53
54
            // Amount of time to delay the request by
55 3
            $delay = $this->getDelay($request);
56
57 3
            if ($delay > 0) {
58 3
                $this->delay($delay);
59 3
                $this->log($request, $delay);
60 3
            }
61
62
            // Sets the time when this request is being made,
63
            // which allows calculation of allowance later on.
64 3
            $this->provider->setLastRequestTime($request);
65
66
            // Set the allowance when the response was received
67 3
            return $handler($request, $options)->then($this->setAllowance());
68 3
        };
69
    }
70
71
    /**
72
     * Logs a request which is being delayed by a specified amount of time.
73
     *
74
     * @param RequestInterface $request The request being delayed.
75
     * @param float            $delay The amount of time that the request is delayed for.
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 80 characters; contains 89 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
76
     */
77 3
    protected function log(RequestInterface $request, $delay)
78
    {
79 3
        if (isset($this->logger)) {
80 3
            $level   = $this->getLogLevel($request);
81 3
            $message = $this->getLogMessage($request, $delay);
82 3
            $context = compact('request', 'delay');
83
84 3
            $this->logger->log($level, $message, $context);
85 3
        }
86 3
    }
87
88
    /**
89
     * Formats a request and delay time as a log message.
90
     *
91
     * @param RequestInterface $request The request being logged.
92
     * @param float $delay The amount of time that the request is delayed for.
93
     *
94
     * @return string Log message
95
     */
96 3
    protected function getLogMessage(RequestInterface $request, $delay)
97
    {
98 3
        return vsprintf("[%s] %s %s was delayed by {$delay} seconds", [
99 3
            gmdate("d/M/Y:H:i:s O"),
100 3
            $request->getMethod(),
101 3
            $request->getUri()
102 3
        ]);
103
    }
104
105
    /**
106
     * Returns the default log level.
107
     *
108
     * @return string LogLevel
109
     */
110 1
    protected function getDefaultLogLevel()
111
    {
112 1
        return LogLevel::DEBUG;
113
    }
114
115
    /**
116
     * Sets the log level to use, which can be either a string or a callable
117
     * that accepts a response (which could be null). A log level could also
118
     * be null, which indicates that the default log level should be used.
119
     *
120
     * @param string|callable|null
121
     */
122 2
    public function setLogLevel($logLevel)
123
    {
124 2
        $this->logLevel = $logLevel;
125 2
    }
126
127
    /**
128
     * Returns a log level for a given request.
129
     *
130
     * @param RequestInterface $request The request being logged.
131
     * @return string LogLevel
132
     */
133 3
    protected function getLogLevel(RequestInterface $request)
134
    {
135 3
        if (!$this->logLevel) {
136 1
            return $this->getDefaultLogLevel();
137
        }
138
139 2
        if (is_callable($this->logLevel)) {
140 1
            return call_user_func($this->logLevel, $request);
141
        }
142
143 1
        return (string) $this->logLevel;
144
    }
145
146
    /**
147
     * Returns the delay duration for the given request (in seconds).
148
     *
149
     * @param RequestInterface $request Request to get the delay duration for.
150
     *
151
     * @return float The delay duration (in seconds).
152
     */
153 4
    protected function getDelay(RequestInterface $request)
154
    {
155 4
        $lastRequestTime  = $this->provider->getLastRequestTime($request);
156 4
        $requestAllowance = $this->provider->getRequestAllowance($request);
157 4
        $requestTime      = $this->provider->getRequestTime($request);
158
159
        // If lastRequestTime is null or false, the max will be 0.
160 4
        return max(0, $requestAllowance - ($requestTime - $lastRequestTime));
161
    }
162
163
    /**
164
     * Delays the given request by an amount of seconds. This method supports microsecond
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 80 characters; contains 89 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
165
     * precision as well as integer seconds, ie. both microtime(true) or time()
166
     *
167
     * @param float $seconds The amount of time (in seconds) to delay by.
168
     *
169
     * @codeCoverageIgnore
170
     */
171
    protected function delay($seconds)
172
    {
173
        $floor = floor($seconds);
174
        $micro = max(0, floor(($seconds - $floor) * 1000000));
175
176
        sleep($floor);
177
        usleep($micro);
178
    }
179
180
    /**
181
     * Returns a callable handler which allows the provider to set the request
182
     * allowance for the next request, using the current response.
183
     *
184
     * @return \Closure Handler to set request allowance on the rate provider.
185
     */
186
    protected function setAllowance()
187
    {
188 3
        return function (ResponseInterface $response) {
189 3
            $this->provider->setRequestAllowance($response);
190 3
            return $response;
191 3
        };
192
    }
193
}
194