1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace N98\Util\Console\Helper; |
4
|
|
|
|
5
|
|
|
use PDO; |
6
|
|
|
use PDOException; |
7
|
|
|
use RuntimeException; |
8
|
|
|
use Symfony\Component\Console\Helper\Helper as AbstractHelper; |
9
|
|
|
use Symfony\Component\Console\Output\NullOutput; |
10
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
11
|
|
|
|
12
|
|
|
class DatabaseHelper extends AbstractHelper |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* @var array |
16
|
|
|
*/ |
17
|
|
|
protected $dbSettings = null; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @var bool |
21
|
|
|
*/ |
22
|
|
|
protected $isSocketConnect = false; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var PDO |
26
|
|
|
*/ |
27
|
|
|
protected $_connection = null; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
protected $_tables; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @param OutputInterface $output |
36
|
|
|
* |
37
|
|
|
* @throws RuntimeException |
38
|
|
|
* @return void |
39
|
|
|
*/ |
40
|
|
|
public function detectDbSettings(OutputInterface $output) |
41
|
|
|
{ |
42
|
|
|
if ($this->dbSettings !== null) { |
43
|
|
|
return; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
$magentoHelper = $this->getHelperSet()->getCommand()->getHelper('magento'); |
47
|
|
|
$config = $magentoHelper->getBaseConfig(); // @TODO Use \Magento\Framework\App\DeploymentConfig ? |
48
|
|
|
|
49
|
|
|
if (!isset($config['db'])) { |
50
|
|
|
$output->writeln('<error>DB settings was not found in config.xml file</error>'); |
51
|
|
|
return; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
if (!isset($config['db']['connection']['default'])) { |
55
|
|
|
throw new RuntimeException('Cannot find default connection config in app/etc/config.php'); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
$this->dbSettings = (array) $config['db']['connection']['default']; |
59
|
|
|
|
60
|
|
|
$this->dbSettings['prefix'] = ''; |
61
|
|
|
if (isset($config['db']['table_prefix'])) { |
62
|
|
|
$this->dbSettings['prefix'] = (string) $config['db']['table_prefix']; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
View Code Duplication |
if (strpos($this->dbSettings['host'], ':') !== false) { |
|
|
|
|
66
|
|
|
list($this->dbSettings['host'], $this->dbSettings['port']) = explode(':', $this->dbSettings['host']); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
if (isset($this->dbSettings['comment'])) { |
70
|
|
|
unset($this->dbSettings['comment']); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
if (isset($this->dbSettings['unix_socket'])) { |
74
|
|
|
$this->isSocketConnect = true; |
75
|
|
|
} |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Connects to the database without initializing magento |
80
|
|
|
* |
81
|
|
|
* @param OutputInterface $output = null |
|
|
|
|
82
|
|
|
* |
83
|
|
|
* @return PDO |
84
|
|
|
* @throws RuntimeException pdo mysql extension is not installed |
85
|
|
|
*/ |
86
|
|
|
public function getConnection(OutputInterface $output = null) |
87
|
|
|
{ |
88
|
|
|
if ($output == null) { |
89
|
|
|
$output = new NullOutput(); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
if ($this->_connection) { |
93
|
|
|
return $this->_connection; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
$this->detectDbSettings($output); |
97
|
|
|
|
98
|
|
|
if (!extension_loaded('pdo_mysql')) { |
99
|
|
|
throw new RuntimeException('pdo_mysql extension is not installed'); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
if (strpos($this->dbSettings['host'], '/') !== false) { |
103
|
|
|
$this->dbSettings['unix_socket'] = $this->dbSettings['host']; |
104
|
|
|
unset($this->dbSettings['host']); |
105
|
|
View Code Duplication |
} elseif (strpos($this->dbSettings['host'], ':') !== false) { |
|
|
|
|
106
|
|
|
list($this->dbSettings['host'], $this->dbSettings['port']) = explode(':', $this->dbSettings['host']); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
$this->_connection = new PDO( |
110
|
|
|
$this->dsn(), |
111
|
|
|
$this->dbSettings['username'], |
112
|
|
|
$this->dbSettings['password'] |
113
|
|
|
); |
114
|
|
|
|
115
|
|
|
/** @link http://bugs.mysql.com/bug.php?id=18551 */ |
116
|
|
|
$this->_connection->query("SET SQL_MODE=''"); |
117
|
|
|
|
118
|
|
|
try { |
119
|
|
|
$this->_connection->query('USE `' . $this->dbSettings['dbname'] . '`'); |
120
|
|
|
} catch (PDOException $e) { |
121
|
|
|
if (OutputInterface::VERBOSITY_VERY_VERBOSE <= $output->getVerbosity()) { |
122
|
|
|
$output->writeln(sprintf( |
123
|
|
|
'<error>Failed to use database <comment>%s</comment>: %s</error>', |
124
|
|
|
var_export($this->dbSettings['dbname'], true), |
125
|
|
|
$e->getMessage() |
126
|
|
|
)); |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
$this->_connection->query("SET NAMES utf8"); |
131
|
|
|
|
132
|
|
|
$this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); |
133
|
|
|
$this->_connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); |
134
|
|
|
|
135
|
|
|
return $this->_connection; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Creates a PDO DSN for the adapter from $this->_config settings. |
140
|
|
|
* |
141
|
|
|
* @see Zend_Db_Adapter_Pdo_Abstract |
142
|
|
|
* @return string |
143
|
|
|
*/ |
144
|
|
|
public function dsn() |
145
|
|
|
{ |
146
|
|
|
$this->detectDbSettings(new NullOutput()); |
147
|
|
|
|
148
|
|
|
// baseline of DSN parts |
149
|
|
|
$dsn = $this->dbSettings; |
150
|
|
|
|
151
|
|
|
// don't pass the username, password, charset, database, persistent and driver_options in the DSN |
152
|
|
|
unset($dsn['username']); |
153
|
|
|
unset($dsn['password']); |
154
|
|
|
unset($dsn['options']); |
155
|
|
|
unset($dsn['charset']); |
156
|
|
|
unset($dsn['persistent']); |
157
|
|
|
unset($dsn['driver_options']); |
158
|
|
|
unset($dsn['dbname']); |
159
|
|
|
|
160
|
|
|
// use all remaining parts in the DSN |
161
|
|
|
$buildDsn = array(); |
162
|
|
|
foreach ($dsn as $key => $val) { |
163
|
|
|
if (is_array($val)) { |
164
|
|
|
continue; |
165
|
|
|
} |
166
|
|
|
$buildDsn[$key] = "$key=$val"; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
return 'mysql:' . implode(';', $buildDsn); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Check whether current mysql user has $privilege privilege |
174
|
|
|
* |
175
|
|
|
* @param string $privilege |
176
|
|
|
* |
177
|
|
|
* @return bool |
178
|
|
|
*/ |
179
|
|
|
public function mysqlUserHasPrivilege($privilege) |
180
|
|
|
{ |
181
|
|
|
$statement = $this->getConnection()->query('SHOW GRANTS'); |
182
|
|
|
|
183
|
|
|
$result = $statement->fetchAll(PDO::FETCH_COLUMN); |
184
|
|
|
foreach ($result as $row) { |
185
|
|
|
if (preg_match('/^GRANT(.*)' . strtoupper($privilege) . '/', $row) |
186
|
|
|
|| preg_match('/^GRANT(.*)ALL/', $row) |
187
|
|
|
) { |
188
|
|
|
return true; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
return false; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* @return string |
197
|
|
|
*/ |
198
|
|
|
public function getMysqlClientToolConnectionString() |
199
|
|
|
{ |
200
|
|
|
$this->detectDbSettings(new NullOutput()); |
201
|
|
|
|
202
|
|
|
if ($this->isSocketConnect) { |
203
|
|
|
$string = '--socket=' . escapeshellarg($this->dbSettings['unix_socket']); |
204
|
|
|
} else { |
205
|
|
|
$string = '-h' . escapeshellarg($this->dbSettings['host']); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$string .= ' ' |
209
|
|
|
. '-u' . escapeshellarg($this->dbSettings['username']) |
210
|
|
|
. ' ' |
211
|
|
|
. (isset($this->dbSettings['port']) |
212
|
|
|
? '-P' . escapeshellarg($this->dbSettings['port']) . ' ' : '') |
213
|
|
|
. (strlen($this->dbSettings['password']) |
214
|
|
|
? '--password=' . escapeshellarg($this->dbSettings['password']) . ' ' : '') |
215
|
|
|
. escapeshellarg($this->dbSettings['dbname']); |
216
|
|
|
|
217
|
|
|
return $string; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Get mysql variable value |
222
|
|
|
* |
223
|
|
|
* @param string $variable |
224
|
|
|
* |
225
|
|
|
* @return bool|string |
226
|
|
|
*/ |
227
|
|
|
public function getMysqlVariableValue($variable) |
228
|
|
|
{ |
229
|
|
|
$statement = $this->getConnection()->query("SELECT @@{$variable};"); |
230
|
|
|
$result = $statement->fetch(PDO::FETCH_ASSOC); |
231
|
|
|
if ($result) { |
232
|
|
|
return $result; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
return false; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* @param array $commandConfig |
240
|
|
|
* |
241
|
|
|
* @throws \Exception |
242
|
|
|
* @internal param $config |
243
|
|
|
* @return array $commandConfig |
244
|
|
|
* @return array |
245
|
|
|
*/ |
246
|
|
|
public function getTableDefinitions(array $commandConfig) |
247
|
|
|
{ |
248
|
|
|
$tableDefinitions = array(); |
249
|
|
|
if (isset($commandConfig['table-groups'])) { |
250
|
|
|
$tableGroups = $commandConfig['table-groups']; |
251
|
|
View Code Duplication |
foreach ($tableGroups as $index => $definition) { |
|
|
|
|
252
|
|
|
$description = isset($definition['description']) ? $definition['description'] : ''; |
253
|
|
|
if (!isset($definition['id'])) { |
254
|
|
|
throw new RuntimeException('Invalid definition of table-groups (id missing) Index: ' . $index); |
255
|
|
|
} |
256
|
|
|
if (!isset($definition['tables'])) { |
257
|
|
|
throw new RuntimeException( |
258
|
|
|
'Invalid definition of table-groups (tables missing) Id: ' . $definition['id'] |
259
|
|
|
); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$tableDefinitions[$definition['id']] = array( |
263
|
|
|
'tables' => $definition['tables'], |
264
|
|
|
'description' => $description, |
265
|
|
|
); |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return $tableDefinitions; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @param array $list |
274
|
|
|
* @param array $definitions |
275
|
|
|
* @param array $resolved Which definitions where already resolved -> prevent endless loops |
276
|
|
|
* |
277
|
|
|
* @return array |
278
|
|
|
* @throws RuntimeException |
279
|
|
|
*/ |
280
|
|
|
public function resolveTables(array $list, array $definitions = array(), array $resolved = array()) |
281
|
|
|
{ |
282
|
|
|
if ($this->_tables === null) { |
283
|
|
|
$this->_tables = $this->getTables(true); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
$resolvedList = array(); |
287
|
|
|
foreach ($list as $entry) { |
288
|
|
|
if (substr($entry, 0, 1) == '@') { |
289
|
|
|
$code = substr($entry, 1); |
290
|
|
|
if (!isset($definitions[$code])) { |
291
|
|
|
throw new RuntimeException('Table-groups could not be resolved: ' . $entry); |
292
|
|
|
} |
293
|
|
|
if (!isset($resolved[$code])) { |
294
|
|
|
$resolved[$code] = true; |
295
|
|
|
$tables = $this->resolveTables( |
296
|
|
|
explode(' ', $definitions[$code]['tables']), |
297
|
|
|
$definitions, |
298
|
|
|
$resolved |
299
|
|
|
); |
300
|
|
|
$resolvedList = array_merge($resolvedList, $tables); |
301
|
|
|
} |
302
|
|
|
continue; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
// resolve wildcards |
306
|
|
|
if (strpos($entry, '*') !== false) { |
307
|
|
|
$connection = $this->getConnection(); |
308
|
|
|
$sth = $connection->prepare( |
309
|
|
|
'SHOW TABLES LIKE :like', |
310
|
|
|
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY) |
311
|
|
|
); |
312
|
|
|
$sth->execute( |
313
|
|
|
array(':like' => str_replace('*', '%', $this->dbSettings['prefix'] . $entry)) |
314
|
|
|
); |
315
|
|
|
$rows = $sth->fetchAll(); |
316
|
|
|
foreach ($rows as $row) { |
317
|
|
|
$resolvedList[] = $row[0]; |
318
|
|
|
} |
319
|
|
|
continue; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
if (in_array($entry, $this->_tables)) { |
323
|
|
|
$resolvedList[] = $this->dbSettings['prefix'] . $entry; |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
asort($resolvedList); |
328
|
|
|
$resolvedList = array_unique($resolvedList); |
329
|
|
|
|
330
|
|
|
return $resolvedList; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Get list of db tables |
335
|
|
|
* |
336
|
|
|
* @param bool $withoutPrefix |
337
|
|
|
* |
338
|
|
|
* @return array |
339
|
|
|
*/ |
340
|
|
|
public function getTables($withoutPrefix = false) |
341
|
|
|
{ |
342
|
|
|
$db = $this->getConnection(); |
343
|
|
|
$prefix = $this->dbSettings['prefix']; |
344
|
|
View Code Duplication |
if (strlen($prefix) > 0) { |
|
|
|
|
345
|
|
|
$statement = $db->prepare('SHOW TABLES LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)); |
346
|
|
|
$statement->execute( |
347
|
|
|
array(':like' => $prefix . '%') |
348
|
|
|
); |
349
|
|
|
} else { |
350
|
|
|
$statement = $db->query('SHOW TABLES'); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
if ($statement) { |
354
|
|
|
$result = $statement->fetchAll(PDO::FETCH_COLUMN); |
355
|
|
|
if ($withoutPrefix === false) { |
356
|
|
|
return $result; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
return array_map(function ($tableName) use ($prefix) { |
360
|
|
|
return str_replace($prefix, '', $tableName); |
361
|
|
|
}, $result); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
return array(); |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Get list of db tables status |
369
|
|
|
* |
370
|
|
|
* @param bool $withoutPrefix |
371
|
|
|
* |
372
|
|
|
* @return array |
373
|
|
|
*/ |
374
|
|
|
public function getTablesStatus($withoutPrefix = false) |
375
|
|
|
{ |
376
|
|
|
$db = $this->getConnection(); |
377
|
|
|
$prefix = $this->dbSettings['prefix']; |
378
|
|
View Code Duplication |
if (strlen($prefix) > 0) { |
|
|
|
|
379
|
|
|
$statement = $db->prepare('SHOW TABLE STATUS LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)); |
380
|
|
|
$statement->execute( |
381
|
|
|
array(':like' => $prefix . '%') |
382
|
|
|
); |
383
|
|
|
} else { |
384
|
|
|
$statement = $db->query('SHOW TABLE STATUS'); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
if ($statement) { |
388
|
|
|
$result = $statement->fetchAll(PDO::FETCH_ASSOC); |
389
|
|
|
$return = array(); |
390
|
|
|
foreach ($result as $table) { |
391
|
|
|
if (true === $withoutPrefix) { |
392
|
|
|
$table['Name'] = str_replace($prefix, '', $table['Name']); |
393
|
|
|
} |
394
|
|
|
$return[$table['Name']] = $table; |
395
|
|
|
} |
396
|
|
|
return $return; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
return array(); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* @return array |
404
|
|
|
*/ |
405
|
|
|
public function getDbSettings() |
406
|
|
|
{ |
407
|
|
|
return $this->dbSettings; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* @return boolean |
412
|
|
|
*/ |
413
|
|
|
public function getIsSocketConnect() |
414
|
|
|
{ |
415
|
|
|
return $this->isSocketConnect; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* Returns the canonical name of this helper. |
420
|
|
|
* |
421
|
|
|
* @return string The canonical name |
422
|
|
|
* |
423
|
|
|
* @api |
424
|
|
|
*/ |
425
|
|
|
public function getName() |
426
|
|
|
{ |
427
|
|
|
return 'database'; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* @param OutputInterface $output |
432
|
|
|
*/ |
433
|
|
View Code Duplication |
public function dropDatabase($output) |
|
|
|
|
434
|
|
|
{ |
435
|
|
|
$this->detectDbSettings($output); |
436
|
|
|
$db = $this->getConnection(); |
437
|
|
|
$db->query('DROP DATABASE `' . $this->dbSettings['dbname'] . '`'); |
438
|
|
|
$output->writeln('<info>Dropped database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>'); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* @param OutputInterface $output |
443
|
|
|
*/ |
444
|
|
|
public function dropTables($output) |
445
|
|
|
{ |
446
|
|
|
$result = $this->getTables(); |
447
|
|
|
$query = 'SET FOREIGN_KEY_CHECKS = 0; '; |
448
|
|
|
$count = 0; |
449
|
|
|
foreach ($result as $tableName) { |
450
|
|
|
$query .= 'DROP TABLE IF EXISTS `' . $tableName . '`; '; |
451
|
|
|
$count++; |
452
|
|
|
} |
453
|
|
|
$query .= 'SET FOREIGN_KEY_CHECKS = 1;'; |
454
|
|
|
$this->getConnection()->query($query); |
455
|
|
|
$output->writeln('<info>Dropped database tables</info> <comment>' . $count . ' tables dropped</comment>'); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* @param OutputInterface $output |
460
|
|
|
*/ |
461
|
|
View Code Duplication |
public function createDatabase($output) |
|
|
|
|
462
|
|
|
{ |
463
|
|
|
$this->detectDbSettings($output); |
464
|
|
|
$db = $this->getConnection(); |
465
|
|
|
$db->query('CREATE DATABASE IF NOT EXISTS `' . $this->dbSettings['dbname'] . '`'); |
466
|
|
|
$output->writeln('<info>Created database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>'); |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* @param string $command example: 'VARIABLES', 'STATUS' |
471
|
|
|
* @param string $variable [optional] |
|
|
|
|
472
|
|
|
* |
473
|
|
|
* @return array |
474
|
|
|
*/ |
475
|
|
|
private function runShowCommand($command, $variable = null) |
476
|
|
|
{ |
477
|
|
|
$db = $this->getConnection(); |
478
|
|
|
|
479
|
|
|
if (null !== $variable) { |
480
|
|
|
$statement = $db->prepare( |
481
|
|
|
'SHOW /*!50000 GLOBAL */ ' . $command . ' LIKE :like', |
482
|
|
|
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY) |
483
|
|
|
); |
484
|
|
|
$statement->execute( |
485
|
|
|
array(':like' => $variable) |
486
|
|
|
); |
487
|
|
|
} else { |
488
|
|
|
$statement = $db->query('SHOW /*!50000 GLOBAL */ ' . $command); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
if ($statement) { |
492
|
|
|
$result = $statement->fetchAll(PDO::FETCH_ASSOC); |
493
|
|
|
$return = array(); |
494
|
|
|
foreach ($result as $row) { |
495
|
|
|
$return[$row['Variable_name']] = $row['Value']; |
496
|
|
|
} |
497
|
|
|
return $return; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
return array(); |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* @param string|null $variable [optional] |
505
|
|
|
* |
506
|
|
|
* @return array |
507
|
|
|
*/ |
508
|
|
|
public function getGlobalVariables($variable = null) |
509
|
|
|
{ |
510
|
|
|
return $this->runShowCommand('VARIABLES', $variable); |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* @param string $variable [optional] |
|
|
|
|
515
|
|
|
* |
516
|
|
|
* @return array |
517
|
|
|
*/ |
518
|
|
|
public function getGlobalStatus($variable = null) |
519
|
|
|
{ |
520
|
|
|
return $this->runShowCommand('STATUS', $variable); |
521
|
|
|
} |
522
|
|
|
} |
523
|
|
|
|
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.