Completed
Push — master ( f5a8cf...dc6729 )
by Christian
07:18
created

resolveRetrieveDefinitionsTablesByCode()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 4
nop 2
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
     * @return void
49
     */
50
    public function detectDbSettings(OutputInterface $output)
51
    {
52
        if (null !== $this->dbSettings) {
53
            return;
54
        }
55
56
        $application = $this->getApplication();
57
        $application->detectMagento();
58
59
        $configFile = $application->getMagentoRootFolder() . '/app/etc/local.xml';
60
61
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
62
            $output->writeln(
63
                sprintf('<debug>Loading database configuration from file <info>%s</info></debug>', $configFile)
64
            );
65
        }
66
67
        try {
68
            $this->dbSettings = new DbSettings($configFile);
69
        } catch (InvalidArgumentException $e) {
70
            $output->writeln('<error>' . $e->getMessage() . '</error>');
71
            throw new RuntimeException('Failed to load database settings from config file', 0, $e);
72
        }
73
    }
74
75
    /**
76
     * Connects to the database without initializing magento
77
     *
78
     * @param OutputInterface $output = null
79
     *
80
     * @return PDO
81
     */
82
    public function getConnection(OutputInterface $output = null)
83
    {
84
        if (!$this->_connection) {
85
            $this->_connection = $this->getDbSettings($output)->getConnection();
86
        }
87
88
        return $this->_connection;
89
    }
90
91
    /**
92
     * Creates a PDO DSN for the adapter from $this->_config settings.
93
     *
94
     * @see Zend_Db_Adapter_Pdo_Abstract
95
     * @return string
96
     */
97
    public function dsn()
98
    {
99
        return $this->getDbSettings()->getDsn();
100
    }
101
102
    /**
103
     * Check whether current mysql user has $privilege privilege
104
     *
105
     * @param string $privilege
106
     *
107
     * @return bool
108
     */
109
    public function mysqlUserHasPrivilege($privilege)
110
    {
111
        $statement = $this->getConnection()->query('SHOW GRANTS');
112
113
        $result = $statement->fetchAll(PDO::FETCH_COLUMN);
114
        foreach ($result as $row) {
115
            if (preg_match('/^GRANT(.*)' . strtoupper($privilege) . '/', $row)
116
                || preg_match('/^GRANT(.*)ALL/', $row)
117
            ) {
118
                return true;
119
            }
120
        }
121
122
        return false;
123
    }
124
125
    /**
126
     * @return string
127
     */
128
    public function getMysqlClientToolConnectionString()
129
    {
130
        return $this->getDbSettings()->getMysqlClientToolConnectionString();
131
    }
132
133
    /**
134
     * Get mysql variable value
135
     *
136
     * @param string $variable
137
     *
138
     * @return bool|array returns array on success, false on failure
139
     */
140
    public function getMysqlVariableValue($variable)
141
    {
142
        $statement = $this->getConnection()->query("SELECT @@{$variable};");
143
        if (false === $statement) {
144
            throw new RuntimeException(sprintf('Failed to query mysql variable %s', var_export($variable, 1)));
145
        }
146
147
        $result = $statement->fetch(PDO::FETCH_ASSOC);
148
        if ($result) {
149
            return $result;
150
        }
151
152
        return false;
153
    }
154
155
    /**
156
     * obtain mysql variable value from the database connection.
157
     *
158
     * in difference to @see getMysqlVariableValue(), this method allows to specify the type of the variable as well
159
     * as to use any variable identifier even such that need quoting.
160
     *
161
     * @param string $name mysql variable name
162
     * @param string $type [optional] variable type, can be a system variable ("@@", default) or a session variable
163
     *                     ("@").
164
     *
165
     * @return string variable value, null if variable was not defined
166
     * @throws RuntimeException in case a system variable is unknown (SQLSTATE[HY000]: 1193: Unknown system variable
167
     *                          'nonexistent')
168
     */
169
    public function getMysqlVariable($name, $type = null)
170
    {
171
        if (null === $type) {
172
            $type = "@@";
173
        } else {
174
            $type = (string) $type;
175
        }
176
177
        if (!in_array($type, array("@@", "@"), true)) {
178
            throw new InvalidArgumentException(
179
                sprintf('Invalid mysql variable type "%s", must be "@@" (system) or "@" (session)', $type)
180
            );
181
        }
182
183
        $quoted = '`' . strtr($name, array('`' => '``')) . '`';
184
        $query = "SELECT {$type}{$quoted};";
185
186
        $connection = $this->getConnection();
187
        $statement = $connection->query($query, PDO::FETCH_COLUMN, 0);
188
        if ($statement instanceof PDOStatement) {
189
            $result = $statement->fetchColumn(0);
190
        } else {
191
            $reason = $connection->errorInfo()
192
                ? vsprintf('SQLSTATE[%s]: %s: %s', $connection->errorInfo())
193
                : 'no error info';
194
195
            throw new RuntimeException(
196
                sprintf('Failed to query mysql variable %s: %s', var_export($name, true), $reason)
197
            );
198
        }
199
200
        return $result;
201
    }
