Completed
Pull Request — develop (#243)
by Tom
20:40 queued 07:41
created

DatabaseHelper::getTableDefinitions()   D

Complexity

Conditions 9
Paths 11

Size

Total Lines 40
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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