Completed
Push — develop ( 21139f...eaa140 )
by Tom
12s
created

DatabaseHelper   C

Complexity

Total Complexity 70

Size/Duplication

Total Lines 582
Duplicated Lines 2.41 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 5
Bugs 4 Features 0
Metric Value
wmc 70
c 5
b 4
f 0
lcom 1
cbo 4
dl 14
loc 582
rs 5.6163

24 Methods

Rating   Name   Duplication   Size   Complexity  
C resolveTables() 0 52 9
A dropTables() 0 13 2
B detectDbSettings() 0 24 4
B getMysqlVariable() 0 33 5
B getTables() 0 42 5
A fallbackOutput() 0 18 4
A getConnection() 0 8 2
A dsn() 0 4 1
A mysqlUserHasPrivilege() 0 15 4
A getMysqlClientToolConnectionString() 0 4 1
A getMysqlVariableValue() 0 14 3
B getTableDefinitions() 0 25 6
A throwRuntimeException() 0 14 3
A quoteLike() 0 10 1
B getTablesStatus() 0 28 5
A getDbSettings() 0 16 3
A getIsSocketConnect() 0 4 1
A getName() 0 4 1
A dropDatabase() 7 7 1
A createDatabase() 7 7 1
B runShowCommand() 0 29 4
A getGlobalVariables() 0 4 1
A getGlobalStatus() 0 4 1
A getApplication() 0 12 2

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 InvalidArgumentException;
6
use N98\Magento\Application;
7
use N98\Magento\DbSettings;
8
use PDO;
9
use PDOStatement;
10
use RuntimeException;
11
use Symfony\Component\Console\Application as BaseApplication;
12
use Symfony\Component\Console\Helper\Helper as AbstractHelper;
13
use Symfony\Component\Console\Output\NullOutput;
14
use Symfony\Component\Console\Output\OutputInterface;
15
16
/**
17
 * Class DatabaseHelper
18
 *
19
 * @package N98\Util\Console\Helper
20
 */
21
class DatabaseHelper extends AbstractHelper
22
{
23
    /**
24
     * @var array|DbSettings
25
     */
26
    protected $dbSettings = null;
27
28
    /**
29
     * @var bool
30
     * @deprecated since 1.97.9, use $dbSettings->isSocketConnect()
31
     */
32
    protected $isSocketConnect = false;
33
34
    /**
35
     * @var PDO
36
     */
37
    protected $_connection = null;
38
39
    /**
40
     * @var array
41
     */
42
    protected $_tables;
43
44
    /**
45
     * @param OutputInterface $output
46
     *
47
     * @throws RuntimeException
48
     */
49
    public function detectDbSettings(OutputInterface $output)
50
    {
51
        if (null !== $this->dbSettings) {
52
            return;
53
        }
54
55
        $application = $this->getApplication();
56
        $application->detectMagento();
57
58
        $configFile = $application->getMagentoRootFolder() . '/app/etc/local.xml';
59
60
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
61
            $output->writeln(
62
                sprintf('<debug>Loading database configuration from file <info>%s</info></debug>', $configFile)
63
            );
64
        }
65
66
        try {
67
            $this->dbSettings = new DbSettings($configFile);
68
        } catch (InvalidArgumentException $e) {
69
            $output->writeln('<error>' . $e->getMessage() . '</error>');
70
            throw new RuntimeException('Failed to load database settings from config file', 0, $e);
71
        }
72
    }
73
74
    /**
75
     * Connects to the database without initializing magento
76
     *
77
     * @param OutputInterface $output = null
78
     *
79
     * @return PDO
80
     */
81
    public function getConnection(OutputInterface $output = null)
82
    {
83
        if (!$this->_connection) {
84
            $this->_connection = $this->getDbSettings($output)->getConnection();
85
        }
86
87
        return $this->_connection;
88
    }
