Completed
Push — master ( bd5921...d9293e )
by Christian
17:54 queued 08:59
created

DatabaseHelper::quoteLike()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 10
rs 9.4286
cc 1
eloc 6
nc 1
nop 2
1
<?php
2
3
namespace N98\Util\Console\Helper;
4
5
use InvalidArgumentException;
6
use N98\Magento\DbSettings;
7
use PDO;
8
use PDOStatement;
9
use RuntimeException;
10
use Symfony\Component\Console\Application as BaseApplication;
11
use Symfony\Component\Console\Helper\Helper as AbstractHelper;
12
use N98\Magento\Application;
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
     */
49
    public function detectDbSettings(OutputInterface $output)
50
    {
51
        if (null !== $this->dbSettings) {
52
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(sprintf('<debug>Loading database configuration from file <info>%s</info></debug>', $configFile));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 126 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...
63
        }
64
65
        try {
66
            $this->dbSettings = new DbSettings($configFile);
67
        } catch (InvalidArgumentException $e) {
68
            $output->writeln('<error>' . $e->getMessage() . '</error>');
69
            throw new RuntimeException('Failed to load database settings from config file', 0, $e);
70
        }
71
    }
72
73
    /**
74
     * Connects to the database without initializing magento
75
     *
76
     * @param OutputInterface $output = null
77
     *
78
     * @return PDO
79
     */
80
    public function getConnection(OutputInterface $output = null)
81
    {
82
        if (!$this->_connection) {
83
            $this->_connection = $this->getDbSettings($output)->getConnection();
84
        }
85
86
        return $this->_connection;
87
    }
88
89
    /**
90
     * Creates a PDO DSN for the adapter from $this->_config settings.
91
     *
92
     * @see Zend_Db_Adapter_Pdo_Abstract
93
     * @return string
94
     */
95
    public function dsn()
96
    {
97
        return $this->getDbSettings()->getDsn();
98
    }
99
100
    /**
101
     * Check whether current mysql user has $privilege privilege
102
     *
103
     * @param string $privilege
104
     *
105
     * @return bool
106
     */
107
    public function mysqlUserHasPrivilege($privilege)
108
    {
109
        $statement = $this->getConnection()->query('SHOW GRANTS');
110
111
        $result = $statement->fetchAll(PDO::FETCH_COLUMN);
112
        foreach ($result as $row) {
113
            if (preg_match('/^GRANT(.*)' . strtoupper($privilege) . '/', $row)
114
                || preg_match('/^GRANT(.*)ALL/', $row)
115
            ) {
116
                return true;
117
            }
118
        }
119
120
        return false;
121
    }
122
123
    /**
124
     * @return string
125
     */
126
    public function getMysqlClientToolConnectionString()
127
    {
128
        return $this->getDbSettings()->getMysqlClientToolConnectionString();
129
    }
130
131
    /**
132
     * Get mysql variable value
133
     *
134
     * @param string $variable
135
     *
136
     * @return bool|array returns array on success, false on failure
137
     */
138
    public function getMysqlVariableValue($variable)
139
    {
140
        $statement = $this->getConnection()->query("SELECT @@{$variable};");
141
        if (false === $statement) {
142
            throw new RuntimeException(sprintf('Failed to query mysql variable %s', var_export($variable, 1)));
143
        }
144
145
        $result = $statement->fetch(PDO::FETCH_ASSOC);
146
        if ($result) {
147
            return $result;
148
        }
149
150
        return false;
151
    }
152
153
    /**
154
     * obtain mysql variable value from the database connection.
155
     *
156
     * in difference to @see getMysqlVariableValue(), this method allows to specify the type of the variable as well
157
     * as to use any variable identifier even such that need quoting.
158
     *
159
     * @param string $name mysql variable name
160
     * @param string $type [optional] variable type, can be a system variable ("@@", default) or a session variable
161
     *                     ("@").
162
     *
163
     * @return string variable value, null if variable was not defined
164
     * @throws RuntimeException in case a system variable is unknown (SQLSTATE[HY000]: 1193: Unknown system variable
165
     *                          'nonexistent')
166
     */
167
    public function getMysqlVariable($name, $type = null)