202
203
    /**
204
     * @param array $commandConfig
205
     *
206
     * @throws RuntimeException
207
     * @return array
208
     */
209
    public function getTableDefinitions(array $commandConfig)
210
    {
211
        $tableDefinitions = array();
212
        if (!isset($commandConfig['table-groups'])) {
213
            return $tableDefinitions;
214
        }
215
216
        $tableGroups = $commandConfig['table-groups'];
217
        foreach ($tableGroups as $index => $definition) {
218
            if (!isset($definition['id'])) {
219
                throw new RuntimeException("Invalid definition of table-groups (id missing) at index: $index");
220
            }
221
            $id = $definition['id'];
222
            if (isset($tableDefinitions[$id])) {
223
                throw new RuntimeException("Invalid definition of table-groups (duplicate id) id: $id");
224
            }
225
226
            if (!isset($definition['tables'])) {
227
                throw new RuntimeException("Invalid definition of table-groups (tables missing) id: $id");
228
            }
229
            $tables = $definition['tables'];
230
231
            if (is_string($tables)) {
232
                $tables = preg_split('~\s+~', $tables, -1, PREG_SPLIT_NO_EMPTY);
233
            }
234
            if (!is_array($tables)) {
235
                throw new RuntimeException("Invalid tables definition of table-groups id: $id");
236
            }
237
            $tables = array_map('trim', $tables);
238
239
            $description = isset($definition['description']) ? $definition['description'] : '';
240
241
            $tableDefinitions[$id] = array(
242
                'tables'      => $tables,
243
                'description' => $description,
244
            );
245
        }
246
247
        return $tableDefinitions;
248
    }
249
250
    /**
251
     * @param array $list to resolve
252
     * @param array $definitions from to resolve
253
     * @param array $resolved Which definitions where already resolved -> prevent endless loops
254
     *
255
     * @return array
256
     * @throws RuntimeException
257
     */
258
    public function resolveTables(array $list, array $definitions = array(), array $resolved = array())
259
    {
260
        if ($this->_tables === null) {
261
            $this->_tables = $this->getTables(true);
262
        }
263
264
        $resolvedList = array();
265
        foreach ($list as $entry) {
266
            if (substr($entry, 0, 1) == '@') {
267
                $code = substr($entry, 1);
268
                if (!isset($definitions[$code])) {
269
                    throw new RuntimeException('Table-groups could not be resolved: ' . $entry);
270
                }
271
                if (!isset($resolved[$code])) {
272
                    $resolved[$code] = true;
273
                    $tables = $this->resolveTables(
274
                        $this->resolveRetrieveDefinitionsTablesByCode($definitions, $code),
275
                        $definitions,
276
                        $resolved
277
                    );
278
                    $resolvedList = array_merge($resolvedList, $tables);
279
                }
280
                continue;
281
            }
282
283
            // resolve wildcards
284
            if (strpos($entry, '*') !== false) {
285
                $connection = $this->getConnection();
286
                $sth = $connection->prepare(
287
                    'SHOW TABLES LIKE :like',
288
                    array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
289
                );
290
                $sth->execute(
291
                    array(':like' => str_replace('*', '%', $this->dbSettings['prefix'] . $entry))
292
                );
293
                $rows = $sth->fetchAll();
294
                foreach ($rows as $row) {
295
                    $resolvedList[] = $row[0];
296
                }
297
                continue;
298
            }
299
300
            if (in_array($entry, $this->_tables)) {
301
                $resolvedList[] = $this->dbSettings['prefix'] . $entry;
302
            }
303
        }
304
305
        asort($resolvedList);
306
        $resolvedList = array_unique($resolvedList);
307
308
        return $resolvedList;
309
    }
310
311
    /**
312
     * @param array $definitions
313
     * @param string $code
314
     * @return array tables
315
     */
316
    private function resolveRetrieveDefinitionsTablesByCode(array $definitions, $code)
317
    {
318
        $tables = $definitions[$code]['tables'];
319
320
        if (is_string($tables)) {
321
            $tables = preg_split('~\s+~', $tables, -1, PREG_SPLIT_NO_EMPTY);
322
        }
323
        if (!is_array($tables)) {
324
            throw new RuntimeException("Invalid tables definition of table-groups code: @$code");
325
        }
326
327
        $tables = array_reduce((array) $tables, array($this, 'resolveTablesArray'), null);
328
329
        return $tables;
330
    }
