Total Complexity | 62 |
Total Lines | 443 |
Duplicated Lines | 0 % |
Changes | 12 | ||
Bugs | 0 | Features | 0 |
Complex classes like limiter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use limiter, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
21 | class limiter |
||
22 | { |
||
23 | /** |
||
24 | * [$capacity Bucket volume] |
||
25 | * @var integer |
||
26 | */ |
||
27 | private $capacity = 100; |
||
28 | |||
29 | /** |
||
30 | * [$leakRate Constant rate at which the bucket will leak] |
||
31 | * @var string|integer |
||
32 | */ |
||
33 | private $leakRate = 1; |
||
34 | |||
35 | /** |
||
36 | * [$unpacked] |
||
37 | */ |
||
38 | private $unpacked; |
||
39 | |||
40 | /** |
||
41 | * [$packed] |
||
42 | * @var string |
||
43 | */ |
||
44 | private $packed; |
||
45 | |||
46 | /** |
||
47 | * [$bucket] |
||
48 | * @var object |
||
49 | */ |
||
50 | private $bucket; |
||
51 | |||
52 | /** |
||
53 | * [$tokenPacker Token packer class object] |
||
54 | * @var object |
||
55 | */ |
||
56 | private $tokenPacker; |
||
57 | |||
58 | /** |
||
59 | * [$account User account object] |
||
60 | */ |
||
61 | private $account; |
||
62 | |||
63 | /** |
||
64 | * [$options Responisble options] |
||
65 | */ |
||
66 | private $options; |
||
67 | |||
68 | /** |
||
69 | * [$timeframe Durations are in seconds] |
||
70 | * @var array |
||
71 | */ |
||
72 | private static $timeframe = [ |
||
73 | 'SECOND' => 1, |
||
74 | 'MINUTE' => 60, |
||
75 | 'HOUR' => 3600, |
||
76 | 'DAY' => 86400, |
||
77 | 'CUSTOM' => 0, |
||
78 | ]; |
||
79 | |||
80 | /** |
||
81 | * [$window Timeframe window] |
||
82 | * @var integer|string |
||
83 | */ |
||
84 | private $window = 'MINUTE'; |
||
85 | |||
86 | /** |
||
87 | * [$unlimited Rate limiter bypass if true] |
||
88 | * @var boolean |
||
89 | */ |
||
90 | private $unlimited = false; |
||
91 | |||
92 | /** |
||
93 | * [$scope Set the default scope] |
||
94 | * @var string |
||
95 | */ |
||
96 | private $scope = 'private'; |
||
97 | |||
98 | public function __construct($limit = null, $rate = null) |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * [setupOptions Set any Responsible API options] |
||
114 | * @return self |
||
115 | */ |
||
116 | public function setupOptions() |
||
135 | } |
||
136 | |||
137 | /** |
||
138 | * [throttleRequest Build the Responsible API throttle] |
||
139 | * @return boolean|void |
||
140 | */ |
||
141 | public function throttleRequest() |
||
142 | { |
||
143 | if ($this->isUnlimited() || $this->scope !== 'private') { |
||
144 | return true; |
||
145 | } |
||
146 | |||
147 | $account = $this->getAccount(); |
||
148 | $bucket = $this->bucketObj(); |
||
149 | |||
150 | $this->unpackBucket(); |
||
151 | |||
152 | if ($bucket->capacity()) { |
||
153 | $bucket->pause(false); |
||
154 | $bucket->fill(); |
||
155 | } else { |
||
156 | if ($this->getLeakRate() <= 0) { |
||
157 | if ($this->unpacked['pauseAccess'] == false) { |
||
158 | $bucket->pause(true); |
||
159 | $this->save(); |
||
160 | } |
||
161 | |||
162 | if ($bucket->refill($account->access)) { |
||
163 | $this->save(); |
||
164 | } |
||
165 | } |
||
166 | |||
167 | (new exception\errorException)->error('TOO_MANY_REQUESTS'); |
||
168 | } |
||
169 | |||
170 | $this->save(); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * Unpack the account bucket data |
||
175 | */ |
||
176 | private function unpackBucket() |
||
177 | { |
||
178 | $account = $this->getAccount(); |
||
179 | $bucket = $this->bucketObj(); |
||
180 | $packer = $this->packerObj(); |
||
181 | |||
182 | $this->unpacked = $packer->unpack( |
||
183 | $account->bucket |
||
184 | ); |
||
185 | if (empty($this->unpacked)) { |
||
186 | $this->unpacked = array( |
||
187 | 'drops' => 1, |
||
188 | 'time' => $account->access, |
||
189 | ); |
||
190 | } |
||
191 | |||
192 | $bucket->setTimeframe($this->getTimeframe()) |
||
193 | ->setCapacity($this->getCapacity()) |
||
194 | ->setLeakRate($this->getLeakRate()) |
||
195 | ->pour($this->unpacked['drops'], $this->unpacked['time']) |
||
196 | ; |
||
197 | } |
||
198 | |||
199 | /** |
||
200 | * [updateBucket Store the buckets token data and user access time] |
||
201 | * @return void |
||
202 | */ |
||
203 | private function save() |
||
204 | { |
||
205 | $packer = $this->packerObj(); |
||
206 | |||
207 | $this->packed = $packer->pack( |
||
208 | $this->bucket->getTokenData() |
||
209 | ); |
||
210 | |||
211 | /** |
||
212 | * [Update account access] |
||
213 | */ |
||
214 | (new user\user) |
||
215 | ->setAccountID($this->getAccount()->account_id) |
||
216 | ->setBucketToken($this->packed) |
||
217 | ->updateAccountAccess() |
||
218 | ; |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * [getThrottle Return a list of the throttled results] |
||
223 | * @return array |
||
224 | */ |
||
225 | public function getThrottle() |
||
226 | { |
||
227 | if ($this->isUnlimited() || $this->scope !== 'private') { |
||
228 | return array( |
||
229 | 'unlimited' => true, |
||
230 | ); |
||
231 | } |
||
232 | |||
233 | $windowFrame = (is_string($this->getTimeframe())) |
||
234 | ? $this->getTimeframe() |
||
235 | : $this->getTimeframe() . 'secs' |
||
236 | ; |
||
237 | |||
238 | if (is_null($this->bucket)) { |
||
239 | return; |
||
240 | } |
||
241 | |||
242 | return array( |
||
243 | 'limit' => $this->getCapacity(), |
||
244 | 'leakRate' => $this->getLeakRate(), |
||
245 | 'leak' => $this->bucket->getLeakage(), |
||
246 | 'lastAccess' => $this->getLastAccessDate(), |
||
247 | 'description' => $this->getCapacity() . ' requests per ' . $windowFrame, |
||
248 | 'bucket' => $this->bucket->getTokenData(), |
||
249 | ); |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * [getLastAccessDate Get the last recorded access in date format] |
||
254 | * @return string |
||
255 | */ |
||
256 | private function getLastAccessDate() |
||
257 | { |
||
258 | if (isset($this->bucket->getTokenData()['time'])) { |
||
259 | return date('m/d/y h:i:sa', $this->bucket->getTokenData()['time']); |
||
260 | } |
||
261 | |||
262 | return 'Can\'t be converted'; |
||
263 | } |
||
264 | |||
265 | /** |
||
266 | * [setAccount Set the requests account] |
||
267 | * @return self |
||
268 | */ |
||
269 | public function setAccount($account) |
||
270 | { |
||
271 | $this->account = $account; |
||
272 | return $this; |
||
273 | } |
||
274 | |||
275 | /** |
||
276 | * [getAccount Get the requests account] |
||
277 | */ |
||
278 | public function getAccount() |
||
279 | { |
||
280 | if (is_null($this->account)||empty($this->account)) { |
||
281 | (new exception\errorException) |
||
282 | ->setOptions($this->getOptions()) |
||
283 | ->error('UNAUTHORIZED'); |
||
284 | return; |
||
285 | } |
||
286 | |||
287 | return $this->account; |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * [bucketObj Get the bucket class object] |
||
292 | * @return object |
||
293 | */ |
||
294 | private function bucketObj() |
||
295 | { |
||
296 | return $this->bucket; |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * [packerObj Get the token packer class object] |
||
301 | * @return object |
||
302 | */ |
||
303 | private function packerObj() |
||
304 | { |
||
305 | return $this->tokenPacker; |
||
306 | } |
||
307 | |||
308 | /** |
||
309 | * [options Responsible API options] |
||
310 | * @param array $options |
||
311 | */ |
||
312 | public function options($options) |
||
313 | { |
||
314 | $this->options = $options; |
||
315 | return $this; |
||
316 | } |
||
317 | |||
318 | /** |
||
319 | * [getOptions Get the stored Responsible API options] |
||
320 | * @return array |
||
321 | */ |
||
322 | private function getOptions() |
||
323 | { |
||
324 | return $this->options; |
||
325 | } |
||
326 | |||
327 | /** |
||
328 | * [hasOptionProperty Check if an option property is set] |
||
329 | * @param array $options |
||
330 | * @param string $property |
||
331 | * @return string|integer|boolean |
||
332 | */ |
||
333 | private function hasOptionProperty(array $options, $property, $default = false) |
||
334 | { |
||
335 | $val = isset($options[$property]) ? $options[$property] : $default; |
||
336 | |||
337 | if ($val && empty($options[$property])) { |
||
338 | $val = $default; |
||
339 | } |
||
340 | |||
341 | return $val; |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * [setCapacity Set the buckets capacity] |
||
346 | * @param array $options |
||
347 | */ |
||
348 | public function setCapacity($options) |
||
349 | { |
||
350 | $hasCapacityOption = $this->hasOptionProperty($options, 'rateLimit'); |
||
351 | |||
352 | if ($hasCapacityOption) { |
||
353 | if (!is_integer($hasCapacityOption) || empty($hasCapacityOption)) { |
||
354 | $hasCapacityOption = false; |
||
355 | } |
||
356 | } |
||
357 | |||
358 | $this->capacity = ($hasCapacityOption) ? $hasCapacityOption : $this->capacity; |
||
|
|||
359 | } |
||
360 | |||
361 | /** |
||
362 | * [getCapacity Get the buckets capacity] |
||
363 | * @return integer |
||
364 | */ |
||
365 | public function getCapacity() |
||
366 | { |
||
367 | return $this->capacity; |
||
368 | } |
||
369 | |||
370 | /** |
||
371 | * [setTimeframe Set the window timeframe] |
||
372 | * @param array $options |
||
373 | */ |
||
374 | public function setTimeframe($options) |
||
375 | { |
||
376 | $timeframe = $this->hasOptionProperty($options, 'rateWindow'); |
||
377 | |||
378 | if (!is_string($timeframe) && !is_numeric($timeframe)) { |
||
379 | $timeframe = $this->window; |
||
380 | } |
||
381 | |||
382 | if (!$timeframe) { |
||
383 | $timeframe = $this->window; |
||
384 | } |
||
385 | |||
386 | if (is_numeric($timeframe)) { |
||
387 | self::$timeframe['CUSTOM'] = $timeframe; |
||
388 | $this->window = self::$timeframe['CUSTOM']; |
||
389 | return; |
||
390 | } |
||
391 | |||
392 | if (isset(self::$timeframe[$timeframe])) { |
||
393 | $this->window = self::$timeframe[$timeframe]; |
||
394 | return; |
||
395 | } |
||
396 | |||
397 | $this->window = self::$timeframe['MINUTE']; |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * [getTimeframe Get the timeframe window] |
||
402 | * @return integer|string |
||
403 | */ |
||
404 | public function getTimeframe() |
||
405 | { |
||
406 | return $this->window; |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * [setLeakRate Set the buckets leak rate] |
||
411 | * Options: slow, medium, normal, default, fast or custom positive integer |
||
412 | * @param array $options |
||
413 | */ |
||
414 | private function setLeakRate($options) |
||
415 | { |
||
416 | if (isset($options['leak']) && !$options['leak']) { |
||
417 | $options['leakRate'] = 'default'; |
||
418 | } |
||
419 | |||
420 | $leakRate = $this->hasOptionProperty($options, 'leakRate'); |
||
421 | |||
422 | if (empty($leakRate) || !is_string($leakRate)) { |
||
423 | $leakRate = 'default'; |
||
424 | } |
||
425 | |||
426 | $this->leakRate = $leakRate; |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * [getLeakRate Get the buckets leak rate] |
||
431 | * @return string|integer |
||
432 | */ |
||
433 | private function getLeakRate() |
||
434 | { |
||
435 | return $this->leakRate; |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * [setUnlimited Rate limiter bypass] |
||
440 | * @param array $options |
||
441 | */ |
||
442 | private function setUnlimited($options) |
||
443 | { |
||
444 | $unlimited = false; |
||
445 | |||
446 | if (isset($options['unlimited']) && ($options['unlimited'] == 1 || $options['unlimited'] == true)) { |
||
447 | $unlimited = true; |
||
448 | } |
||
449 | |||
450 | if (isset($options['requestType']) && $options['requestType'] === 'debug') { |
||
451 | $unlimited = true; |
||
452 | } |
||
453 | |||
454 | $this->unlimited = $unlimited; |
||
455 | } |
||
456 | |||
457 | /** |
||
458 | * [isUnlimited Check if the Responsible API is set to unlimited] |
||
459 | * @return boolean |
||
460 | */ |
||
461 | private function isUnlimited() |
||
464 | } |
||
465 | } |
||
466 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.