168
    {
169
        if (null === $type) {
170
            $type = "@@";
171
        } else {
172
            $type = (string) $type;
173
        }
174
175
        if (!in_array($type, array("@@", "@"), true)) {
176
            throw new InvalidArgumentException(
177
                sprintf('Invalid mysql variable type "%s", must be "@@" (system) or "@" (session)', $type)
178
            );
179
        }
180
181
        $quoted = '`' . strtr($name, array('`' => '``')) . '`';
182
        $query  = "SELECT {$type}{$quoted};";
183
184
        $connection = $this->getConnection();
185
        $statement  = $connection->query($query, PDO::FETCH_COLUMN, 0);
186
        if ($statement instanceof PDOStatement) {
187
            $result = $statement->fetchColumn(0);
188
        } else {
189
            $reason = $connection->errorInfo()
190
                ? vsprintf('SQLSTATE[%s]: %s: %s', $connection->errorInfo())
191
                : 'no error info';
192
193
            throw new RuntimeException(
194
                sprintf('Failed to query mysql variable %s: %s', var_export($name, true), $reason)
195
            );
196
        }
197
198
        return $result;
199
    }
200
201
    /**
202
     * @param array $commandConfig
203
     *
204
     * @return array
205
     */
206
    public function getTableDefinitions(array $commandConfig)
207
    {
208
        $tableDefinitions = array();
209
        if (isset($commandConfig['table-groups'])) {
210
            $tableGroups = $commandConfig['table-groups'];
211
            foreach ($tableGroups as $index => $definition) {
212
                $description = isset($definition['description']) ? $definition['description'] : '';
213
                if (!isset($definition['id'])) {
214
                    throw new RuntimeException('Invalid definition of table-groups (id missing) Index: ' . $index);
215
                }
216
                if (!isset($definition['tables'])) {
217
                    throw new RuntimeException('Invalid definition of table-groups (tables missing) Id: '
218
                        . $definition['id']
219
                    );
220
                }
221
222
                $tableDefinitions[$definition['id']] = array(
223
                    'tables'      => $definition['tables'],
224
                    'description' => $description,
225
                );
226
            }
227
        }
228
229
        return $tableDefinitions;
230
    }
231
232
    /**
233
     * @param array $list
234
     * @param array $definitions
235
     * @param array $resolved Which definitions where already resolved -> prevent endless loops
236
     *
237
     * @return array
238
     * @throws RuntimeException
239
     */
240
    public function resolveTables(array $list, array $definitions = array(), array $resolved = array())
