Completed
Push — develop ( 9739cc...72819a )
by Christian
02:34
created

DumpCommand::excludeTables()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.8817
c 0
b 0
f 0
cc 6
nc 12
nop 2
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
                'set-gtid-purged-off',
74
                null,
75
                InputOption::VALUE_NONE,
76
                'add --set-gtid-purged=OFF'
77
            )
78
            ->addOption(
79
                'no-single-transaction',
80
                null,
81
                InputOption::VALUE_NONE,
82
                'Do not use single-transaction (not recommended, this is blocking)'
83
            )
84
            ->addOption(
85
                'human-readable',
86
                null,
87
                InputOption::VALUE_NONE,
88
                'Use a single insert with column names per row. Useful to track database differences. Use db:import ' .
89
                '--optimize for speeding up the import.'
90
            )
91
            ->addOption(
92
                'git-friendly',
93
                null,
94
                InputOption::VALUE_NONE,
95
                '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.'
96
            )
97
            ->addOption(
98
                'add-routines',
99
                null,
100
                InputOption::VALUE_NONE,
101
                'Include stored routines in dump (procedures & functions)'
102
            )
103
            ->addOption(
104
                'no-tablespaces',
105
                null,
106
                InputOption::VALUE_NONE,
107
                'Use this option if you want to create a dump without having the PROCESS privilege'
108
            )
109
            ->addOption(
110
                'stdout',
111
                null,
112
                InputOption::VALUE_NONE,
113
                'Dump to stdout'
114
            )
115
            ->addOption(
116
                'strip',
117
                's',
118
                InputOption::VALUE_OPTIONAL,
119
                'Tables to strip (dump only structure of those tables)'
120
            )
121
            ->addOption(
122
                'exclude',
123
                'e',
124
                InputOption::VALUE_OPTIONAL,
125
                'Tables to exclude entirely from the dump (including structure)'
126
            )
127
            ->addOption(
128
                'include',
129
                'i',
130
                InputOption::VALUE_OPTIONAL,
131
                'Tables to include entirely in the dump (including structure)'
132
            )
133
            ->addOption(
134
                'force',
135
                'f',
136
                InputOption::VALUE_NONE,
137
                'Do not prompt if all options are defined'
138
            )
139
            ->addOption(
140
                'keep-column-statistics',
141
                null,
142
                InputOption::VALUE_NONE,
143
                'Keeps the Column Statistics table in SQL dump'
144
            )
145
            ->setDescription('Dumps database with mysqldump cli client');
146
147
        $help = <<<HELP
148
Dumps configured magento database with `mysqldump`. You must have installed
149
the MySQL client tools.
150
151
On debian systems run `apt-get install mysql-client` to do that.
152
153
The command reads app/etc/env.php to find the correct settings.
154
155
See it in action: http://youtu.be/ttjZHY6vThs
156
157
- If you like to prepend a timestamp to the dump name the --add-time option
158
  can be used.
159
160
- The command comes with a compression function. Add i.e. `--compression=gz`
161
  to dump directly in gzip compressed file.
162
163
HELP;
164
        $this->setHelp($help);
165
    }
166
167
    /**
168
     * @return array
169
     *
170
     * @deprecated Use database helper
171
     */
172
    private function getTableDefinitions()
173
    {
174
        $this->commandConfig = $this->getCommandConfig();
175
176
        if ($this->tableDefinitions === null) {
177
            /* @var $dbHelper DatabaseHelper */
178
            $dbHelper = $this->getHelper('database');
179
180
            $this->tableDefinitions = $dbHelper->getTableDefinitions($this->commandConfig);
181
        }
182
183
        return $this->tableDefinitions;
184
    }
185
186
    /**
187
     * Generate help for table definitions
188
     *
189
     * @return string
190
     */
191
    public function getTableDefinitionHelp()