331
332
    /**
333
     * @param array|null $carry [optional]
334
     * @param $item [optional]
335
     * @return array
336
     * @throws InvalidArgumentException if item is not an array or string
337
     */
338
    private function resolveTablesArray(array $carry = null, $item = null)
339
    {
340
        if (is_string($item)) {
341
            $item = preg_split('~\s+~', $item, -1, PREG_SPLIT_NO_EMPTY);
342
        }
343
344
        if (is_array($item)) {
345
            if (count($item) > 1) {
346
                $item = array_reduce($item, array($this, 'resolveTablesArray'), (array) $carry);
347
            }
348
        } else {
349
            throw new InvalidArgumentException(sprintf('Unable to handle %s', var_export($item, true)));
350
        }
351
352
        return array_merge((array) $carry, $item);
353
    }
354
355
    /**
356
     * Get list of database tables
357
     *
358
     * @param bool $withoutPrefix [optional] remove prefix from the returned table names. prefix is obtained from
359
     *                            magento database configuration. defaults to false.
360
     *
361
     * @return array
362
     * @throws RuntimeException
363
     */
364
    public function getTables($withoutPrefix = null)
365
    {
366
        $withoutPrefix = (bool) $withoutPrefix;
367
368
        $db = $this->getConnection();
369
        $prefix = $this->dbSettings['prefix'];
370
        $prefixLength = strlen($prefix);
371
372
        $column = $columnName = 'table_name';
373
374
        $input = array();
375
376
        if ($withoutPrefix && $prefixLength) {
377
            $column = sprintf('SUBSTRING(%1$s FROM 1 + CHAR_LENGTH(:name)) %1$s', $columnName);
378
            $input[':name'] = $prefix;
379
        }
380
381
        $condition = 'table_schema = database()';
382
383
        if ($prefixLength) {
384
            $escape = '=';
385
            $condition .= sprintf(" AND %s LIKE :like ESCAPE '%s'", $columnName, $escape);
386
            $input[':like'] = $this->quoteLike($prefix, $escape) . '%';
387
        }
388
389
        $query = sprintf('SELECT %s FROM information_schema.tables WHERE %s;', $column, $condition);
390
        $statement = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
391
        $result = $statement->execute($input);
392
393
        if (!$result) {
394
            // @codeCoverageIgnoreStart
395
            $this->throwRuntimeException(
396
                $statement,
397
                sprintf('Failed to obtain tables from database: %s', var_export($query, true))
398
            );
399
        } // @codeCoverageIgnoreEnd
400
401
        $result = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
402
403
        return $result;
404
    }
405
406
    /**
407
     * throw a runtime exception and provide error info for the statement if available
408
     *
409
     * @param PDOStatement $statement
410
     * @param string $message
411
     *
412
     * @throws RuntimeException
413
     */
414
    private function throwRuntimeException(PDOStatement $statement, $message = "")
415
    {
416
        $reason = $statement->errorInfo()
417
            ? vsprintf('SQLSTATE[%s]: %s: %s', $statement->errorInfo())
418
            : 'no error info for statement';
419
420
        if (strlen($message)) {
421
            $message .= ': ';
422
        } else {
423
            $message = '';
424
        }
425
426
        throw new RuntimeException($message . $reason);
427
    }
428
429
    /**
430
     * quote a string so that it is safe to use in a LIKE
431
     *
432
     * @param string $string
433
     * @param string $escape character - single us-ascii character
434
     *
435
     * @return string
436
     */
437
    private function quoteLike($string, $escape = '=')
438
    {
439
        $translation = array(
440
            $escape => $escape . $escape,
441
            '%'     => $escape . '%',
442
            '_'     => $escape . '_',
443
        );
444
445
        return strtr($string, $translation);
446
    }
447
448
    /**
449
     * Get list of db tables status
450
     *
451
     * @param bool $withoutPrefix
452
     *
453
     * @return array
454
     */
455
    public function getTablesStatus($withoutPrefix = false)
