Completed
Push — develop ( 9a96eb...ec6961 )
by Christian
02:45
created

DumpCommand::createExecs()   F

Complexity

Conditions 11
Paths 768

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 64
rs 3.5632
c 0
b 0
f 0
cc 11
nc 768
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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