Completed
Push — develop ( 2134af...ee2042 )
by Tom
03:34
created

DatabaseHelper::detectDbSettings()   D

Complexity

Conditions 9
Paths 19

Size

Total Lines 40
Code Lines 22

Duplication

Lines 6
Ratio 15 %

Importance

Changes 0
Metric Value
dl 6
loc 40
rs 4.909
c 0
b 0
f 0
cc 9
eloc 22
nc 19
nop 1
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
     * @var string
36
     */
37
    private $connectionType = 'default';
38
39
    /**
40
     * Set connection type when several db used.
41
     *
42
     * @param $connectionType
43
     */
44
    public function setConnectionType($connectionType)
45
    {
46
        $this->connectionType = $connectionType;
47
    }
48
49
    /**
50
     * @param OutputInterface $output
51
     *
52
     * @throws RuntimeException
53
     * @return void
54
     */
55
    public function detectDbSettings(OutputInterface $output)
56
    {
57
        if ($this->dbSettings !== null) {
58
            return;
59
        }
60
61
        $magentoHelper = $this->getHelperSet()->getCommand()->getHelper('magento');
62
        $config = $magentoHelper->getBaseConfig(); // @TODO Use \Magento\Framework\App\DeploymentConfig ?
63
64
        if (!isset($config['db'])) {
65
            $output->writeln('<error>DB settings was not found in app/etc/env.php file</error>');
66
            return;
67
        }
68
69
        if (!isset($config['db']['connection'][$this->connectionType])) {
70
            throw new RuntimeException(sprintf('Cannot find "%s" connection config in app/etc/env.php', $this->connectionType));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
71
        }
72
73
        $this->dbSettings = (array) $config['db']['connection'][$this->connectionType];
74
75
        $this->dbSettings['prefix'] = '';
76
        if (isset($config['db']['table_prefix'])) {
77
            $this->dbSettings['prefix'] = (string) $config['db']['table_prefix'];
78
        }
79
80 View Code Duplication
        if (
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...
81
            isset($this->dbSettings['host'])
82
            && strpos($this->dbSettings['host'], ':') !== false
83
        ) {
84
            list($this->dbSettings['host'], $this->dbSettings['port']) = explode(':', $this->dbSettings['host']);
85
        }
86
87
        if (isset($this->dbSettings['comment'])) {
88
            unset($this->dbSettings['comment']);
89
        }
90
91
        if (isset($this->dbSettings['unix_socket'])) {
92
            $this->isSocketConnect = true;
93
        }
94
    }
95
96
    /**
97
     * Connects to the database without initializing magento
98
     *
99
     * @param OutputInterface $output = null
0 ignored issues
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...
100
     *
101
     * @return PDO
102
     * @throws RuntimeException pdo mysql extension is not installed
103
     */
104
    public function getConnection(OutputInterface $output = null)
105
    {
106
        if ($output == null) {
107
            $output = new NullOutput();
108
        }
109
110
        if ($this->_connection) {
111
            return $this->_connection;
112
        }
113
114
        $this->detectDbSettings($output);
115
116
        if (!extension_loaded('pdo_mysql')) {
117
            throw new RuntimeException('pdo_mysql extension is not installed');
118
        }
119
120
        if (strpos($this->dbSettings['host'], '/') !== false) {
121
            $this->dbSettings['unix_socket'] = $this->dbSettings['host'];
122
            unset($this->dbSettings['host']);
123 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...
124
            list($this->dbSettings['host'], $this->dbSettings['port']) = explode(':', $this->dbSettings['host']);
125
        }
126
127
        $this->_connection = new PDO(
128
            $this->dsn(),
129
            $this->dbSettings['username'],
130
            $this->dbSettings['password']
131
        );
132
133
        /** @link http://bugs.mysql.com/bug.php?id=18551 */
134
        $this->_connection->query("SET SQL_MODE=''");
135
136
        try {
137
            $this->_connection->query('USE `' . $this->dbSettings['dbname'] . '`');
138
        } catch (PDOException $e) {
139
            if (OutputInterface::VERBOSITY_VERY_VERBOSE <= $output->getVerbosity()) {
140
                $output->writeln(sprintf(
141
                    '<error>Failed to use database <comment>%s</comment>: %s</error>',
142
                    var_export($this->dbSettings['dbname'], true),
143
                    $e->getMessage()
144
                ));
145
            }
146
        }
147
148
        $this->_connection->query("SET NAMES utf8");
149
150
        $this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
151
        $this->_connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
152
153
        return $this->_connection;
154
    }
