Completed
Push — master ( 82aa13...9616bb )
by Christian
02:16
created

DumpCommand::createExecs()   D

Complexity

Conditions 10
Paths 384

Size

Total Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

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