89
90
    /**
91
     * Creates a PDO DSN for the adapter from $this->_config settings.
92
     *
93
     * @see Zend_Db_Adapter_Pdo_Abstract
94
     * @return string
95
     */
96
    public function dsn()
97
    {
98
        return $this->getDbSettings()->getDsn();
99
    }
100
101
    /**
102
     * Check whether current mysql user has $privilege privilege
103
     *
104
     * @param string $privilege
105
     *
106
     * @return bool
107
     */
108
    public function mysqlUserHasPrivilege($privilege)
109
    {
110
        $statement = $this->getConnection()->query('SHOW GRANTS');
111
112
        $result = $statement->fetchAll(PDO::FETCH_COLUMN);
113
        foreach ($result as $row) {
114
            if (preg_match('/^GRANT(.*)' . strtoupper($privilege) . '/', $row)
115
                || preg_match('/^GRANT(.*)ALL/', $row)
116
            ) {
117
                return true;
118
            }
119
        }
120
121
        return false;
122
    }
123
124
    /**
125
     * @return string
126
     */
127
    public function getMysqlClientToolConnectionString()
128
    {
129
        return $this->getDbSettings()->getMysqlClientToolConnectionString();
130
    }
131
132
    /**
133
     * Get mysql variable value
134
     *
135
     * @param string $variable
136
     *
137
     * @return bool|array returns array on success, false on failure
138
     */
139
    public function getMysqlVariableValue($variable)
140
    {
141
        $statement = $this->getConnection()->query("SELECT @@{$variable};");
142
        if (false === $statement) {
143
            throw new RuntimeException(sprintf('Failed to query mysql variable %s', var_export($variable, 1)));
144
        }
145
146
        $result = $statement->fetch(PDO::FETCH_ASSOC);
147
        if ($result) {
148
            return $result;
149
        }
150
151
        return false;
152
    }
153
154
    /**
155
     * obtain mysql variable value from the database connection.
156
     *
157
     * in difference to @see getMysqlVariableValue(), this method allows to specify the type of the variable as well
158
     * as to use any variable identifier even such that need quoting.
159
     *
160
     * @param string $name mysql variable name
161
     * @param string $type [optional] variable type, can be a system variable ("@@", default) or a session variable
162
     *                     ("@").
163
     *
164
     * @return string variable value, null if variable was not defined
165
     * @throws RuntimeException in case a system variable is unknown (SQLSTATE[HY000]: 1193: Unknown system variable
166
     *                          'nonexistent')
167
     */
168
    public function getMysqlVariable($name, $type = null)
169
    {
170
        if (null === $type) {
171
            $type = "@@";
172
        } else {
173
            $type = (string) $type;
174
        }
175
176
        if (!in_array($type, array("@@", "@"), true)) {
177
            throw new InvalidArgumentException(
178
                sprintf('Invalid mysql variable type "%s", must be "@@" (system) or "@" (session)', $type)
179
            );
180
        }
181
182
        $quoted = '`' . strtr($name, array('`' => '``')) . '`';
183
        $query = "SELECT {$type}{$quoted};";
184
185
        $connection = $this->getConnection();
186
        $statement = $connection->query($query, PDO::FETCH_COLUMN, 0);
187
        if ($statement instanceof PDOStatement) {
188
            $result = $statement->fetchColumn(0);
189
        } else {
190
            $reason = $connection->errorInfo()
191
                ? vsprintf('SQLSTATE[%s]: %s: %s', $connection->errorInfo())
192
                : 'no error info';
193
194
            throw new RuntimeException(
195
                sprintf('Failed to query mysql variable %s: %s', var_export($name, true), $reason)
196
            );
197
        }
198
199
        return $result;
200
    }
201
202
    /**
203
     * @param array $commandConfig
204
     *
205
     * @return array
206
     */
207
    public function getTableDefinitions(array $commandConfig)
