Passed
Push — master ( 372102...07d96b )
by Vince
01:39
created

limiter   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Importance

Changes 14
Bugs 0 Features 0
Metric Value
eloc 145
c 14
b 0
f 0
dl 0
loc 461
rs 3.36
wmc 63

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
A setupOptions() 0 19 4
A isUnlimited() 0 3 1
A getTimeframe() 0 3 1
A getCapacity() 0 3 1
A packerObj() 0 3 1
A getAccount() 0 10 3
A options() 0 4 1
A setCapacity() 0 11 5
A getOptions() 0 3 1
A setTimeframe() 0 24 6
A bucketObj() 0 3 1
A getThrottle() 0 26 5
A setUnlimited() 0 13 6
A setAccount() 0 4 1
A hasOptionProperty() 0 9 4
A unpackBucket() 0 20 2
A save() 0 16 1
A getLeakRate() 0 3 1
A throttlePause() 0 17 4
A setLeakRate() 0 13 5
A throttleRequest() 0 20 4
A getLastAccessDate() 0 9 2

How to fix   Complexity   

Complex Class

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
2
/**
3
 * ==================================
4
 * Responsible PHP API
5
 * ==================================
6
 *
7
 * @link Git https://github.com/vince-scarpa/responsibleAPI.git
8
 *
9
 * @api Responible API
10
 * @package responsible\core\throttle
11
 *
12
 * @author Vince scarpa <[email protected]>
13
 *
14
 */
15
namespace responsible\core\throttle;
16
17
use responsible\core\exception;
18
use responsible\core\throttle;
19
use responsible\core\user;
20
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)
99
    {
100
        if (!is_null($limit)) {
101
            $this->capacity = $limit;
102
        }
103
104
        if (!is_null($rate)) {
105
            $this->window = $rate;
106
        }
107
108
        $this->bucket = new throttle\tokenBucket;
109
        $this->tokenPacker = new throttle\tokenPack;
110
    }
111
112
    /**
113
     * [setupOptions Set any Responsible API options]
114
     * @return self
115
     */
116
    public function setupOptions()
117
    {
118
        $options = $this->getOptions();
119
120
        $this->setCapacity($options);
121
122
        $this->setTimeframe($options);
123
124
        $this->setLeakRate($options);
125
126
        $this->setUnlimited($options);
127
128
        if (isset($this->account->scope) &&
129
            ($this->account->scope == 'anonymous' || $this->account->scope == 'public')
130
        ) {
131
            $this->scope = $this->account->scope;
132
        }
133
134
        return $this;
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();
0 ignored issues
show
Unused Code introduced by
The assignment to $account is dead and can be removed.
Loading history...
148
        $bucket = $this->bucketObj();
149
150
        $this->unpackBucket();
151
        
152
        if ($bucket->capacity()) {
153
            $bucket->pause(false);
154
            $bucket->fill();
155
156
        } else {
157
            $this->throttlePause();
158
        }
159
160
        $this->save();
161
    }
162
163
    /**
164
     * [throttlePause Throttle the limiter when there are too many requests]
165
     * @return void
166
     */
167
    private function throttlePause()
168
    {
169
        $account = $this->getAccount();
170
        $bucket = $this->bucketObj();
171
172
        if ($this->getLeakRate() <= 0) {
173
            if ($this->unpacked['pauseAccess'] == false) {
174
                $bucket->pause(true);
175
                $this->save();
176
            }
177
178
            if ($bucket->refill($account->access)) {
179
                $this->save();
180
            }
181
        }
182
183
        (new exception\errorException)->error('TOO_MANY_REQUESTS');
184
    }
185
186
    /**
187
     * Unpack the account bucket data
188
     */
189
    private function unpackBucket()