155
156
    /**
157
     * Creates a PDO DSN for the adapter from $this->_config settings.
158
     *
159
     * @see Zend_Db_Adapter_Pdo_Abstract
160
     * @return string
161
     */
162
    public function dsn()
163
    {
164
        $this->detectDbSettings(new NullOutput());
165
166
        // baseline of DSN parts
167
        $dsn = $this->dbSettings;
168
169
        // don't pass the username, password, charset, database, persistent and driver_options in the DSN
170
        unset($dsn['username']);
171
        unset($dsn['password']);
172
        unset($dsn['options']);
173
        unset($dsn['charset']);
174
        unset($dsn['persistent']);
175
        unset($dsn['driver_options']);
176
        unset($dsn['dbname']);
177
178
        // use all remaining parts in the DSN
179
        $buildDsn = array();
180
        foreach ($dsn as $key => $val) {
181
            if (is_array($val)) {
182
                continue;
183
            }
184
            $buildDsn[$key] = "$key=$val";
185
        }
186
187
        return 'mysql:' . implode(';', $buildDsn);
188
    }
189
190
    /**
191
     * Check whether current mysql user has $privilege privilege
192
     *
193
     * @param string $privilege
194
     *
195
     * @return bool
196
     */
197
    public function mysqlUserHasPrivilege($privilege)
198
    {
199
        $statement = $this->getConnection()->query('SHOW GRANTS');
200
201
        $result = $statement->fetchAll(PDO::FETCH_COLUMN);
202
        foreach ($result as $row) {
203
            if (preg_match('/^GRANT(.*)' . strtoupper($privilege) . '/', $row)
204
                || preg_match('/^GRANT(.*)ALL/', $row)
205
            ) {
206
                return true;
207
            }
208
        }
209
210
        return false;
211
    }
212
213
    /**
214
     * @return string
215
     */
216
    public function getMysqlClientToolConnectionString()
217
    {
218
        $this->detectDbSettings(new NullOutput());
219
220
        if ($this->isSocketConnect) {
221
            $string = '--socket=' . escapeshellarg($this->dbSettings['unix_socket']);
222
        } else {
223
            $string = '-h' . escapeshellarg($this->dbSettings['host']);
224
        }
225
226
        $string .= ' '
227
            . '-u' . escapeshellarg($this->dbSettings['username'])
228
            . ' '
229
            . (isset($this->dbSettings['port'])
230
                ? '-P' . escapeshellarg($this->dbSettings['port']) . ' ' : '')
231
            . (strlen($this->dbSettings['password'])
232
                ? '--password=' . escapeshellarg($this->dbSettings['password']) . ' ' : '')
233
            . escapeshellarg($this->dbSettings['dbname']);
234
235
        return $string;
236
    }
237
238
    /**
239
     * Get mysql variable value
240
     *
241
     * @param string $variable
242
     *
243
     * @return bool|string
244
     */
245
    public function getMysqlVariableValue($variable)
246
    {
247
        $statement = $this->getConnection()->query("SELECT @@{$variable};");
248
        $result = $statement->fetch(PDO::FETCH_ASSOC);
249
        if ($result) {
250
            return $result;
251
        }
252
253
        return false;
254
    }
255
256
    /**
257
     * @param array $commandConfig
258
     *
259
     * @throws \Exception
260
     * @internal param $config
261
     * @return array $commandConfig
262
     * @return array
263
     */
264
    public function getTableDefinitions(array $commandConfig)