208
    {
209
        $tableDefinitions = array();
210
        if (isset($commandConfig['table-groups'])) {
211
            $tableGroups = $commandConfig['table-groups'];
212
            foreach ($tableGroups as $index => $definition) {
213
                $description = isset($definition['description']) ? $definition['description'] : '';
214
                if (!isset($definition['id'])) {
215
                    throw new RuntimeException('Invalid definition of table-groups (id missing) Index: ' . $index);
216
                }
217
                if (!isset($definition['tables'])) {
218
                    throw new RuntimeException('Invalid definition of table-groups (tables missing) Id: '
219
                        . $definition['id']
220
                    );
221
                }
222
223
                $tableDefinitions[$definition['id']] = array(
224
                    'tables'      => $definition['tables'],
225
                    'description' => $description,
226
                );
227
            }
228
        }
229
230
        return $tableDefinitions;
231
    }
232
233
    /**
234
     * @param array $list
235
     * @param array $definitions
236
     * @param array $resolved Which definitions where already resolved -> prevent endless loops
237
     *
238
     * @return array
239
     * @throws RuntimeException
240
     */
241
    public function resolveTables(array $list, array $definitions = array(), array $resolved = array())
242
    {
243
        if ($this->_tables === null) {
244
            $this->_tables = $this->getTables(true);
245
        }
246
247
        $resolvedList = array();
248
        foreach ($list as $entry) {
249
            if (substr($entry, 0, 1) == '@') {
250
                $code = substr($entry, 1);
251
                if (!isset($definitions[$code])) {
252
                    throw new RuntimeException('Table-groups could not be resolved: ' . $entry);
253
                }
254
                if (!isset($resolved[$code])) {
255
                    $resolved[$code] = true;
256
                    $tables = $this->resolveTables(
257
                        explode(' ', $definitions[$code]['tables']),
258
                        $definitions,
259
                        $resolved
260
                    );
261
                    $resolvedList = array_merge($resolvedList, $tables);
262
                }
263
                continue;
264
            }
265
266
            // resolve wildcards
267
            if (strpos($entry, '*') !== false) {
268
                $connection = $this->getConnection();
269
                $sth = $connection->prepare(
270
                    'SHOW TABLES LIKE :like',
271
                    array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
272
                );
273
                $sth->execute(
274
                    array(':like' => str_replace('*', '%', $this->dbSettings['prefix'] . $entry))
275
                );
276
                $rows = $sth->fetchAll();
277
                foreach ($rows as $row) {
278
                    $resolvedList[] = $row[0];
279
                }
280
                continue;
281
            }
282
283
            if (in_array($entry, $this->_tables)) {
284
                $resolvedList[] = $this->dbSettings['prefix'] . $entry;
285
            }
286
        }
287
288
        asort($resolvedList);
289
        $resolvedList = array_unique($resolvedList);
290
291
        return $resolvedList;
292
    }
293
294
    /**
295
     * Get list of database tables
296
     *
297
     * @param bool $withoutPrefix [optional] remove prefix from the returned table names. prefix is obtained from
298
     *                            magento database configuration. defaults to false.
299
     *
300
     * @return array
301
     * @throws RuntimeException
302
     */
303
    public function getTables($withoutPrefix = null)