190
    {
191
        $account = $this->getAccount();
192
        $bucket = $this->bucketObj();
193
        $packer = $this->packerObj();
194
195
        $this->unpacked = $packer->unpack(
196
            $account->bucket
197
        );
198
        if (empty($this->unpacked)) {
199
            $this->unpacked = array(
200
                'drops' => 1,
201
                'time' => $account->access,
202
            );
203
        }
204
205
        $bucket->setTimeframe($this->getTimeframe())
206
            ->setCapacity($this->getCapacity())
207
            ->setLeakRate($this->getLeakRate())
208
            ->pour($this->unpacked['drops'], $this->unpacked['time'])
209
        ;
210
    }
211
212
    /**
213
     * [updateBucket Store the buckets token data and user access time]
214
     * @return void
215
     */
216
    private function save()
217
    {
218
        $bucket = $this->bucketObj();
219
        $packer = $this->packerObj();
220
221
        $this->packed = $packer->pack(
222
            $bucket->getTokenData()
223
        );
224
225
        /**
226
         * [Update account access]
227
         */
228
        (new user\user)
229
            ->setAccountID($this->getAccount()->account_id)
230
            ->setBucketToken($this->packed)
231
            ->updateAccountAccess()
232
        ;
233
    }
234
235
    /**
236
     * [getThrottle Return a list of the throttled results]
237
     * @return array
238
     */
239
    public function getThrottle()
240
    {
241
        if ($this->isUnlimited() || $this->scope !== 'private') {
242
            return array(
243
                'unlimited' => true,
244
            );
245
        }
246
247
        $bucket = $this->bucketObj();
248
249
        $windowFrame = (is_string($this->getTimeframe()))
250
        ? $this->getTimeframe()
251
        : $this->getTimeframe() . 'secs'
252
        ;
253
254
        if (is_null($bucket)) {
255
            return;
256
        }
257
258
        return array(
259
            'limit' => $this->getCapacity(),
260
            'leakRate' => $this->getLeakRate(),
261
            'leak' => $bucket->getLeakage(),
262
            'lastAccess' => $this->getLastAccessDate(),
263
            'description' => $this->getCapacity() . ' requests per ' . $windowFrame,
264
            'bucket' => $bucket->getTokenData(),
265
        );
266
    }
267
268
    /**
269
     * [getLastAccessDate Get the last recorded access in date format]
270
     * @return string
271
     */
272
    private function getLastAccessDate()
273
    {
274
        $bucket = $this->bucketObj();
275
276
        if (isset($bucket->getTokenData()['time'])) {
277
            return date('m/d/y h:i:sa', $bucket->getTokenData()['time']);
278
        }
279
280
        return 'Can\'t be converted';
281
    }
282
283
    /**
284
     * [setAccount Set the requests account]
285
     * @return self
286
     */
287
    public function setAccount($account)
288
    {
289
        $this->account = $account;
290
        return $this;
291
    }
292
293
    /**
294
     * [getAccount Get the requests account]
295
     */
296
    public function getAccount()
297
    {
298
        if (is_null($this->account)||empty($this->account)) {
299
            (new exception\errorException)
300
                ->setOptions($this->getOptions())
301
                ->error('UNAUTHORIZED');
302
            return;
303
        }
304
305
        return $this->account;
306
    }
307
308
    /**
309
     * [bucketObj Get the bucket class object]
310
     * @return object
311
     */
312
    private function bucketObj()
313
    {
314
        return $this->bucket;
315
    }
316
317
    /**
318
     * [packerObj Get the token packer class object]
319
     * @return object
320
     */
321
    private function packerObj()
322
    {
323
        return $this->tokenPacker;
324
    }
325
326
    /**
327
     * [options Responsible API options]
328
     * @param array $options
329
     */
330
    public function options($options)
331
    {
332
        $this->options = $options;
333
        return $this;
334
    }
335
336
    /**
337
     * [getOptions Get the stored Responsible API options]
338
     * @return array
339
     */
340
    private function getOptions()
341
    {
342
        return $this->options;
343
    }
344
345
    /**
346
     * [hasOptionProperty Check if an option property is set]
347
     * @param  array  $options
348
     * @param  string  $property
349
     * @return string|integer|boolean
350
     */