192
    {
193
        $messages = PHP_EOL;
194
        $this->commandConfig = $this->getCommandConfig();
195
        $messages .= <<<HELP
196
<comment>Strip option</comment>
197
 If you like to skip data of some tables you can use the --strip option.
198
 The strip option creates only the structure of the defined tables and
199
 forces `mysqldump` to skip the data.
200
201
 Separate each table to strip by a space.
202
 You can use wildcards like * and ? in the table names to strip multiple
203
 tables. In addition you can specify pre-defined table groups, that start
204
 with an @ symbol.
205
206
 Example: "dataflow_batch_export unimportant_module_* @log"
207
208
    $ n98-magerun2.phar db:dump --strip="@stripped"
209
210
<comment>Available Table Groups</comment>
211
212
HELP;
213
214
        $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...
215
        $list = [];
216
        $maxNameLen = 0;
217
        foreach ($definitions as $id => $definition) {
218
            $name = '@' . $id;
219
            $description = isset($definition['description']) ? $definition['description'] . '.' : '';
220
            $nameLen = strlen($name);
221
            if ($nameLen > $maxNameLen) {
222
                $maxNameLen = $nameLen;
223
            }
224
            $list[] = [$name, $description];
225
        }
226
227
        $decrSize = 78 - $maxNameLen - 3;
228
229
        foreach ($list as $entry) {
230
            list($name, $description) = $entry;
231
            $delta = max(0, $maxNameLen - strlen($name));
232
            $spacer = $delta ? str_repeat(' ', $delta) : '';
233
            $buffer = wordwrap($description, $decrSize);
234
            $buffer = strtr($buffer, ["\n" => "\n" . str_repeat(' ', 3 + $maxNameLen)]);
235
            $messages .= sprintf(" <info>%s</info>%s  %s\n", $name, $spacer, $buffer);
236
        }
237
238
        $messages .= <<<HELP
239
240
Extended: https://github.com/netz98/n98-magerun/wiki/Stripped-Database-Dumps
241
HELP;
242
243
        return $messages;
244
    }
245
246
    public function getHelp()
247
    {
248
        return
249
            parent::getHelp() . PHP_EOL
250
            . $this->getCompressionHelp() . PHP_EOL
251
            . $this->getTableDefinitionHelp();
252
    }
253
254
    /**
255
     * @param InputInterface $input
256
     * @param OutputInterface $output
257
     *
258
     * @return int|void
259
     * @throws \Magento\Framework\Exception\FileSystemException
260
     */
261
    protected function execute(InputInterface $input, OutputInterface $output)
262
    {
263
        // communicate early what is required for this command to run (is enabled)
264
        $enabler = new Enabler($this);
265
        $enabler->functionExists('exec');
266
        $enabler->functionExists('passthru');
267
        $enabler->operatingSystemIsNotWindows();
268
269
        $this->detectDbSettings($output);
270
271
        if ($this->nonCommandOutput($input)) {
272
            $this->writeSection($output, 'Dump MySQL Database');
273
        }
274
275
        $execs = $this->createExecs($input, $output);
276
277
        $success = $this->runExecs($execs, $input, $output);
278
279
        return $success ? 0 : 1;
280
    }
281
282
    /**
283
     * @param InputInterface $input
284
     * @param OutputInterface $output
285
     * @return Execs
286
     * @throws \Magento\Framework\Exception\FileSystemException
287
     */
288
    private function createExecs(InputInterface $input, OutputInterface $output)
289
    {
290
        $execs = new Execs('mysqldump');
291
        $execs->setCompression($input->getOption('compression'));
292
        $execs->setFileName($this->getFileName($input, $output, $execs->getCompressor()));
293
294
        if (!$input->getOption('no-single-transaction')) {
295
            $execs->addOptions('--single-transaction --quick');
296
        }
297
298
        if ($input->getOption('human-readable')) {
299
            $execs->addOptions('--complete-insert --skip-extended-insert ');
300
        }
301
302
        if ($input->getOption('set-gtid-purged-off')) {
303
            $execs->addOptions('--set-gtid-purged=OFF ');
304
        }
305
306
        if ($input->getOption('add-routines')) {
307
            $execs->addOptions('--routines ');
308
        }
309
310
        if ($input->getOption('no-tablespaces')) {
311
            $execs->addOptions('--no-tablespaces ');
312
        }
313
314
        if ($this->checkColumnStatistics()) {
315
            if ($input->getOption('keep-column-statistics')) {
316
                $execs->addOptions('--column-statistics=1 ');
317
            } else {
318
                $execs->addOptions('--column-statistics=0 ');
319
            }
320
        }
321
322
        $postDumpGitFriendlyPipeCommands = '';
323
        if ($input->getOption('git-friendly')) {
324
            $postDumpGitFriendlyPipeCommands = ' | sed \'s$VALUES ($VALUES\n($g\' | sed \'s$),($),\n($g\'';
325
        }
326
327
        /* @var $database DatabaseHelper */
328
        $database = $this->getDatabaseHelper();
329
330
        $mysqlClientToolConnectionString = $database->getMysqlClientToolConnectionString();
331
332
        $excludeTables = $this->excludeTables($input, $output);
333
        $stripTables = array_diff($this->stripTables($input, $output), $excludeTables);
334
        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...
335
            // dump structure for strip-tables
336
            $execs->add(
337
                '--no-data ' . $mysqlClientToolConnectionString .
338
                ' ' . implode(' ', $stripTables) . $this->postDumpPipeCommands()
339
            );
340
        }
341
342
        // dump data for all other tables
343
        $ignore = '';
344
        foreach (array_merge($excludeTables, $stripTables) as $ignoreTable) {
345
            $ignore .= '--ignore-table=' . $this->dbSettings['dbname'] . '.' . $ignoreTable . ' ';
346
        }