304
    {
305
        $withoutPrefix = (bool) $withoutPrefix;
306
307
        $db = $this->getConnection();
308
        $prefix = $this->dbSettings['prefix'];
309
        $length = strlen($prefix);
310
311
        $columnName = 'table_name';
312
        $column = $columnName;
313
314
        $input = array();
315
316
        if ($withoutPrefix && $length) {
317
            $column = sprintf('SUBSTRING(%1$s FROM 1 + CHAR_LENGTH(:name)) %1$s', $columnName);
318
            $input[':name'] = $prefix;
319
        }
320
321
        $condition = 'table_schema = database()';
322
323
        if ($length) {
324
            $escape = '=';
325
            $condition .= sprintf(" AND %s LIKE :like ESCAPE '%s'", $columnName, $escape);
326
            $input[':like'] = $this->quoteLike($prefix, $escape) . '%';
327
        }
328
329
        $query = sprintf('SELECT %s FROM information_schema.tables WHERE %s;', $column, $condition);
330
        $statement = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
331
        $result = $statement->execute($input);
332
333
        if (!$result) {
334
            // @codeCoverageIgnoreStart
335
            $this->throwRuntimeException(
336
                $statement,
337
                sprintf('Failed to obtain tables from database: %s', var_export($query, true))
338
            );
339
        } // @codeCoverageIgnoreEnd
340
341
        $result = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
342
343
        return $result;
344
    }
345
346
    /**
347
     * throw a runtime exception and provide error info for the statement if available
348
     *
349
     * @param PDOStatement $statement
350
     * @param string $message
351
     *
352
     * @throws RuntimeException
353
     */
354
    private function throwRuntimeException(PDOStatement $statement, $message = "")
355
    {
356
        $reason = $statement->errorInfo()
357
            ? vsprintf('SQLSTATE[%s]: %s: %s', $statement->errorInfo())
358
            : 'no error info for statement';
359
360
        if (strlen($message)) {
361
            $message .= ': ';
362
        } else {
363
            $message = '';
364
        }
365
366
        throw new RuntimeException($message . $reason);
367
    }
368
369
    /**
370
     * quote a string so that it is safe to use in a LIKE
371
     *
372
     * @param string $string
373
     * @param string $escape character - single us-ascii character
374
     *
375
     * @return string
376
     */
377
    private function quoteLike($string, $escape = '=')
378
    {
379
        $translation = array(
380
            $escape => $escape . $escape,
381
            '%'     => $escape . '%',
382
            '_'     => $escape . '_',
383
        );
384
385
        return strtr($string, $translation);
386
    }
387
388
    /**
389
     * Get list of db tables status
390
     *
391
     * @param bool $withoutPrefix
392
     *
393
     * @return array
394
     */
395
    public function getTablesStatus($withoutPrefix = false)
