Completed
Push — develop ( 4b1080...1cf9a9 )
by Tom
04:09
created

DatabaseHelper::setConnectionType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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