347
348
        $execs->add($ignore . $mysqlClientToolConnectionString . $postDumpGitFriendlyPipeCommands . $this->postDumpPipeCommands());
349
350
        return $execs;
351
    }
352
353
    /**
354
     * @param Execs $execs
355
     * @param InputInterface $input
356
     * @param OutputInterface $output
357
     * @return bool
358
     */
359
    private function runExecs(Execs $execs, InputInterface $input, OutputInterface $output)
360
    {
361
        if ($input->getOption('only-command') && !$input->getOption('print-only-filename')) {
362
            foreach ($execs->getCommands() as $command) {
363
                $output->writeln($command);
364
            }
365
        } else {
366
            if ($this->nonCommandOutput($input)) {
367
                $output->writeln(
368
                    '<comment>Start dumping database <info>' . $this->dbSettings['dbname'] .
369
                    '</info> to file <info>' . $execs->getFileName() . '</info>'
370
                );
371
            }
372
373
            $commands = $input->getOption('dry-run') ? [] : $execs->getCommands();
374
375
            foreach ($commands as $command) {
376
                if (!$this->runExec($command, $input, $output)) {
377
                    return false;
378
                }
379
            }
380
381
            if (!$input->getOption('stdout') && !$input->getOption('print-only-filename')) {
382
                $output->writeln('<info>Finished</info>');
383
            }
384
        }
385
386
        if ($input->getOption('print-only-filename')) {
387
            $output->writeln($execs->getFileName());
388
        }
389
390
        return true;
391
    }
392
393
    /**
394
     * @param string $command
395
     * @param InputInterface $input
396
     * @param OutputInterface $output
397
     * @return bool
398
     */
399
    private function runExec($command, InputInterface $input, OutputInterface $output)
400
    {
401
        $commandOutput = '';
402
403
        if ($input->getOption('stdout')) {
404
            passthru($command, $returnCode);
405
        } else {
406
            Exec::run($command, $commandOutput, $returnCode);
407
        }
408
409
        if ($returnCode > 0) {
410
            $output->writeln('<error>' . $commandOutput . '</error>');
411
            $output->writeln('<error>Return Code: ' . $returnCode . '. ABORTED.</error>');
412
413
            return false;
414
        }
415
416
        return true;
417
    }
418
419
    /**
420
     * @param InputInterface $input
421
     * @param OutputInterface $output
422
     * @return array
423
     * @throws \Magento\Framework\Exception\FileSystemException
424
     */
425
    private function stripTables(InputInterface $input, OutputInterface $output)
426
    {
427
        if (!$input->getOption('strip')) {
428
            return [];
429
        }
430
431
        $stripTables = $this->resolveDatabaseTables($input->getOption('strip'));
432
433
        if ($this->nonCommandOutput($input)) {
434
            $output->writeln(
435
                sprintf('<comment>No-data export for: <info>%s</info></comment>', implode(' ', $stripTables))
436
            );
437
        }
438
439
        return $stripTables;
440
    }
441
442
    /**
443
     * @param InputInterface $input
444
     * @param OutputInterface $output
445
     * @return array
446
     * @throws \Magento\Framework\Exception\FileSystemException
447
     */
448
    private function excludeTables(InputInterface $input, OutputInterface $output)
449
    {
450
        $excludeTables = [];
451
452
        if ($input->getOption('include')) {
453
            $database = $this->getDatabaseHelper();
454
            $includeTables = $this->resolveDatabaseTables($input->getOption('include'));
455
            $excludeTables = array_diff($database->getTables(), $includeTables);
456
        }
457
458
        if ($input->getOption('exclude')) {
459
            $excludeTables = array_merge($excludeTables, $this->resolveDatabaseTables($input->getOption('exclude')));
460
            if (isset($includeTables)) { // only needed when also "include" was given
461
                asort($excludeTables);
462
                $excludeTables = array_unique($excludeTables);
463
            }
464
        }
465
466
        if ($excludeTables && $this->nonCommandOutput($input)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $excludeTables 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...
467
            $output->writeln(
468
                sprintf('<comment>Excluded: <info>%s</info></comment>', implode(' ', $excludeTables))
469
            );
470
        }
471
472
        return $excludeTables;
473
    }
474
475
    /**
476
     * @param string $list space separated list of tables
477
     * @return array
478
     * @throws \Magento\Framework\Exception\FileSystemException
479
     */
480
    private function resolveDatabaseTables($list)
481
    {
482
        $database = $this->getDatabaseHelper();
483
484
        return $database->resolveTables(
485
            explode(' ', $list),
486
            $database->getTableDefinitions($this->getCommandConfig())
487
        );
488
    }
489
490
    /**
491
     * Commands which filter mysql data. Piped to mysqldump command
492
     *
493
     * @return string
494
     */
