Completed
Push — develop ( 084461...97ea8b )
by Christian
02:30
created

DumpCommand::checkColumnStatistics()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
3
namespace N98\Magento\Command\Database;
4
5
use InvalidArgumentException;
6
use N98\Magento\Command\Database\Compressor\Compressor;
7
use N98\Util\Console\Enabler;
8
use N98\Util\Console\Helper\DatabaseHelper;
9
use N98\Util\Exec;
10
use N98\Util\VerifyOrDie;
11
use Symfony\Component\Console\Helper\QuestionHelper;
12
use Symfony\Component\Console\Input\InputArgument;
13
use Symfony\Component\Console\Input\InputInterface;
14
use Symfony\Component\Console\Input\InputOption;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Symfony\Component\Console\Question\Question;
17
18
/**
19
 * Class DumpCommand
20
 * @package N98\Magento\Command\Database
21
 */
22
class DumpCommand extends AbstractDatabaseCommand
23
{
24
    /**
25
     * @var array
26
     */
27
    protected $tableDefinitions = null;
28
29
    /**
30
     * @var array
31
     */
32
    protected $commandConfig = null;
33
34
    protected function configure()
35
    {
36
        parent::configure();
37
        $this
38
            ->setName('db:dump')
39
            ->addArgument('filename', InputArgument::OPTIONAL, 'Dump filename')
40
            ->addOption(
41
                'add-time',
42
                't',
43
                InputOption::VALUE_OPTIONAL,
44
                'Append or prepend a timestamp to filename if a filename is provided. ' .
45
                'Possible values are "suffix", "prefix" or "no".',
46
                ''
47
            )
48
            ->addOption(
49
                'compression',
50
                'c',
51
                InputOption::VALUE_REQUIRED,
52
                'Compress the dump file using one of the supported algorithms'
53
            )
54
            ->addOption(
55
                'only-command',
56
                null,
57
                InputOption::VALUE_NONE,
58
                'Print only mysqldump command. Do not execute'
59
            )
60
            ->addOption(
61
                'print-only-filename',
62
                null,
63
                InputOption::VALUE_NONE,
64
                'Execute and prints no output except the dump filename'
65
            )
66
            ->addOption(
67
                'dry-run',
68
                null,
69
                InputOption::VALUE_NONE,
70
                'Do everything but the actual dump'
71
            )
72
            ->addOption(
73
                'no-single-transaction',
74
                null,
75
                InputOption::VALUE_NONE,
76
                'Do not use single-transaction (not recommended, this is blocking)'
77
            )
78
            ->addOption(
79
                'human-readable',
80
                null,
81
                InputOption::VALUE_NONE,
82
                'Use a single insert with column names per row. Useful to track database differences. Use db:import ' .
83
                '--optimize for speeding up the import.'
84
            )
85
            ->addOption(
86
                'git-friendly',
87
                null,
88
                InputOption::VALUE_NONE,
89
                'Use one insert statement, but with line breaks instead of separate insert statements. Similar to --human-readable, but you wont need to use --optimize to speed up the import.'
90
            )
91
            ->addOption(
92
                'add-routines',
93
                null,
94
                InputOption::VALUE_NONE,
95
                'Include stored routines in dump (procedures & functions)'
96
            )
97
            ->addOption(
98
                'stdout',
99
                null,
100
                InputOption::VALUE_NONE,
101
                'Dump to stdout'
102
            )
103
            ->addOption(
104
                'strip',
105
                's',
106
                InputOption::VALUE_OPTIONAL,
107
                'Tables to strip (dump only structure of those tables)'
108
            )
109
            ->addOption(
110
                'exclude',
111
                'e',
112
                InputOption::VALUE_OPTIONAL,
113
                'Tables to exclude entirely from the dump (including structure)'
114
            )
115
            ->addOption(
116
                'force',
117
                'f',
118
                InputOption::VALUE_NONE,
119
                'Do not prompt if all options are defined'
120
            )
121
            ->addOption(
122
                'keep-column-statistics',
123
                null,
124
                InputOption::VALUE_NONE,
125
                'Keeps the Column Statistics table in SQL dump'
126
            )
127
            ->setDescription('Dumps database with mysqldump cli client');
128
129
        $help = <<<HELP
130
Dumps configured magento database with `mysqldump`. You must have installed
131
the MySQL client tools.
132
133
On debian systems run `apt-get install mysql-client` to do that.
134
135
The command reads app/etc/env.php to find the correct settings.
136
137
See it in action: http://youtu.be/ttjZHY6vThs
138
139
- If you like to prepend a timestamp to the dump name the --add-time option
140
  can be used.
141
142
- The command comes with a compression function. Add i.e. `--compression=gz`
143
  to dump directly in gzip compressed file.
144
145
HELP;
146
        $this->setHelp($help);
147
    }
148
149
    /**
150
     * @return array
151
     *
152
     * @deprecated Use database helper
153
     */
154
    private function getTableDefinitions()
155
    {
156
        $this->commandConfig = $this->getCommandConfig();
157
158
        if ($this->tableDefinitions === null) {
159
            /* @var $dbHelper DatabaseHelper */
160
            $dbHelper = $this->getHelper('database');
161
162
            $this->tableDefinitions = $dbHelper->getTableDefinitions($this->commandConfig);
163
        }
164
165
        return $this->tableDefinitions;
166
    }
167
168
    /**
169
     * Generate help for table definitions
170
     *
171
     * @return string
172
     */
173
    public function getTableDefinitionHelp()
174
    {
175
        $messages = PHP_EOL;
176
        $this->commandConfig = $this->getCommandConfig();
177
        $messages .= <<<HELP
178
<comment>Strip option</comment>
179
 If you like to skip data of some tables you can use the --strip option.
180
 The strip option creates only the structure of the defined tables and
181
 forces `mysqldump` to skip the data.
182
183
 Separate each table to strip by a space.
184
 You can use wildcards like * and ? in the table names to strip multiple
185
 tables. In addition you can specify pre-defined table groups, that start
186
 with an
187
188
 Example: "dataflow_batch_export unimportant_module_* @log
189
190
    $ n98-magerun2.phar db:dump --strip="@stripped"
191
192
<comment>Available Table Groups</comment>
193
194
HELP;
195
196
        $definitions = $this->getTableDefinitions();
0 ignored issues
show
Deprecated Code introduced by
The method N98\Magento\Command\Data...::getTableDefinitions() has been deprecated with message: Use database helper

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
197
        $list = [];
198
        $maxNameLen = 0;
199
        foreach ($definitions as $id => $definition) {
200
            $name = '@' . $id;
201
            $description = isset($definition['description']) ? $definition['description'] . '.' : '';
202
            $nameLen = strlen($name);
203
            if ($nameLen > $maxNameLen) {
204
                $maxNameLen = $nameLen;
205
            }
206
            $list[] = [$name, $description];
207
        }
208
209
        $decrSize = 78 - $maxNameLen - 3;
210
211
        foreach ($list as $entry) {
212
            list($name, $description) = $entry;
213
            $delta = max(0, $maxNameLen - strlen($name));
214
            $spacer = $delta ? str_repeat(' ', $delta) : '';
215
            $buffer = wordwrap($description, $decrSize);
216
            $buffer = strtr($buffer, ["\n" => "\n" . str_repeat(' ', 3 + $maxNameLen)]);
217
            $messages .= sprintf(" <info>%s</info>%s  %s\n", $name, $spacer, $buffer);
218
        }
219
220
        $messages .= <<<HELP
221
222
Extended: https://github.com/netz98/n98-magerun/wiki/Stripped-Database-Dumps
223
HELP;
224
225
        return $messages;
226
    }
227
228
    public function getHelp()
229
    {
230
        return
231
            parent::getHelp() . PHP_EOL
232
            . $this->getCompressionHelp() . PHP_EOL
233
            . $this->getTableDefinitionHelp();
234
    }
235
236
    /**
237
     * @param InputInterface $input
238
     * @param OutputInterface $output
239
     *
240
     * @return int|void
241
     * @throws \Magento\Framework\Exception\FileSystemException
242
     */
243
    protected function execute(InputInterface $input, OutputInterface $output)
244
    {
245
        // communicate early what is required for this command to run (is enabled)
246
        $enabler = new Enabler($this);
247
        $enabler->functionExists('exec');
248
        $enabler->functionExists('passthru');
249
        $enabler->operatingSystemIsNotWindows();
250
251
        $this->detectDbSettings($output);
252
253
        if ($this->nonCommandOutput($input)) {
254
            $this->writeSection($output, 'Dump MySQL Database');
255
        }
256
257
        $execs = $this->createExecs($input, $output);
258
259
        $success = $this->runExecs($execs, $input, $output);
260
261
        return $success ? 0 : 1;
262
    }
263
264
    /**
265
     * @param InputInterface $input
266
     * @param OutputInterface $output
267
     * @return Execs
268
     * @throws \Magento\Framework\Exception\FileSystemException
269
     */
270
    private function createExecs(InputInterface $input, OutputInterface $output)
271
    {
272
        $execs = new Execs('mysqldump');
273
        $execs->setCompression($input->getOption('compression'));
274
        $execs->setFileName($this->getFileName($input, $output, $execs->getCompressor()));
275
276
        if (!$input->getOption('no-single-transaction')) {
277
            $execs->addOptions('--single-transaction --quick');
278
        }
279
280
        if ($input->getOption('human-readable')) {
281
            $execs->addOptions('--complete-insert --skip-extended-insert ');
282
        }
283
284
        if ($input->getOption('add-routines')) {
285
            $execs->addOptions('--routines ');
286
        }
287
288
        if ($this->checkColumnStatistics()) {
289
            if ($input->getOption('keep-column-statistics')) {
290
                $execs->addOptions('--column-statistics=1 ');
291
            } else {
292
                $execs->addOptions('--column-statistics=0 ');
293
            }
294
        }
295
296
        $postDumpGitFriendlyPipeCommands = '';
297
        if ($input->getOption('git-friendly')) {
298
            $postDumpGitFriendlyPipeCommands = ' | sed \'s$VALUES ($VALUES\n($g\' | sed \'s$),($),\n($g\'';
299
        }
300
301
        /* @var $database DatabaseHelper */
302
        $database = $this->getDatabaseHelper();
303
304
        $mysqlClientToolConnectionString = $database->getMysqlClientToolConnectionString();
305
306
        $excludeTables = $this->excludeTables($input, $output);
307
        $stripTables = array_diff($this->stripTables($input, $output), $excludeTables);
308
        if ($stripTables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stripTables 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...
309
            // dump structure for strip-tables
310
            $execs->add(
311
                '--no-data ' . $mysqlClientToolConnectionString .
312
                ' ' . implode(' ', $stripTables) . $this->postDumpPipeCommands()
313
            );
314
        }
315
316
        // dump data for all other tables
317
        $ignore = '';
318
        foreach (array_merge($excludeTables, $stripTables) as $ignoreTable) {
319
            $ignore .= '--ignore-table=' . $this->dbSettings['dbname'] . '.' . $ignoreTable . ' ';
320
        }
321
322
        $execs->add($ignore . $mysqlClientToolConnectionString . $postDumpGitFriendlyPipeCommands . $this->postDumpPipeCommands());
323
324
        return $execs;
325
    }
326
327
    /**
328
     * @param Execs $execs
329
     * @param InputInterface $input
330
     * @param OutputInterface $output
331
     * @return bool
332
     */
333
    private function runExecs(Execs $execs, InputInterface $input, OutputInterface $output)
334
    {
335
        if ($input->getOption('only-command') && !$input->getOption('print-only-filename')) {
336
            foreach ($execs->getCommands() as $command) {
337
                $output->writeln($command);
338
            }
339
        } else {
340
            if ($this->nonCommandOutput($input)) {
341
                $output->writeln(
342
                    '<comment>Start dumping database <info>' . $this->dbSettings['dbname'] .
343
                    '</info> to file <info>' . $execs->getFileName() . '</info>'
344
                );
345
            }
346
347
            $commands = $input->getOption('dry-run') ? [] : $execs->getCommands();
348
349
            foreach ($commands as $command) {
350
                if (!$this->runExec($command, $input, $output)) {
351
                    return false;
352
                }
353
            }
354
355
            if (!$input->getOption('stdout') && !$input->getOption('print-only-filename')) {
356
                $output->writeln('<info>Finished</info>');
357
            }
358
        }
359
360
        if ($input->getOption('print-only-filename')) {
361
            $output->writeln($execs->getFileName());
362
        }
363
364
        return true;
365
    }
366
367
    /**
368
     * @param string $command
369
     * @param InputInterface $input
370
     * @param OutputInterface $output
371
     * @return bool
372
     */
373
    private function runExec($command, InputInterface $input, OutputInterface $output)
374
    {
375
        $commandOutput = '';
376
377
        if ($input->getOption('stdout')) {
378
            passthru($command, $returnCode);
379
        } else {
380
            Exec::run($command, $commandOutput, $returnCode);
381
        }
382
383
        if ($returnCode > 0) {
384
            $output->writeln('<error>' . $commandOutput . '</error>');
385
            $output->writeln('<error>Return Code: ' . $returnCode . '. ABORTED.</error>');
386
387
            return false;
388
        }
389
390
        return true;
391
    }
392
393
    /**
394
     * @param InputInterface $input
395
     * @param OutputInterface $output
396
     * @return array
397
     * @throws \Magento\Framework\Exception\FileSystemException
398
     */
399 View Code Duplication
    private function stripTables(InputInterface $input, OutputInterface $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...
400
    {
401
        if (!$input->getOption('strip')) {
402
            return [];
403
        }
404
405
        $stripTables = $this->resolveDatabaseTables($input->getOption('strip'));
406
407
        if ($this->nonCommandOutput($input)) {
408
            $output->writeln(
409
                sprintf('<comment>No-data export for: <info>%s</info></comment>', implode(' ', $stripTables))
410
            );
411
        }
412
413
        return $stripTables;
414
    }
415
416
    /**
417
     * @param InputInterface $input
418
     * @param OutputInterface $output
419
     * @return array
420
     * @throws \Magento\Framework\Exception\FileSystemException
421
     */
422 View Code Duplication
    private function excludeTables(InputInterface $input, OutputInterface $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...
423
    {
424
        if (!$input->getOption('exclude')) {
425
            return [];
426
        }
427
428
        $excludeTables = $this->resolveDatabaseTables($input->getOption('exclude'));
429
430
        if ($this->nonCommandOutput($input)) {
431
            $output->writeln(
432
                sprintf('<comment>Excluded: <info>%s</info></comment>', implode(' ', $excludeTables))
433
            );
434
        }
435
436
        return $excludeTables;
437
    }
438
439
    /**
440
     * @param string $list space separated list of tables
441
     * @return array
442
     * @throws \Magento\Framework\Exception\FileSystemException
443
     */
444
    private function resolveDatabaseTables($list)
445
    {
446
        $database = $this->getDatabaseHelper();
447
448
        return $database->resolveTables(
449
            explode(' ', $list),
450
            $database->getTableDefinitions($this->getCommandConfig())
451
        );
452
    }
453
454
    /**
455
     * Commands which filter mysql data. Piped to mysqldump command
456
     *
457
     * @return string
458
     */
459
    protected function postDumpPipeCommands()
460
    {
461
        return ' | LANG=C LC_CTYPE=C LC_ALL=C sed -e ' . escapeshellarg('s/DEFINER[ ]*=[ ]*[^*]*\*/\*/');
462
    }
463
464
    /**
465
     * Command which makes the dump git friendly. Piped to mysqldump command.
466
     *
467
     * @return string
468
     */
469
    protected function postDumpGitFriendlyPipeCommands()
470
    {
471
        return ' | sed \'s$VALUES ($VALUES\n($g\' | sed \'s$),($),\n($g\'';
472
    }
473
474
    /**
475
     * @param InputInterface $input
476
     * @param OutputInterface $output
477
     * @param Compressor $compressor
478
     *
479
     * @return string
480
     */
481
    protected function getFileName(InputInterface $input, OutputInterface $output, Compressor $compressor)
482
    {
483
        $nameExtension = '.sql';
484
485
        $optionAddTime = 'no';
486
        if ($input->getOption('add-time')) {
487
            $optionAddTime = $input->getOption('add-time');
488
            if (empty($optionAddTime)) {
489
                $optionAddTime = 'suffix';
490
            }
491
        }
492
493
        list($namePrefix, $nameSuffix) = $this->getFileNamePrefixSuffix($optionAddTime);
494
495
        if (
496
            (
497
                ($fileName = $input->getArgument('filename')) === null
498
                || ($isDir = is_dir($fileName))
499
            )
500
            && !$input->getOption('stdout')
501
        ) {
502
            $defaultName = VerifyOrDie::filename(
503
                $namePrefix . $this->dbSettings['dbname'] . $nameSuffix . $nameExtension
504
            );
505
            if (isset($isDir) && $isDir) {
506
                $defaultName = rtrim($fileName, '/') . '/' . $defaultName;
507
            }
508
            if (!$input->getOption('force')) {
509
                $question = new Question(
510
                    '<question>Filename for SQL dump:</question> [<comment>' . $defaultName . '</comment>]',
511
                    $defaultName
512
                );
513
514
                /** @var QuestionHelper $questionHelper */
515
                $questionHelper = $this->getHelper('question');
516
                $fileName = $questionHelper->ask(
517
                    $input,
518
                    $output,
519
                    $question
520
                );
521
            } else {
522
                $fileName = $defaultName;
523
            }
524
        } elseif ($optionAddTime && $fileName !== null) {
525
            $pathParts = pathinfo($fileName);
526
527
            $fileName = ($pathParts['dirname'] === '.' ? '' : $pathParts['dirname'] . '/')
528
                . $namePrefix
529
                . (isset($pathParts['filename']) ? $pathParts['filename'] : '')
530
                . $nameSuffix
531
                . (isset($pathParts['extension']) ? ('.' . $pathParts['extension']) : '');
532
        }
533
534
        $fileName = $compressor->getFileName($fileName);
535
536
        return $fileName;
537
    }
538
539
    /**
540
     * @param null|bool|string $optionAddTime [optional] true for default "suffix", other string values: "prefix", "no"
541
     * @return array
542
     */
543
    private function getFileNamePrefixSuffix($optionAddTime = null)
544
    {
545
        $namePrefix = '';
546
        $nameSuffix = '';
547
        if ($optionAddTime === null) {
548
            return [$namePrefix, $nameSuffix];
549
        }
550
551
        $timeStamp = date('Y-m-d_His');
552
553
        if (in_array($optionAddTime, ['suffix', true], true)) {
554
            $nameSuffix = '_' . $timeStamp;
555
        } elseif ($optionAddTime === 'prefix') {
556
            $namePrefix = $timeStamp . '_';
557
        } elseif ($optionAddTime !== 'no') {
558
            throw new InvalidArgumentException(
559
                sprintf(
560
                    'Invalid --add-time value %s, possible values are none (for) "suffix", "prefix" or "no"',
561
                    var_export($optionAddTime, true)
562
                )
563
            );
564
        }
565
566
        return [$namePrefix, $nameSuffix];
567
    }
568
569
    /**
570
     * @param InputInterface $input
571
     * @return bool
572
     */
573
    private function nonCommandOutput(InputInterface $input)
574
    {
575
        return
576
            !$input->getOption('stdout')
577
            && !$input->getOption('only-command')
578
            && !$input->getOption('print-only-filename');
579
    }
580
581
    /**
582
     * Checks if 'column statistics' are present in the current MySQL distribution
583
     *
584
     * @return bool
585
     */
586
    private function checkColumnStatistics()
587
    {
588
        Exec::run('mysqldump --help | grep -c column-statistics || true', $output, $returnCode);
589
590
        if ($output > 0) {
591
            return true;
592
        }
593
594
        return false;
595
    }
596
}
597