396
    {
397
        $db = $this->getConnection();
398
        $prefix = $this->dbSettings['prefix'];
399
        if (strlen($prefix) > 0) {
400
            $statement = $db->prepare('SHOW TABLE STATUS LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
401
            $statement->execute(
402
                array(':like' => $prefix . '%')
403
            );
404
        } else {
405
            $statement = $db->query('SHOW TABLE STATUS');
406
        }
407
408
        if ($statement) {
409
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
410
            $return = array();
411
            foreach ($result as $table) {
412
                if (true === $withoutPrefix) {
413
                    $table['Name'] = str_replace($prefix, '', $table['Name']);
414
                }
415
                $return[$table['Name']] = $table;
416
            }
417
418
            return $return;
419
        }
420
421
        return array();
422
    }
423
424
    /**
425
     * @param OutputInterface $output [optional]
426
     *
427
     * @return array|DbSettings
428
     */
429
    public function getDbSettings(OutputInterface $output = null)
430
    {
431
        if ($this->dbSettings) {
432
            return $this->dbSettings;
433
        }
434
435
        $output = $this->fallbackOutput($output);
436
437
        $this->detectDbSettings($output);
438
439
        if (!$this->dbSettings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->dbSettings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
440
            throw new RuntimeException('Database settings fatal error');
441
        }
442
443
        return $this->dbSettings;
444
    }
445
446
    /**
447
     * @return boolean
448
     */
449
    public function getIsSocketConnect()
450
    {
451
        return $this->getDbSettings()->isSocketConnect();
452
    }
453
454
    /**
455
     * Returns the canonical name of this helper.
456
     *
457
     * @return string The canonical name
458
     *
459
     * @api
460
     */
461
    public function getName()
462
    {
463
        return 'database';
464
    }
465
466
    /**
467
     * @param OutputInterface $output
468
     */
469 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...
470
    {
471
        $this->detectDbSettings($output);
472
        $db = $this->getConnection();
473
        $db->query('DROP DATABASE `' . $this->dbSettings['dbname'] . '`');
474
        $output->writeln('<info>Dropped database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
475
    }
476
477
    /**
478
     * @param OutputInterface $output
479
     */
480
    public function dropTables($output)
481
    {
482
        $result = $this->getTables();
483
        $query = 'SET FOREIGN_KEY_CHECKS = 0; ';
484
        $count = 0;
485
        foreach ($result as $tableName) {
486
            $query .= 'DROP TABLE IF EXISTS `' . $tableName . '`; ';
487
            $count++;
488
        }
489
        $query .= 'SET FOREIGN_KEY_CHECKS = 1;';
490
        $this->getConnection()->query($query);
491
        $output->writeln('<info>Dropped database tables</info> <comment>' . $count . ' tables dropped</comment>');
492
    }
493
494
    /**
495
     * @param OutputInterface $output
496
     */
497 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...
498
    {
499
        $this->detectDbSettings($output);
500
        $db = $this->getConnection();
501
        $db->query('CREATE DATABASE IF NOT EXISTS `' . $this->dbSettings['dbname'] . '`');
502
        $output->writeln('<info>Created database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
503
    }
504
505
    /**
506
     * @param string $command example: 'VARIABLES', 'STATUS'
507
     * @param string|null $variable [optional]
508
     *
509
     * @return array
510
     */
511
    private function runShowCommand($command, $variable = null)
512
    {
513
        $db = $this->getConnection();
514
515
        if (null !== $variable) {
516
            $statement = $db->prepare(
517
                'SHOW /*!50000 GLOBAL */ ' . $command . ' LIKE :like',
518
                array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
519
            );
520
            $statement->execute(
521
                array(':like' => $variable)
522
            );
523
        } else {
524
            $statement = $db->query('SHOW /*!50000 GLOBAL */ ' . $command);
525
        }
526
527
        if ($statement) {
528
            /** @var array|string[] $result */
529
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
530
            $return = array();
531
            foreach ($result as $row) {
532
                $return[$row['Variable_name']] = $row['Value'];
533
            }
534
535
            return $return;
536
        }
537
538
        return array();
539
    }
540
541
    /**
542
     * @param string|null $variable [optional]
543
     *
544
     * @return array
545
     */
546
    public function getGlobalVariables($variable = null)
547
    {
548
        return $this->runShowCommand('VARIABLES', $variable);
549
    }
550
551
    /**
552
     * @param string|null $variable [optional]
553
     *
554
     * @return array
555
     */
556
    public function getGlobalStatus($variable = null)
557
    {
558
        return $this->runShowCommand('STATUS', $variable);
559
    }
560
561
    /**
562
     * @return Application|BaseApplication
563
     */
564
    private function getApplication()
565
    {
566
        $command = $this->getHelperSet()->getCommand();
567
568
        if ($command) {
569
            $application = $command->getApplication();
570
        } else {
571
            $application = new Application();
572
        }
573
574
        return $application;
575
    }
576
577
    /**
578
     * small helper method to obtain an object of type OutputInterface
579
     *
580
     * @param OutputInterface|null $output
581
     *
582
     * @return OutputInterface
583
     */
584
    private function fallbackOutput(OutputInterface $output = null)
585
    {
586
        if (null !== $output) {
587
            return $output;
588
        }
589
590
        if ($this->getHelperSet()->has('io')) {
591
            /** @var $helper IoHelper */
592
            $helper = $this->getHelperSet()->get('io');
593
            $output = $helper->getOutput();
594
        }
595
596
        if (null === $output) {
597
            $output = new NullOutput();
598
        }
599
600
        return $output;
601
    }
602
}
603