1
|
|
|
<?php namespace Comodojo\Cache; |
2
|
|
|
|
3
|
|
|
use \Comodojo\Cache\CacheInterface\CacheInterface; |
4
|
|
|
use \Comodojo\Cache\CacheObject\CacheObject; |
5
|
|
|
use \Comodojo\Exception\CacheException; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Cache manager |
9
|
|
|
* |
10
|
|
|
* @package Comodojo Spare Parts |
11
|
|
|
* @author Marco Giovinazzi <[email protected]> |
12
|
|
|
* @license MIT |
13
|
|
|
* |
14
|
|
|
* LICENSE: |
15
|
|
|
* |
16
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
17
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
18
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
19
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
20
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
21
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
22
|
|
|
* THE SOFTWARE. |
23
|
|
|
*/ |
24
|
|
|
|
25
|
|
|
class CacheManager { |
26
|
|
|
|
27
|
|
|
const PICK_FIRST = 1; |
28
|
|
|
const PICK_LAST = 2; |
29
|
|
|
const PICK_RANDOM = 3; |
30
|
|
|
const PICK_BYWEIGHT = 4; |
31
|
|
|
const PICK_ALL = 5; |
32
|
|
|
|
33
|
|
|
private $caches = array(); |
34
|
|
|
|
35
|
|
|
private $cache_weights = array(); |
36
|
|
|
|
37
|
|
|
private $selector = null; |
38
|
|
|
|
39
|
|
|
private $selected_cache = null; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Determine the current cache scope (default: GLOBAL) |
43
|
|
|
* |
44
|
|
|
* @var string |
45
|
|
|
*/ |
46
|
|
|
protected $namespace = "GLOBAL"; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* current time (in sec) |
50
|
|
|
* |
51
|
|
|
* @var int |
52
|
|
|
*/ |
53
|
|
|
protected $current_time = null; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Current instance of \Monolog\Logger |
57
|
|
|
* |
58
|
|
|
* @var \Monolog\Logger |
59
|
|
|
*/ |
60
|
|
|
protected $logger = null; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Cache ttl |
64
|
|
|
* |
65
|
|
|
* @var int |
66
|
|
|
*/ |
67
|
|
|
protected $ttl = null; |
68
|
|
|
|
69
|
135 |
|
public function __construct($select_mode = null, \Monolog\Logger $logger = null) { |
70
|
|
|
|
71
|
135 |
|
$this->selector = filter_var($select_mode, FILTER_VALIDATE_INT, array( |
72
|
|
|
'options' => array( |
73
|
135 |
|
'min_range' => 1, |
74
|
135 |
|
'max_range' => 4, |
75
|
|
|
'default' => 3 |
76
|
135 |
|
) |
77
|
135 |
|
)); |
78
|
|
|
|
79
|
|
|
try { |
80
|
|
|
|
81
|
135 |
|
$this->setTime(); |
82
|
|
|
|
83
|
135 |
|
$this->setTtl(); |
84
|
|
|
|
85
|
135 |
|
if ( !is_null($logger) ) $this->setLogger($logger); |
86
|
|
|
|
87
|
135 |
|
} catch (CacheException $ce) { |
88
|
|
|
|
89
|
|
|
throw $ce; |
90
|
|
|
|
91
|
|
|
} |
92
|
|
|
|
93
|
135 |
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Get current time |
97
|
|
|
* |
98
|
|
|
* @return float |
99
|
|
|
*/ |
100
|
135 |
|
final public function getTime() { |
101
|
|
|
|
102
|
135 |
|
return $this->current_time; |
103
|
|
|
|
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Get current ttl |
108
|
|
|
* |
109
|
|
|
* @return int |
110
|
|
|
*/ |
111
|
135 |
|
final public function getTtl() { |
112
|
|
|
|
113
|
135 |
|
return $this->ttl; |
114
|
|
|
|
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Get current namespace |
119
|
|
|
* |
120
|
|
|
* @return int |
121
|
|
|
*/ |
122
|
|
|
final public function getNamespace() { |
123
|
|
|
|
124
|
|
|
return $this->namespace; |
125
|
|
|
|
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Get current logger |
130
|
|
|
* |
131
|
|
|
* @return \Monolog\Logger |
132
|
|
|
*/ |
133
|
|
|
final public function getLogger() { |
134
|
|
|
|
135
|
|
|
return $this->logger; |
136
|
|
|
|
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
public function raiseError($message, $parameters = array()) { |
140
|
|
|
|
141
|
|
|
if ( $this->logger instanceof \Monolog\Logger ) $this->logger->addError($message, $parameters); |
142
|
|
|
|
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Set current time |
147
|
|
|
* |
148
|
|
|
* @param int $time Set current time (in msec - float) |
149
|
|
|
* |
150
|
|
|
* @return \Comodojo\Cache\CacheObject\CacheObject |
151
|
|
|
* @throws \Comodojo\Exception\CacheException |
152
|
|
|
*/ |
153
|
135 |
View Code Duplication |
final public function setTime($time = null) { |
|
|
|
|
154
|
|
|
|
155
|
135 |
|
if ( is_null($time) ) $this->current_time = time(); |
156
|
|
|
|
157
|
|
|
else if ( preg_match('/^[0-9]{10}$/', $time) ) $this->current_time = $time; |
158
|
|
|
|
159
|
|
|
else { |
160
|
|
|
|
161
|
|
|
throw new CacheException("Invalid time"); |
162
|
|
|
|
163
|
|
|
} |
164
|
|
|
|
165
|
135 |
|
foreach ( $this->caches as $cache ) $cache->setTime($time); |
166
|
|
|
|
167
|
135 |
|
return $this; |
168
|
|
|
|
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Set time to live for cache |
173
|
|
|
* |
174
|
|
|
* @param int $ttl Time to live (in secs) |
175
|
|
|
* |
176
|
|
|
* @return \Comodojo\Cache\CacheObject\CacheObject |
177
|
|
|
* @throws \Comodojo\Exception\CacheException |
178
|
|
|
*/ |
179
|
135 |
View Code Duplication |
final public function setTtl($ttl = null) { |
|
|
|
|
180
|
|
|
|
181
|
135 |
|
if ( is_null($ttl) ) { |
182
|
|
|
|
183
|
135 |
|
$this->ttl = defined('COMODOJO_CACHE_DEFAULT_TTL') ? COMODOJO_CACHE_DEFAULT_TTL : 3600; |
184
|
|
|
|
185
|
135 |
|
} else if ( is_int($ttl) ) { |
186
|
|
|
|
187
|
|
|
$this->ttl = $ttl; |
188
|
|
|
|
189
|
|
|
} else { |
190
|
|
|
|
191
|
|
|
throw new CacheException("Invalid time to live"); |
192
|
|
|
|
193
|
|
|
} |
194
|
|
|
|
195
|
135 |
|
foreach ( $this->caches as $cache ) $cache->setTtl($ttl); |
196
|
|
|
|
197
|
135 |
|
return $this; |
198
|
|
|
|
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Set namespace for cache |
203
|
|
|
* |
204
|
|
|
* @param string $namespace Selected namespace (64 chars limited) |
205
|
|
|
* |
206
|
|
|
* @return \Comodojo\Cache\CacheObject\CacheObject |
207
|
|
|
* @throws \Comodojo\Exception\CacheException |
208
|
|
|
*/ |
209
|
15 |
|
final public function setNamespace($namespace) { |
210
|
|
|
|
211
|
15 |
View Code Duplication |
if ( preg_match('/^[0-9a-zA-Z]+$/', $namespace) && strlen($namespace) <= 64 ) { |
|
|
|
|
212
|
|
|
|
213
|
15 |
|
$this->namespace = strtoupper($namespace); |
214
|
|
|
|
215
|
15 |
|
} else { |
216
|
|
|
|
217
|
|
|
throw new CacheException("Invalid namespace"); |
218
|
|
|
|
219
|
|
|
} |
220
|
|
|
|
221
|
15 |
|
foreach ( $this->caches as $cache ) $cache->setNamespace($namespace); |
222
|
|
|
|
223
|
15 |
|
return $this; |
224
|
|
|
|
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Set the monolog instance |
229
|
|
|
* |
230
|
|
|
* @param \Monolog\Logger $logger |
231
|
|
|
* |
232
|
|
|
* @return \Comodojo\Cache\CacheObject\CacheObject |
233
|
|
|
*/ |
234
|
|
|
final public function setLogger(\Monolog\Logger $logger) { |
235
|
|
|
|
236
|
|
|
$this->logger = $logger; |
237
|
|
|
|
238
|
|
|
foreach ( $this->caches as $cache ) $cache->setLogger($logger); |
239
|
|
|
|
240
|
|
|
return $this; |
241
|
|
|
|
242
|
|
|
} |
243
|
|
|
|
244
|
135 |
|
public function addProvider(CacheInterface $cache_provider, $weight = 0) { |
245
|
|
|
|
246
|
135 |
|
$corrected_weight = filter_var($weight, FILTER_VALIDATE_INT, array( |
247
|
|
|
'options' => array( |
248
|
135 |
|
'min_range' => 0, |
249
|
135 |
|
'max_range' => 100, |
250
|
|
|
'default' => 0 |
251
|
135 |
|
) |
252
|
135 |
|
)); |
253
|
|
|
|
254
|
135 |
|
$cache_id = $cache_provider->getCacheId(); |
255
|
|
|
|
256
|
135 |
|
if ( array_key_exists($cache_id, $this->caches) ) throw new CacheException("Cache provider already registered"); |
257
|
|
|
|
258
|
135 |
|
$cache_provider->setTime($this->getTime())->setTtl($this->getTtl()); |
259
|
|
|
|
260
|
135 |
|
if ( $this->logger instanceof \Monolog\Logger ) $cache_provider->setLogger($this->logger); |
261
|
|
|
|
262
|
135 |
|
$this->caches[$cache_id] = $cache_provider; |
263
|
|
|
|
264
|
135 |
|
$this->cache_weights[$cache_id] = $corrected_weight; |
265
|
|
|
|
266
|
135 |
|
return $cache_id; |
267
|
|
|
|
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
public function removeProvider($cache_id) { |
271
|
|
|
|
272
|
|
|
if ( array_key_exists($cache_id, $this->caches) && array_key_exists($cache_id, $this->cache_weights) ) { |
273
|
|
|
|
274
|
|
|
unset($this->caches[$cache_id]); |
275
|
|
|
|
276
|
|
|
unset($this->cache_weights[$cache_id]); |
277
|
|
|
|
278
|
|
|
} else { |
279
|
|
|
|
280
|
|
|
throw new CacheException("Cache not registered"); |
281
|
|
|
|
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
return true; |
285
|
|
|
|
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
public function getProviders($type = null) { |
289
|
|
|
|
290
|
|
|
$providers = array(); |
291
|
|
|
|
292
|
|
|
if ( is_null($type) ) { |
293
|
|
|
|
294
|
|
|
foreach ( $this->caches as $id => $cache ) $providers[$id] = get_class($cache); |
295
|
|
|
|
296
|
|
|
} else { |
297
|
|
|
|
298
|
|
|
foreach ( $this->caches as $id => $cache ) { |
299
|
|
|
|
300
|
|
|
$provider_class = get_class($cache); |
301
|
|
|
|
302
|
|
|
if ( $provider_class == $type ) $providers[$id] = get_class($cache); |
303
|
|
|
|
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $providers; |
309
|
|
|
|
310
|
|
|
} |
311
|
|
|
|
312
|
60 |
|
public function set($name, $data, $ttl = null) { |
313
|
|
|
|
314
|
60 |
|
$set = array(); |
315
|
|
|
|
316
|
|
|
try { |
317
|
|
|
|
318
|
60 |
|
foreach ( $this->caches as $cache_id => $cache ) { |
319
|
|
|
|
320
|
60 |
|
$set[$cache_id] = $cache->set($name, $data, $ttl); |
321
|
|
|
|
322
|
60 |
|
} |
323
|
|
|
|
324
|
60 |
|
} catch (CacheException $ce) { |
325
|
|
|
|
326
|
|
|
throw $ce; |
327
|
|
|
|
328
|
|
|
} |
329
|
|
|
|
330
|
60 |
|
return $set; |
331
|
|
|
|
332
|
|
|
} |
333
|
|
|
|
334
|
75 |
|
public function get($name) { |
335
|
|
|
|
336
|
75 |
|
reset($this->caches); |
337
|
|
|
|
338
|
75 |
|
$this->selected_cache = null; |
339
|
|
|
|
340
|
75 |
|
$result = null; |
341
|
|
|
|
342
|
|
|
try { |
343
|
|
|
|
344
|
75 |
|
switch ( $this->selector ) { |
345
|
|
|
|
346
|
75 |
|
case 1: |
|
|
|
|
347
|
|
|
|
348
|
15 |
|
$result = $this->getCacheByLoop($this->caches, $name); |
349
|
|
|
|
350
|
15 |
|
break; |
351
|
|
|
|
352
|
60 |
|
case 2: |
|
|
|
|
353
|
|
|
|
354
|
15 |
|
$result = $this->getCacheByLoop(array_reverse($this->caches, true), $name); |
355
|
|
|
|
356
|
15 |
|
break; |
357
|
|
|
|
358
|
45 |
|
case 3: |
|
|
|
|
359
|
|
|
|
360
|
30 |
|
$result = $this->getRandomCache($this->caches, $name); |
361
|
|
|
|
362
|
30 |
|
break; |
363
|
|
|
|
364
|
15 |
|
case 4: |
|
|
|
|
365
|
|
|
|
366
|
15 |
|
$result = $this->getCacheByWeight($this->caches, $this->cache_weights, $name); |
367
|
|
|
|
368
|
15 |
|
break; |
369
|
|
|
|
370
|
|
|
case 5: |
|
|
|
|
371
|
|
|
|
372
|
|
|
$values = array(); |
373
|
|
|
|
374
|
|
|
foreach ( $this->caches as $cache ) { |
375
|
|
|
|
376
|
|
|
$values[] = $cache->get($name); |
377
|
|
|
|
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
if ( count(array_unique($values)) === 1 ) { |
381
|
|
|
|
382
|
|
|
$result = $values[0]; |
383
|
|
|
|
384
|
|
|
} else { |
385
|
|
|
|
386
|
|
|
$this->raiseError("Inconsistent values in cache providers, exiting gracefully"); |
387
|
|
|
|
388
|
|
|
$result = null; |
389
|
|
|
|
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
break; |
393
|
|
|
|
394
|
75 |
|
} |
395
|
|
|
|
396
|
75 |
|
} catch (\Exception $e) { |
397
|
|
|
|
398
|
|
|
throw $ce; |
399
|
|
|
|
400
|
|
|
} |
401
|
|
|
|
402
|
75 |
|
return $result; |
403
|
|
|
|
404
|
|
|
} |
405
|
|
|
|
406
|
15 |
View Code Duplication |
public function delete($name = null) { |
|
|
|
|
407
|
|
|
|
408
|
15 |
|
$delete = array(); |
409
|
|
|
|
410
|
|
|
try { |
411
|
|
|
|
412
|
15 |
|
foreach ( $this->caches as $cache_id => $cache ) { |
413
|
|
|
|
414
|
15 |
|
$delete[$cache_id] = $cache->delete($name); |
415
|
|
|
|
416
|
15 |
|
} |
417
|
|
|
|
418
|
15 |
|
} catch (CacheException $ce) { |
419
|
|
|
|
420
|
|
|
throw $ce; |
421
|
|
|
|
422
|
|
|
} |
423
|
|
|
|
424
|
15 |
|
return $delete; |
425
|
|
|
|
426
|
|
|
} |
427
|
|
|
|
428
|
15 |
View Code Duplication |
public function flush() { |
|
|
|
|
429
|
|
|
|
430
|
15 |
|
$flush = array(); |
431
|
|
|
|
432
|
|
|
try { |
433
|
|
|
|
434
|
15 |
|
foreach ( $this->caches as $cache_id => $cache ) { |
435
|
|
|
|
436
|
15 |
|
$flush[$cache_id] = $cache->flush(); |
437
|
|
|
|
438
|
15 |
|
} |
439
|
|
|
|
440
|
15 |
|
} catch (CacheException $ce) { |
441
|
|
|
|
442
|
|
|
throw $ce; |
443
|
|
|
|
444
|
|
|
} |
445
|
|
|
|
446
|
15 |
|
return $flush; |
447
|
|
|
|
448
|
|
|
} |
449
|
|
|
|
450
|
15 |
View Code Duplication |
public function status() { |
|
|
|
|
451
|
|
|
|
452
|
15 |
|
$status = array(); |
453
|
|
|
|
454
|
|
|
try { |
455
|
|
|
|
456
|
15 |
|
foreach ( $this->caches as $cache_id => $cache ) { |
457
|
|
|
|
458
|
15 |
|
$status[$cache_id] = $cache->status(); |
459
|
|
|
|
460
|
15 |
|
} |
461
|
|
|
|
462
|
15 |
|
} catch (CacheException $ce) { |
463
|
|
|
|
464
|
|
|
throw $ce; |
465
|
|
|
|
466
|
|
|
} |
467
|
|
|
|
468
|
15 |
|
return $status; |
469
|
|
|
|
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
public function getSelectedCache() { |
473
|
|
|
|
474
|
|
|
return $this->selected_cache; |
475
|
|
|
|
476
|
|
|
} |
477
|
|
|
|
478
|
30 |
|
private function getCacheByLoop($caches, $name) { |
479
|
|
|
|
480
|
30 |
|
$result = null; |
481
|
|
|
|
482
|
30 |
|
$active_cache = false; |
483
|
|
|
|
484
|
30 |
|
foreach ( $caches as $cache ) { |
485
|
|
|
|
486
|
30 |
|
if ( $cache->isEnabled() ) { |
487
|
|
|
|
488
|
30 |
|
$result = $cache->get($name); |
489
|
|
|
|
490
|
30 |
|
if ( $cache->getErrorState() === false ) { |
491
|
|
|
|
492
|
30 |
|
$this->selected_cache = $cache->getCacheId(); |
493
|
|
|
|
494
|
30 |
|
$active_cache = true; |
495
|
|
|
|
496
|
30 |
|
break; |
497
|
|
|
|
498
|
|
|
} |
499
|
|
|
|
500
|
3 |
|
} |
501
|
|
|
|
502
|
30 |
|
} |
503
|
|
|
|
504
|
30 |
|
if ( $active_cache === false ) $this->raiseError("Cannot find an active cache provider (Manager), exiting gracefully"); |
505
|
|
|
|
506
|
30 |
|
return $result; |
507
|
|
|
|
508
|
|
|
} |
509
|
|
|
|
510
|
30 |
|
private function getRandomCache($caches, $name) { |
511
|
|
|
|
512
|
30 |
|
$result = null; |
513
|
|
|
|
514
|
30 |
|
$active_cache = false; |
515
|
|
|
|
516
|
30 |
|
$size = sizeof($caches); |
517
|
|
|
|
518
|
30 |
|
for ( $i = 0; $i < $size; $i++ ) { |
519
|
|
|
|
520
|
30 |
|
$cache_id = array_rand($caches); |
521
|
|
|
|
522
|
30 |
|
$cache = $caches[$cache_id]; |
523
|
|
|
|
524
|
30 |
View Code Duplication |
if ( $cache->isEnabled() ) { |
|
|
|
|
525
|
|
|
|
526
|
30 |
|
$result = $cache->get($name); |
527
|
|
|
|
528
|
30 |
|
if ( $cache->getErrorState() === false ) { |
529
|
|
|
|
530
|
30 |
|
$this->selected_cache = $cache_id; |
531
|
|
|
|
532
|
30 |
|
$active_cache = true; |
533
|
|
|
|
534
|
30 |
|
break; |
535
|
|
|
|
536
|
|
|
} else { |
537
|
|
|
|
538
|
1 |
|
unset($caches[$cache_id]); |
539
|
|
|
|
540
|
|
|
} |
541
|
|
|
|
542
|
1 |
|
} else { |
543
|
|
|
|
544
|
|
|
unset($caches[$cache_id]); |
545
|
|
|
|
546
|
|
|
} |
547
|
|
|
|
548
|
1 |
|
} |
549
|
|
|
|
550
|
30 |
|
if ( $active_cache === false ) $this->raiseError("Cannot find an active cache provider (Manager), exiting gracefully"); |
551
|
|
|
|
552
|
30 |
|
return $result; |
553
|
|
|
|
554
|
|
|
} |
555
|
|
|
|
556
|
15 |
|
private function getCacheByWeight($caches, $weights, $name) { |
557
|
|
|
|
558
|
15 |
|
$result = null; |
559
|
|
|
|
560
|
15 |
|
$active_cache = false; |
561
|
|
|
|
562
|
15 |
|
$size = sizeof($weights); |
563
|
|
|
|
564
|
15 |
|
for ( $i = 0; $i < $size; $i++ ) { |
565
|
|
|
|
566
|
15 |
|
$cache_ids = array_keys($weights, max($weights)); |
567
|
|
|
|
568
|
15 |
|
$cache_id = $cache_ids[0]; |
569
|
|
|
|
570
|
15 |
|
$cache = $caches[$cache_id]; |
571
|
|
|
|
572
|
15 |
View Code Duplication |
if ( $cache->isEnabled() ) { |
|
|
|
|
573
|
|
|
|
574
|
15 |
|
$result = $cache->get($name); |
575
|
|
|
|
576
|
15 |
|
if ( $cache->getErrorState() === false ) { |
577
|
|
|
|
578
|
15 |
|
$this->selected_cache = $cache_id; |
579
|
|
|
|
580
|
15 |
|
$active_cache = true; |
581
|
|
|
|
582
|
15 |
|
break; |
583
|
|
|
|
584
|
|
|
} else { |
585
|
|
|
|
586
|
|
|
unset($weights[$cache_id]); |
587
|
|
|
|
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
} else { |
591
|
|
|
|
592
|
|
|
unset($weights[$cache_id]); |
593
|
|
|
|
594
|
|
|
} |
595
|
|
|
|
596
|
|
|
} |
597
|
|
|
|
598
|
15 |
|
if ( $active_cache === false ) $this->raiseError("Cannot find an active cache provider (Manager), exiting gracefully"); |
599
|
|
|
|
600
|
15 |
|
return $result; |
601
|
|
|
|
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
} |
605
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.