265
    {
266
        $tableDefinitions = array();
267
        if (!isset($commandConfig['table-groups'])) {
268
            return $tableDefinitions;
269
        }
270
271
        $tableGroups = $commandConfig['table-groups'];
272
        foreach ($tableGroups as $index => $definition) {
273
            if (!isset($definition['id'])) {
274
                throw new RuntimeException("Invalid definition of table-groups (id missing) at index: $index");
275
            }
276
            $id = $definition['id'];
277
            if (isset($definitions[$id])) {
0 ignored issues
show
Bug introduced by
The variable $definitions does not exist. Did you mean $tableDefinitions?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
278
                throw new RuntimeException("Invalid definition of table-groups (duplicate id) id: $id");
279
            }
280
281
            if (!isset($definition['tables'])) {
282
                throw new RuntimeException("Invalid definition of table-groups (tables missing) id: $id");
283
            }
284
            $tables = $definition['tables'];
285
286
            if (is_string($tables)) {
287
                $tables = preg_split('~\s+~', $tables, -1, PREG_SPLIT_NO_EMPTY);
288
            }
289
            if (!is_array($tables)) {
290
                throw new RuntimeException("Invalid tables definition of table-groups id: $id");
291
            }
292
            $tables = array_map('trim', $tables);
293
294
            $description = isset($definition['description']) ? $definition['description'] : '';
295
296
            $tableDefinitions[$id] = array(
297
                'tables'      => $tables,
298
                'description' => $description,
299
            );
300
        }
301
302
        return $tableDefinitions;
303
    }
304
305
    /**
306
     * @param array $list
307
     * @param array $definitions
308
     * @param array $resolved Which definitions where already resolved -> prevent endless loops
309
     *
310
     * @return array
311
     * @throws RuntimeException
312
     */
313
    public function resolveTables(array $list, array $definitions = array(), array $resolved = array())
314
    {
315
        if ($this->_tables === null) {
316
            $this->_tables = $this->getTables(true);
317
        }
318
319
        $resolvedList = array();
320
        foreach ($list as $entry) {
321
            if (substr($entry, 0, 1) == '@') {
322
                $code = substr($entry, 1);
323
                if (!isset($definitions[$code])) {
324
                    throw new RuntimeException('Table-groups could not be resolved: ' . $entry);
325
                }
326
                if (!isset($resolved[$code])) {
327
                    $resolved[$code] = true;
328
                    $tables = $this->resolveTables(
329
                        $definitions[$code]['tables'],
330
                        $definitions,
331
                        $resolved
332
                    );
333
                    $resolvedList = array_merge($resolvedList, $tables);
334
                }
335
                continue;
336
            }
337
338
            // resolve wildcards
339
            if (strpos($entry, '*') !== false) {
340
                $connection = $this->getConnection();
341
                $sth = $connection->prepare(
342
                    'SHOW TABLES LIKE :like',
343
                    array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
344
                );
345
                $sth->execute(
346
                    array(':like' => str_replace('*', '%', $this->dbSettings['prefix'] . $entry))
347
                );
348
                $rows = $sth->fetchAll();
349
                foreach ($rows as $row) {
350
                    $resolvedList[] = $row[0];
351
                }
352
                continue;
353
            }
354
355
            if (in_array($entry, $this->_tables)) {
356
                $resolvedList[] = $this->dbSettings['prefix'] . $entry;
357
            }
358
        }
359
360
        asort($resolvedList);
361
        $resolvedList = array_unique($resolvedList);
362
363
        return $resolvedList;
364
    }
365
366
    /**
367
     * Get list of db tables
368
     *
369
     * @param bool $withoutPrefix
370
     *
371
     * @return array
372
     */
373
    public function getTables($withoutPrefix = false)
374
    {
375
        $db = $this->getConnection();
376
        $prefix = $this->dbSettings['prefix'];
377 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...
378
            $statement = $db->prepare('SHOW TABLES LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
379
            $statement->execute(
380
                array(':like' => $prefix . '%')
381
            );
382
        } else {
383
            $statement = $db->query('SHOW TABLES');
384
        }
385
386
        if ($statement) {
387
            $result = $statement->fetchAll(PDO::FETCH_COLUMN);
388
            if ($withoutPrefix === false) {
389
                return $result;
390
            }
391
392
            return array_map(function ($tableName) use ($prefix) {
393
                return str_replace($prefix, '', $tableName);
394
            }, $result);
395
        }
396
397
        return array();
398
    }