351
    private function hasOptionProperty(array $options, $property, $default = false)
352
    {
353
        $val = isset($options[$property]) ? $options[$property] : $default;
354
355
        if ($val && empty($options[$property])) {
356
            $val = $default;
357
        }
358
359
        return $val;
360
    }
361
362
    /**
363
     * [setCapacity Set the buckets capacity]
364
     * @param array $options
365
     */
366
    public function setCapacity($options)
367
    {
368
        $hasCapacityOption = $this->hasOptionProperty($options, 'rateLimit');
369
370
        if ($hasCapacityOption) {
371
            if (!is_integer($hasCapacityOption) || empty($hasCapacityOption)) {
372
                $hasCapacityOption = false;
373
            }
374
        }
375
376
        $this->capacity = ($hasCapacityOption) ? $hasCapacityOption : $this->capacity;
0 ignored issues
show
Documentation Bug introduced by
It seems like $hasCapacityOption ? $ha...ption : $this->capacity can also be of type string. However, the property $capacity is declared as type integer. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
377
    }
378
379
    /**
380
     * [getCapacity Get the buckets capacity]
381
     * @return integer
382
     */
383
    public function getCapacity()
384
    {
385
        return $this->capacity;
386
    }
387
388
    /**
389
     * [setTimeframe Set the window timeframe]
390
     * @param array $options
391
     */
392
    public function setTimeframe($options)
393
    {
394
        $timeframe = $this->hasOptionProperty($options, 'rateWindow');
395
396
        if (!is_string($timeframe) && !is_numeric($timeframe)) {
397
            $timeframe = $this->window;
398
        }
399
400
        if (!$timeframe) {
401
            $timeframe = $this->window;
402
        }
403
404
        if (is_numeric($timeframe)) {
405
            self::$timeframe['CUSTOM'] = $timeframe;
406
            $this->window = self::$timeframe['CUSTOM'];
407
            return;
408
        }
409
410
        if (isset(self::$timeframe[$timeframe])) {
411
            $this->window = self::$timeframe[$timeframe];
412
            return;
413
        }
414
415
        $this->window = self::$timeframe['MINUTE'];
416
    }
417
418
    /**
419
     * [getTimeframe Get the timeframe window]
420
     * @return integer|string
421
     */
422
    public function getTimeframe()
423
    {
424
        return $this->window;
425
    }
426
427
    /**
428
     * [setLeakRate Set the buckets leak rate]
429
     * Options: slow, medium, normal, default, fast or custom positive integer
430
     * @param array $options
431
     */
432
    private function setLeakRate($options)
433
    {
434
        if (isset($options['leak']) && !$options['leak']) {
435
            $options['leakRate'] = 'default';
436
        }
437
438
        $leakRate = $this->hasOptionProperty($options, 'leakRate');
439
440
        if (empty($leakRate) || !is_string($leakRate)) {
441
            $leakRate = 'default';
442
        }
443
444
        $this->leakRate = $leakRate;
445
    }
446
447
    /**
448
     * [getLeakRate Get the buckets leak rate]
449
     * @return string|integer
450
     */
451
    private function getLeakRate()
452
    {
453
        return $this->leakRate;
454
    }
455
456
    /**
457
     * [setUnlimited Rate limiter bypass]
458
     * @param array $options
459
     */
460
    private function setUnlimited($options)
461
    {
462
        $unlimited = false;
463
464
        if (isset($options['unlimited']) && ($options['unlimited'] == 1 || $options['unlimited'] == true)) {
465
            $unlimited = true;
466
        }
467
468
        if (isset($options['requestType']) && $options['requestType'] === 'debug') {
469
            $unlimited = true;
470
        }
471
472
        $this->unlimited = $unlimited;
473
    }
474
475
    /**
476
     * [isUnlimited Check if the Responsible API is set to unlimited]
477
     * @return boolean
478
     */
479
    private function isUnlimited()
480
    {
481
        return $this->unlimited;
482
    }
483
}
484