241
    {
242
        if ($this->_tables === null) {
243
            $this->_tables = $this->getTables(true);
244
        }
245
246
        $resolvedList = array();
247
        foreach ($list as $entry) {
248
            if (substr($entry, 0, 1) == '@') {
249
                $code = substr($entry, 1);
250
                if (!isset($definitions[$code])) {
251
                    throw new RuntimeException('Table-groups could not be resolved: ' . $entry);
252
                }
253
                if (!isset($resolved[$code])) {
254
                    $resolved[$code] = true;
255
                    $tables          = $this->resolveTables(explode(' ', $definitions[$code]['tables']), $definitions, $resolved);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 130 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...
256
                    $resolvedList    = array_merge($resolvedList, $tables);
257
                }
258
                continue;
259
            }
260
261
            // resolve wildcards
262
            if (strpos($entry, '*') !== false) {
263
                $connection = $this->getConnection();
264
                $sth        = $connection->prepare('SHOW TABLES LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 125 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...
265
                $sth->execute(
266
                    array(':like' => str_replace('*', '%', $this->dbSettings['prefix'] . $entry))
267
                );
268
                $rows = $sth->fetchAll();
269
                foreach ($rows as $row) {
270
                    $resolvedList[] = $row[0];
271
                }
272
                continue;
273
            }
274
275
            if (in_array($entry, $this->_tables)) {
276
                $resolvedList[] = $this->dbSettings['prefix'] . $entry;
277
            }
278
        }
279
280
        asort($resolvedList);
281
        $resolvedList = array_unique($resolvedList);
282
283
        return $resolvedList;
284
    }
285
286
    /**
287
     * Get list of database tables
288
     *
289
     * @param bool $withoutPrefix [optional] remove prefix from the returned table names. prefix is obtained from
290
     *                            magento database configuration. defaults to false.
291
     *
292
     * @return array
293
     * @throws RuntimeException
294
     */
295
    public function getTables($withoutPrefix = null)
296
    {
297
        $withoutPrefix = (bool) $withoutPrefix;
298
299
        $db     = $this->getConnection();
300
        $prefix = $this->dbSettings['prefix'];
301
        $length = strlen($prefix);
302
303
        $columnName = 'table_name';
304
        $column     = $columnName;
305
306
        $input = array();
307
308
        if ($withoutPrefix && $length) {
309
            $column         = sprintf('SUBSTRING(%1$s FROM 1 + CHAR_LENGTH(:name)) %1$s', $columnName);
310
            $input[':name'] = $prefix;
311
        }
312
313
        $condition = 'table_schema = database()';
314
315
        if ($length) {
316
            $escape = '=';
317
            $condition .= sprintf(" AND %s LIKE :like ESCAPE '%s'", $columnName, $escape);
318
            $input[':like'] = $this->quoteLike($prefix, $escape) . '%';
319
        }
320
321
        $query     = sprintf('SELECT %s FROM information_schema.tables WHERE %s;', $column, $condition);
322
        $statement = $db->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
323
        $result    = $statement->execute($input);
324
325
        if (!$result) {
326
            // @codeCoverageIgnoreStart
327
            $this->throwRuntimeException(
328
                $statement
329
                , sprintf('Failed to obtain tables from database: %s', var_export($query, true))
330
            );
331
        } // @codeCoverageIgnoreEnd
332
333
        $result = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
334
335
        return $result;
336
    }
337
338
    /**
339
     * throw a runtime exception and provide error info for the statement if available
340
     *
341
     * @param PDOStatement $statement
342
     * @param string       $message
343
     *
344
     * @throws RuntimeException
345
     */
346
    private function throwRuntimeException(PDOStatement $statement, $message = "")
347
    {
348
        $reason = $statement->errorInfo()
349
            ? vsprintf('SQLSTATE[%s]: %s: %s', $statement->errorInfo())
350
            : 'no error info for statement';
351
352
        if (strlen($message)) {
353
            $message .= ': ';
354
        } else {
355
            $message = '';
356
        }
357
358
        throw new RuntimeException($message . $reason);
359
    }
360
361
    /**
362
     * quote a string so that it is safe to use in a LIKE
363
     *
364
     * @param string $string
365
     * @param string $escape character - single us-ascii character
366
     *
367
     * @return string
368
     */
369
    private function quoteLike($string, $escape = '=')
370
    {
371
        $translation = array(
372
            $escape => $escape . $escape,
373
            '%'     => $escape . '%',
374
            '_'     => $escape . '_',
375
        );
376
377
        return strtr($string, $translation);
378
    }
379
380
    /**
381
     * Get list of db tables status
382
     *
383
     * @param bool $withoutPrefix
384
     *
385
     * @return array
386
     */
387
    public function getTablesStatus($withoutPrefix = false)
388
    {
389
        $db     = $this->getConnection();
390
        $prefix = $this->dbSettings['prefix'];
391
        if (strlen($prefix) > 0) {
392
            $statement = $db->prepare('SHOW TABLE STATUS LIKE :like', array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
393
            $statement->execute(
394
                array(':like' => $prefix . '%')
395
            );
396
        } else {
397
            $statement = $db->query('SHOW TABLE STATUS');
398
        }
399
400
        if ($statement) {
401
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
402
            $return = array();
403
            foreach ($result as $table) {
404
                if (true === $withoutPrefix) {
405
                    $table['Name'] = str_replace($prefix, '', $table['Name']);
406
                }
407
                $return[$table['Name']] = $table;
408
            }
409
410
            return $return;
411
        }
412
413
        return array();
414
    }
415
416
    /**
417
     * @param OutputInterface $output [optional]
418
     *
419
     * @return array|DbSettings
420
     */
421
    public function getDbSettings(OutputInterface $output = null)
422
    {
423
        if ($this->dbSettings) {
424
            return $this->dbSettings;
425
        }
426
427
        $output = $this->fallbackOutput($output);
428
429
        $this->detectDbSettings($output);
430
431
        if (!$this->dbSettings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->dbSettings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
432
            throw new RuntimeException('Database settings fatal error');
433
        }
434
435
        return $this->dbSettings;
436
    }
437
438
    /**
439
     * @return boolean
440
     */
441
    public function getIsSocketConnect()
442
    {
443
        return $this->getDbSettings()->isSocketConnect();
444
    }
445
446
    /**
447
     * Returns the canonical name of this helper.
448
     *
449
     * @return string The canonical name
450
     *
451
     * @api
452
     */
453
    public function getName()
454
    {
455
        return 'database';
456
    }
457
458
    /**
459
     * @param OutputInterface $output
460
     */
461 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...
462
    {
463
        $this->detectDbSettings($output);
464
        $db = $this->getConnection();
465
        $db->query('DROP DATABASE `' . $this->dbSettings['dbname'] . '`');
466
        $output->writeln('<info>Dropped database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
467
    }
468
469
    /**
470
     * @param OutputInterface $output
471
     */
472
    public function dropTables($output)
473
    {
474
        $result = $this->getTables();
475
        $query  = 'SET FOREIGN_KEY_CHECKS = 0; ';
476
        $count  = 0;
477
        foreach ($result as $tableName) {
478
            $query .= 'DROP TABLE IF EXISTS `' . $tableName . '`; ';
479
            $count++;
480
        }
481
        $query .= 'SET FOREIGN_KEY_CHECKS = 1;';
482
        $this->getConnection()->query($query);
483
        $output->writeln('<info>Dropped database tables</info> <comment>' . $count . ' tables dropped</comment>');
484
    }
485
486
    /**
487
     * @param OutputInterface $output
488
     */
489 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...
490
    {
491
        $this->detectDbSettings($output);
492
        $db = $this->getConnection();
493
        $db->query('CREATE DATABASE IF NOT EXISTS `' . $this->dbSettings['dbname'] . '`');
494
        $output->writeln('<info>Created database</info> <comment>' . $this->dbSettings['dbname'] . '</comment>');
495
    }
496
497
    /**
498
     * @param string $command  example: 'VARIABLES', 'STATUS'
499
     * @param string $variable [optional]
500
     *
501
     * @return array
502
     */
503
    private function runShowCommand($command, $variable = null)
504
    {
505
        $db = $this->getConnection();
506
507
        if (null !== $variable) {
508
            $statement = $db->prepare(
509
                'SHOW /*!50000 GLOBAL */ ' . $command . ' LIKE :like',
510
                array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
511
            );
512
            $statement->execute(
513
                array(':like' => $variable)
514
            );
515
        } else {
516
            $statement = $db->query('SHOW /*!50000 GLOBAL */ ' . $command);
517
        }
518
519
        if ($statement) {
520
            /** @var array|string[] $result */
521
            $result = $statement->fetchAll(PDO::FETCH_ASSOC);
522
            $return = array();
523
            foreach ($result as $row) {
524
                $return[$row['Variable_name']] = $row['Value'];
525
            }
526
527
            return $return;
528
        }
529
530
        return array();
531
    }
532
533
    /**
534
     * @param string $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]
545
     *
546
     * @return array
547
     */
548
    public function getGlobalStatus($variable = null)
549
    {
550
        return $this->runShowCommand('STATUS', $variable);
551
    }
552
553
    /**
554
     * @return Application|BaseApplication
555
     */
556
    private function getApplication()
557
    {
558
        $command = $this->getHelperSet()->getCommand();
559
560
        if ($command) {
561
            $application = $command->getApplication();
562
        } else {
563
            $application = new Application();
564
        }
565
566
        return $application;
567
    }
568
569
    /**
570
     * small helper method to obtain an object of type OutputInterface
571
     *
572
     * @param OutputInterface|null $output
573
     *
574
     * @return OutputInterface
575
     */
576
    private function fallbackOutput(OutputInterface $output = null)
577
    {
578
        if (null !== $output) {
579
            return $output;
580
        }
581
582
        if ($helper = $this->getHelperSet()->get('io')) {
583
            /** @var $helper IoHelper */
584
            $output = $helper->getOutput();
585
        }
586
587
        if (null === $output) {
588
            $output = new NullOutput();
589
        }
590
591
        return $output;
592
    }
593
}
594