399
400
    /**
401
     * Get list of db tables status
402
     *
403
     * @param bool $withoutPrefix
404
     *
405
     * @return array
406
     */
407
    public function getTablesStatus($withoutPrefix = false)
408
    {
409
        $db = $this->getConnection();
410
        $prefix = $this->dbSettings['prefix'];
411 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...
412
            $statement = $db->prepare('SHOW TABLE STATUS LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
413
            $statement->execute(
414
                array(':like' => $prefix . '%')
415
            );
416
        } else {
417
            $statement = $db->query('SHOW TABLE STATUS');
418
        }
419
420
        if ($statement) {
421
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
422
            $return = array();
423
            foreach ($result as $table) {
424
                if (true === $withoutPrefix) {
425
                    $table['Name'] = str_replace($prefix, '', $table['Name']);
426
                }
427
                $return[$table['Name']] = $table;
428
            }
429
            return $return;
430
        }
431
432
        return array();
433
    }
434
435
    /**
436
     * @return array
437
     */
438
    public function getDbSettings()
439
    {
440
        return $this->dbSettings;
441
    }
442
443
    /**
444
     * @return boolean
445
     */
446
    public function getIsSocketConnect()
447
    {
448
        return $this->isSocketConnect;
449
    }
450
451
    /**
452
     * Returns the canonical name of this helper.
453
     *
454
     * @return string The canonical name
455
     *
456
     * @api
457
     */
458
    public function getName()
459
    {
460
        return 'database';
461
    }
462
463
    /**
464
     * @param OutputInterface $output
465
     */
466 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...
467
    {
468
        $this->detectDbSettings($output);
469
        $db = $this->getConnection();
470
        $db->query('DROP DATABASE `' . $this->dbSettings['dbname'] . '`');
471
        $output->writeln('<info>Dropped database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
472
    }
473
474
    /**
475
     * @param OutputInterface $output
476
     */
477
    public function dropTables($output)
478
    {
479
        $result = $this->getTables();
480
        $query = 'SET FOREIGN_KEY_CHECKS = 0; ';
481
        $count = 0;
482
        foreach ($result as $tableName) {
483
            $query .= 'DROP TABLE IF EXISTS `' . $tableName . '`; ';
484
            $count++;
485
        }
486
        $query .= 'SET FOREIGN_KEY_CHECKS = 1;';
487
        $this->getConnection()->query($query);
488
        $output->writeln('<info>Dropped database tables</info> <comment>' . $count . ' tables dropped</comment>');
489
    }
490
491
    /**
492
     * @param OutputInterface $output
493
     */
494 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...
495
    {
496
        $this->detectDbSettings($output);
497
        $db = $this->getConnection();
498
        $db->query('CREATE DATABASE IF NOT EXISTS `' . $this->dbSettings['dbname'] . '`');
499
        $output->writeln('<info>Created database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
500
    }
501
502
    /**
503
     * @param string $command  example: 'VARIABLES', 'STATUS'
504
     * @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...
505
     *
506
     * @return array
507
     */
508
    private function runShowCommand($command, $variable = null)
509
    {
510
        $db = $this->getConnection();
511
512
        if (null !== $variable) {
513
            $statement = $db->prepare(
514
                'SHOW /*!50000 GLOBAL */ ' . $command . ' LIKE :like',
515
                array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
516
            );
517
            $statement->execute(
518
                array(':like' => $variable)
519
            );
520
        } else {
521
            $statement = $db->query('SHOW /*!50000 GLOBAL */ ' . $command);
522
        }
523
524
        if ($statement) {
525
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
526
            $return = array();
527
            foreach ($result as $row) {
528
                $return[$row['Variable_name']] = $row['Value'];
529
            }
530
            return $return;
531
        }
532
533
        return array();
534
    }
535
536
    /**
537
     * @param string|null $variable [optional]
538
     *
539
     * @return array
540
     */
541
    public function getGlobalVariables($variable = null)
542
    {
543
        return $this->runShowCommand('VARIABLES', $variable);
544
    }
545
546
    /**
547
     * @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...
548
     *
549
     * @return array
550
     */
551
    public function getGlobalStatus($variable = null)
552
    {
553
        return $this->runShowCommand('STATUS', $variable);
554
    }
555
}
556