Completed
Push — bugfix/sales-order-status-stri... ( fd0fa8...b19ab6 )
by Tom
09:03
created

DatabaseHelper::getTableDefinitions()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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