495
    protected function postDumpPipeCommands()
496
    {
497
        return ' | LANG=C LC_CTYPE=C LC_ALL=C sed -e ' . escapeshellarg('s/DEFINER[ ]*=[ ]*[^*]*\*/\*/');
498
    }
499
500
    /**
501
     * Command which makes the dump git friendly. Piped to mysqldump command.
502
     *
503
     * @return string
504
     */
505
    protected function postDumpGitFriendlyPipeCommands()
506
    {
507
        return ' | sed \'s$VALUES ($VALUES\n($g\' | sed \'s$),($),\n($g\'';
508
    }
509
510
    /**
511
     * @param InputInterface $input
512
     * @param OutputInterface $output
513
     * @param Compressor $compressor
514
     *
515
     * @return string
516
     */
517
    protected function getFileName(InputInterface $input, OutputInterface $output, Compressor $compressor)
518
    {
519
        $nameExtension = '.sql';
520
521
        $optionAddTime = 'no';
522
        if ($input->getOption('add-time')) {
523
            $optionAddTime = $input->getOption('add-time');
524
            if (empty($optionAddTime)) {
525
                $optionAddTime = 'suffix';
526
            }
527
        }
528
529
        list($namePrefix, $nameSuffix) = $this->getFileNamePrefixSuffix($optionAddTime);
530
531
        if (
532
            (
533
                ($fileName = $input->getArgument('filename')) === null
534
                || ($isDir = is_dir($fileName))
535
            )
536
            && !$input->getOption('stdout')
537
        ) {
538
            $defaultName = VerifyOrDie::filename(
539
                $namePrefix . $this->dbSettings['dbname'] . $nameSuffix . $nameExtension
540
            );
541
            if (isset($isDir) && $isDir) {
542
                $defaultName = rtrim($fileName, '/') . '/' . $defaultName;
543
            }
544
            if (!$input->getOption('force')) {
545
                $question = new Question(
546
                    '<question>Filename for SQL dump:</question> [<comment>' . $defaultName . '</comment>]',
547
                    $defaultName
548
                );
549
550
                /** @var QuestionHelper $questionHelper */
551
                $questionHelper = $this->getHelper('question');
552
                $fileName = $questionHelper->ask(
553
                    $input,
554
                    $output,
555
                    $question
556
                );
557
            } else {
558
                $fileName = $defaultName;
559
            }
560
        } elseif ($optionAddTime && $fileName !== null) {
561
            $pathParts = pathinfo($fileName);
562
563
            $fileName = ($pathParts['dirname'] === '.' ? '' : $pathParts['dirname'] . '/')
564
                . $namePrefix
565
                . (isset($pathParts['filename']) ? $pathParts['filename'] : '')
566
                . $nameSuffix
567
                . (isset($pathParts['extension']) ? ('.' . $pathParts['extension']) : '');
568
        }
569
570
        $fileName = $compressor->getFileName($fileName);
571
572
        return $fileName;
573
    }
574
575
    /**
576
     * @param null|bool|string $optionAddTime [optional] true for default "suffix", other string values: "prefix", "no"
577
     * @return array
578
     */
579
    private function getFileNamePrefixSuffix($optionAddTime = null)
580
    {
581
        $namePrefix = '';
582
        $nameSuffix = '';
583
        if ($optionAddTime === null) {
584
            return [$namePrefix, $nameSuffix];
585
        }
586
587
        $timeStamp = date('Y-m-d_His');
588
589
        if (in_array($optionAddTime, ['suffix', true], true)) {
590
            $nameSuffix = '_' . $timeStamp;
591
        } elseif ($optionAddTime === 'prefix') {
592
            $namePrefix = $timeStamp . '_';
593
        } elseif ($optionAddTime !== 'no') {
594
            throw new InvalidArgumentException(
595
                sprintf(
596
                    'Invalid --add-time value %s, possible values are none (for) "suffix", "prefix" or "no"',
597
                    var_export($optionAddTime, true)
598
                )
599
            );
600
        }
601
602
        return [$namePrefix, $nameSuffix];
603
    }
604
605
    /**
606
     * @param InputInterface $input
607
     * @return bool
608
     */
609
    private function nonCommandOutput(InputInterface $input)
610
    {
611
        return
612
            !$input->getOption('stdout')
613
            && !$input->getOption('only-command')
614
            && !$input->getOption('print-only-filename');
615
    }
616
617
    /**
618
     * Checks if 'column statistics' are present in the current MySQL distribution
619
     *
620
     * @return bool
621
     */
622
    private function checkColumnStatistics()
623
    {
624
        Exec::run('mysqldump --help | grep -c column-statistics || true', $output, $returnCode);
625
626
        if ($output > 0) {
627
            return true;
628
        }
629
630
        return false;
631
    }
632
}
633