456
    {
457
        $db = $this->getConnection();
458
        $prefix = $this->dbSettings['prefix'];
459
        if (strlen($prefix) > 0) {
460
            $statement = $db->prepare('SHOW TABLE STATUS LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
461
            $statement->execute(
462
                array(':like' => $prefix . '%')
463
            );
464
        } else {
465
            $statement = $db->query('SHOW TABLE STATUS');
466
        }
467
468
        if ($statement) {
469
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
470
            $return = array();
471
            foreach ($result as $table) {
472
                if (true === $withoutPrefix) {
473
                    $table['Name'] = str_replace($prefix, '', $table['Name']);
474
                }
475
                $return[$table['Name']] = $table;
476
            }
477
478
            return $return;
479
        }
480
481
        return array();
482
    }
483
484
    /**
485
     * @param OutputInterface $output [optional]
486
     *
487
     * @return array|DbSettings
488
     */
489
    public function getDbSettings(OutputInterface $output = null)
490
    {
491
        if ($this->dbSettings) {
492
            return $this->dbSettings;
493
        }
494
495
        $output = $this->fallbackOutput($output);
496
497
        $this->detectDbSettings($output);
498
499
        if (!$this->dbSettings) {
500
            throw new RuntimeException('Database settings fatal error');
501
        }
502
503
        return $this->dbSettings;
504
    }
505
506
    /**
507
     * @return boolean
508
     */
509
    public function getIsSocketConnect()
510
    {
511
        return $this->getDbSettings()->isSocketConnect();
512
    }
513
514
    /**
515
     * Returns the canonical name of this helper.
516
     *
517
     * @return string The canonical name
518
     *
519
     * @api
520
     */
521
    public function getName()
522
    {
523
        return 'database';
524
    }
525
526
    /**
527
     * @param OutputInterface $output
528
     */
529 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...
530
    {
531
        $this->detectDbSettings($output);
532
        $db = $this->getConnection();
533
        $db->query('DROP DATABASE `' . $this->dbSettings['dbname'] . '`');
534
        $output->writeln('<info>Dropped database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
535
    }
536
537
    /**
538
     * @param OutputInterface $output
539
     */
540
    public function dropTables($output)
541
    {
542
        $result = $this->getTables();
543
        $query = 'SET FOREIGN_KEY_CHECKS = 0; ';
544
        $count = 0;
545
        foreach ($result as $tableName) {
546
            $query .= 'DROP TABLE IF EXISTS `' . $tableName . '`; ';
547
            $count++;
548
        }
549
        $query .= 'SET FOREIGN_KEY_CHECKS = 1;';
550
        $this->getConnection()->query($query);
551
        $output->writeln('<info>Dropped database tables</info> <comment>' . $count . ' tables dropped</comment>');
552
    }
553
554
    /**
555
     * @param OutputInterface $output
556
     */
557 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...
558
    {
559
        $this->detectDbSettings($output);
560
        $db = $this->getConnection();
561
        $db->query('CREATE DATABASE IF NOT EXISTS `' . $this->dbSettings['dbname'] . '`');
562
        $output->writeln('<info>Created database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
563
    }
564
565
    /**
566
     * @param string $command example: 'VARIABLES', 'STATUS'
567
     * @param string|null $variable [optional]
568
     *
569
     * @return array
570
     */
571
    private function runShowCommand($command, $variable = null)
572
    {
573
        $db = $this->getConnection();
574
575
        if (null !== $variable) {
576
            $statement = $db->prepare(
577
                'SHOW /*!50000 GLOBAL */ ' . $command . ' LIKE :like',
578
                array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
579
            );
580
            $statement->execute(
581
                array(':like' => $variable)
582
            );
583
        } else {
584
            $statement = $db->query('SHOW /*!50000 GLOBAL */ ' . $command);
585
        }
586
587
        if ($statement) {
588
            /** @var array|string[] $result */
589
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
590
            $return = array();
591
            foreach ($result as $row) {
592
                $return[$row['Variable_name']] = $row['Value'];
593
            }
594
595
            return $return;
596
        }
597
598
        return array();
599
    }
600
601
    /**
602
     * @param string|null $variable [optional]
603
     *
604
     * @return array
605
     */
606
    public function getGlobalVariables($variable = null)
607
    {
608
        return $this->runShowCommand('VARIABLES', $variable);
609
    }
610
611
    /**
612
     * @param string|null $variable [optional]
613
     *
614
     * @return array
615
     */
616
    public function getGlobalStatus($variable = null)
617
    {
618
        return $this->runShowCommand('STATUS', $variable);
619
    }
620
621
    /**
622
     * @return Application|BaseApplication
623
     */
624
    private function getApplication()
625
    {
626
        $command = $this->getHelperSet()->getCommand();
627
628
        if ($command) {
629
            $application = $command->getApplication();
630
        } else {
631
            $application = new Application();
632
        }
633
634
        return $application;
635
    }
636
637
    /**
638
     * small helper method to obtain an object of type OutputInterface
639
     *
640
     * @param OutputInterface|null $output
641
     *
642
     * @return OutputInterface
643
     */
644
    private function fallbackOutput(OutputInterface $output = null)
645
    {
646
        if (null !== $output) {
647
            return $output;
648
        }
649
650
        if ($this->getHelperSet()->has('io')) {
651
            /** @var $helper IoHelper */
652
            $helper = $this->getHelperSet()->get('io');
653
            $output = $helper->getOutput();
654
        }
655
656
        if (null === $output) {
657
            $output = new NullOutput();
658
        }
659
660
        return $output;
661
    }
662
}
663