DatabaseHelper   C
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 511
Duplicated Lines 10.18 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 3
Bugs 2 Features 0
Metric Value
c 3
b 2
f 0
dl 52
loc 511
wmc 66
lcom 1
cbo 1
rs 5.7474

19 Methods

Rating   Name   Duplication   Size   Complexity  
C getConnection() 3 51 8
A getMysqlClientToolConnectionString() 0 21 4
A getMysqlVariableValue() 0 10 2
B getTableDefinitions() 16 25 6
C resolveTables() 0 52 9
B getTables() 8 26 4
B getTablesStatus() 8 27 5
A dropDatabase() 7 7 1
A dropTables() 0 13 2
A createDatabase() 7 7 1
B runShowCommand() 0 27 4
A getGlobalVariables() 0 4 1
C detectDbSettings() 3 37 8
B dsn() 0 27 3
A mysqlUserHasPrivilege() 0 15 4
A getDbSettings() 0 4 1
A getIsSocketConnect() 0 4 1
A getName() 0 4 1
A getGlobalStatus() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DatabaseHelper 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 DatabaseHelper, and based on these observations, apply Extract Interface, too.

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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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
1 ignored issue
show
Documentation introduced by
Should the type for parameter $output not be null|OutputInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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]
0 ignored issues
show
Documentation introduced by
Should the type for parameter $variable not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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]
0 ignored issues
show
Documentation introduced by
Should the type for parameter $variable not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
515
     *
516
     * @return array
517
     */
518
    public function getGlobalStatus($variable = null)
519
    {
520
        return $this->runShowCommand('STATUS', $variable);
521
    }
522
}
523