Completed
Pull Request — master (#894)
by
unknown
04:03
created

DumpCommand::nonCommandOutput()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 1
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\DialogHelper;
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
17
class DumpCommand extends AbstractDatabaseCommand
18
{
19
    /**
20
     * @var array
21
     */
22
    protected $tableDefinitions = null;
23
24
    /**
25
     * @var array
26
     */
27
    protected $commandConfig = null;
28
29
    protected function configure()
30
    {
31
        $this
32
            ->setName('db:dump')
33
            ->addArgument('filename', InputArgument::OPTIONAL, 'Dump filename')
34
            ->addOption(
35
                'add-time',
36
                't',
37
                InputOption::VALUE_OPTIONAL,
38
                'Adds time to filename (only if filename was not provided)'
39
            )
40
            ->addOption(
41
                'compression',
42
                'c',
43
                InputOption::VALUE_REQUIRED,
44
                'Compress the dump file using one of the supported algorithms'
45
            )
46
            ->addOption(
47
                'xml',
48
                null,
49
                InputOption::VALUE_NONE,
50
                'Dump database in xml format'
51
            )
52
            ->addOption(
53
                'hex-blob',
54
                null,
55
                InputOption::VALUE_NONE,
56
                'Dump binary columns using hexadecimal notation (for example, "abc" becomes 0x616263)'
57
            )
58
            ->addOption(
59
                'only-command',
60
                null,
61
                InputOption::VALUE_NONE,
62
                'Print only mysqldump command. Do not execute'
63
            )
64
            ->addOption(
65
                'print-only-filename',
66
                null,
67
                InputOption::VALUE_NONE,
68
                'Execute and prints no output except the dump filename'
69
            )
70
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'do everything but the dump')
71
            ->addOption(
72
                'no-single-transaction',
73
                null,
74
                InputOption::VALUE_NONE,
75
                'Do not use single-transaction (not recommended, this is blocking)'
76
            )
77
            ->addOption(
78
                'human-readable',
79
                null,
80
                InputOption::VALUE_NONE,
81
                'Use a single insert with column names per row. Useful to track database differences. Use db:import ' .
82
                '--optimize for speeding up the import.'
83
            )
84
            ->addOption(
85
                'add-routines',
86
                null,
87
                InputOption::VALUE_NONE,
88
                'Include stored routines in dump (procedures & functions)'
89
            )
90
            ->addOption('stdout', null, InputOption::VALUE_NONE, 'Dump to stdout')
91
            ->addOption(
92
                'strip',
93
                's',
94
                InputOption::VALUE_OPTIONAL,
95
                'Tables to strip (dump only structure of those tables)'
96
            )
97
            ->addOption('exclude', 'e', InputOption::VALUE_OPTIONAL, 'Tables to exclude from the dump')
98
            ->addOption('include', 'i', InputOption::VALUE_OPTIONAL, 'Tables to include in the dump')
99
            ->addOption('force', 'f', InputOption::VALUE_NONE, 'Do not prompt if all options are defined')
100
            ->setDescription('Dumps database with mysqldump cli client according to informations from local.xml');
101
102
        $help = <<<HELP
103
Dumps configured magento database with `mysqldump`. You must have installed
104
the MySQL client tools.
105
106
On debian systems run `apt-get install mysql-client` to do that.
107
108
The command reads app/etc/local.xml to find the correct settings.
109
110
See it in action: http://youtu.be/ttjZHY6vThs
111
112
- If you like to prepend a timestamp to the dump name the --add-time option
113
  can be used.
114
115
- The command comes with a compression function. Add i.e. `--compression=gz`
116
  to dump directly in gzip compressed file.
117
118
HELP;
119
        $this->setHelp($help);
120
    }
121
122
    /**
123
     * @return array
124
     *
125
     * @deprecated Use database helper
126
     */
127
    private function getTableDefinitions()
128
    {
129
        $this->commandConfig = $this->getCommandConfig();
130
131
        if (is_null($this->tableDefinitions)) {
132
            /* @var $dbHelper DatabaseHelper */
133
            $dbHelper = $this->getHelper('database');
134
135
            $this->tableDefinitions = $dbHelper->getTableDefinitions($this->commandConfig);
136
        }
137
138
        return $this->tableDefinitions;
139
    }
140
141
    /**
142
     * Generate help for table definitions
143
     *
144
     * @return string
145
     */
146
    public function getTableDefinitionHelp()
147
    {
148
        $messages = PHP_EOL;
149
        $this->commandConfig = $this->getCommandConfig();
150
        $messages .= <<<HELP
151
<comment>Strip option</comment>
152
 If you like to skip data of some tables you can use the --strip option.
153
 The strip option creates only the structure of the defined tables and
154
 forces `mysqldump` to skip the data.
155
156
 Separate each table to strip by a space.
157
 You can use wildcards like * and ? in the table names to strip multiple
158
 tables. In addition you can specify pre-defined table groups, that start
159
 with an
160
161
 Example: "dataflow_batch_export unimportant_module_* @log
162
163
    $ n98-magerun.phar db:dump --strip="@stripped"
164
165
<comment>Available Table Groups</comment>
166
167
HELP;
168
169
        $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...
170
        $list = array();
171
        $maxNameLen = 0;
172
        foreach ($definitions as $id => $definition) {
173
            $name = '@' . $id;
174
            $description = isset($definition['description']) ? $definition['description'] . '.' : '';
175
            $nameLen = strlen($name);
176
            if ($nameLen > $maxNameLen) {
177
                $maxNameLen = $nameLen;
178
            }
179
            $list[] = array($name, $description);
180
        }
181
182
        $decrSize = 78 - $maxNameLen - 3;
183
184
        foreach ($list as $entry) {
185
            list($name, $description) = $entry;
186
            $delta = max(0, $maxNameLen - strlen($name));
187
            $spacer = $delta ? str_repeat(' ', $delta) : '';
188
            $buffer = wordwrap($description, $decrSize);
189
            $buffer = strtr($buffer, array("\n" => "\n" . str_repeat(' ', 3 + $maxNameLen)));
190
            $messages .= sprintf(" <info>%s</info>%s  %s\n", $name, $spacer, $buffer);
191
        }
192
193
        $messages .= <<<HELP
194
195
Extended: https://github.com/netz98/n98-magerun/wiki/Stripped-Database-Dumps
196
HELP;
197
198
        return $messages;
199
    }
200
201
    public function getHelp()
202
    {
203
        return
204
            parent::getHelp() . PHP_EOL
205
            . $this->getCompressionHelp() . PHP_EOL
206
            . $this->getTableDefinitionHelp();
207
    }
208
209
    /**
210
     * @param InputInterface $input
211
     * @param OutputInterface $output
212
     *
213
     * @return int|void
214
     */
215
    protected function execute(InputInterface $input, OutputInterface $output)
216
    {
217
        // communicate early what is required for this command to run (is enabled)
218
        $enabler = new Enabler($this);
219
        $enabler->functionExists('exec');
220
        $enabler->functionExists('passthru');
221
        $enabler->operatingSystemIsNotWindows();
222
223
        // TODO(tk): Merge the DatabaseHelper, detectDbSettings is within abstract database command base class
224
        $this->detectDbSettings($output);
225
226
        if ($this->nonCommandOutput($input)) {
227
            $this->writeSection($output, 'Dump MySQL Database');
228
        }
229
230
        list($fileName, $execs) = $this->createExecsArray($input, $output);
231
232
        $this->runExecs($execs, $fileName, $input, $output);
233
    }
234
235
    /**
236
     * @param InputInterface $input
237
     * @param OutputInterface $output
238
     * @return array
239
     */
240
    private function createExecsArray(InputInterface $input, OutputInterface $output)
241
    {
242
        $execs = array();
0 ignored issues
show
Unused Code introduced by
$execs is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
243
244
        $stripTables = $this->stripTables($input, $output);
245
246
        /* @var $database DatabaseHelper */
247
        $database = $this->getDatabaseHelper();
248
249
        if ($input->getOption('exclude') && $input->getOption('include')) {
250
            throw new InvalidArgumentException('Cannot specify --include with --exclude');
251
        }
252
253
        $excludeTables = array();
254
        if ($input->getOption('exclude')) {
255
            $excludeTables = $database->resolveTables(
256
                explode(' ', $input->getOption('exclude')),
257
                $database->getTableDefinitions($this->getCommandConfig())
258
            );
259
            if ($this->nonCommandOutput($input)) {
260
                $output->writeln(
261
                    sprintf('<comment>Excluded: <info>%s</info></comment>', implode(' ', $excludeTables))
262
                );
263
            }
264
        }
265
266
        if ($input->getOption('include')) {
267
            $includeTables = $database->resolveTables(
268
                explode(' ', $input->getOption('include')),
269
                $database->getTableDefinitions($this->getCommandConfig())
270
            );
271
            $excludeTables = array_diff($database->getTables(), $includeTables);
272
            if ($this->nonCommandOutput($input)) {
273
                $output->writeln(
274
                    sprintf('<comment>Included: <info>%s</info></comment>', implode(' ', $includeTables))
275
                );
276
            }
277
        }
278
279
        $dumpOptions = '';
280
        if (!$input->getOption('no-single-transaction')) {
281
            $dumpOptions = '--single-transaction --quick ';
282
        }
283
284
        if ($input->getOption('human-readable')) {
285
            $dumpOptions .= '--complete-insert --skip-extended-insert ';
286
        }
287
288
        if ($input->getOption('add-routines')) {
289
            $dumpOptions .= '--routines ';
290
        }
291
292
        if ($input->getOption('xml')) {
293
            $dumpOptions .= '--xml ';
294
        }
295
296
        if ($input->getOption('hex-blob')) {
297
            $dumpOptions .= '--hex-blob ';
298
        }
299
300
        $ignore = '';
301
        foreach (array_merge($excludeTables, $stripTables) as $tableName) {
302
            $ignore .= '--ignore-table=' . $this->dbSettings['dbname'] . '.' . $tableName . ' ';
303
        }
304
305
        $mysqlClientToolConnectionString = $database->getMysqlClientToolConnectionString();
306
307
        $compressor = $this->getCompressor($input->getOption('compression'));
0 ignored issues
show
Deprecated Code introduced by
The method N98\Magento\Command\Data...ommand::getCompressor() has been deprecated with message: Since 1.97.29; use AbstractCompressor::create() instead

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...
308
        $fileName = $this->getFileName($input, $output, $compressor);
309
310
        if (count($stripTables) > 0) {
311
            // dump structure for strip-tables
312
            $exec = 'mysqldump ' . $dumpOptions . '--no-data ' . $mysqlClientToolConnectionString;
313
            $exec .= ' ' . implode(' ', $stripTables);
314
            $exec .= $this->postDumpPipeCommands();
315
            $exec = $compressor->getCompressingCommand($exec);
316
            if (!$input->getOption('stdout')) {
317
                $exec .= ' > ' . escapeshellarg($fileName);
318
            }
319
            $execsArray[] = $exec;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$execsArray was never initialized. Although not strictly required by PHP, it is generally a good practice to add $execsArray = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
320
        }
321
322
        // dump data for all other tables
323
        $exec = 'mysqldump ' . $dumpOptions . $mysqlClientToolConnectionString . ' ' . $ignore;
324
        $exec .= $this->postDumpPipeCommands();
325
        $exec = $compressor->getCompressingCommand($exec);
326
        if (!$input->getOption('stdout')) {
327
            $exec .= (count($stripTables) > 0 ? ' >> ' : ' > ') . escapeshellarg($fileName);
328
        }
329
        $execsArray[] = $exec;
0 ignored issues
show
Bug introduced by
The variable $execsArray does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
330
        return array($fileName, $execsArray);
331
    }
332
333
    /**
334
     * @param array $execs
335
     * @param string $fileName
336
     * @param InputInterface $input
337
     * @param OutputInterface $output
338
     */
339
    private function runExecs(array $execs, $fileName, InputInterface $input, OutputInterface $output)
340
    {
341
        if ($input->getOption('only-command') && !$input->getOption('print-only-filename')) {
342
            foreach ($execs as $command) {
343
                $output->writeln($command);
344
            }
345
        } else {
346
            if ($this->nonCommandOutput($input)) {
347
                $output->writeln(
348
                    '<comment>Start dumping database <info>' . $this->dbSettings['dbname'] .
349
                    '</info> to file <info>' . $fileName . '</info>'
350
                );
351
            }
352
353
            $commands = $input->getOption('dry-run') ? array() : $execs;
354
355
            foreach ($commands as $command) {
356
                if (!$this->runExec($command, $input, $output)) {
357
                    return;
358
                }
359
            }
360
361
            if (!$input->getOption('stdout') && !$input->getOption('print-only-filename')) {
362
                $output->writeln('<info>Finished</info>');
363
            }
364
        }
365
366
        if ($input->getOption('print-only-filename')) {
367
            $output->writeln($fileName);
368
        }
369
    }
370
371
    /**
372
     * @param string $command
373
     * @param InputInterface $input
374
     * @param OutputInterface $output
375
     * @return bool
376
     */
377
    private function runExec($command, InputInterface $input, OutputInterface $output)
378
    {
379
        $commandOutput = '';
380
381
        if ($input->getOption('stdout')) {
382
            passthru($command, $returnValue);
383
        } else {
384
            Exec::run($command, $commandOutput, $returnValue);
385
        }
386
387
        if ($returnValue > 0) {
388
            $output->writeln('<error>' . $commandOutput . '</error>');
389
            $output->writeln('<error>Return Code: ' . $returnValue . '. ABORTED.</error>');
390
391
            return false;
392
        }
393
394
        return true;
395
    }
396
397
    /**
398
     * @param InputInterface $input
399
     * @param OutputInterface $output
400
     * @return array
401
     */
402
    private function stripTables(InputInterface $input, OutputInterface $output)
403
    {
404
        if (!$input->getOption('strip')) {
405
            return array();
406
        }
407
408
        $stripTables = $this->getDatabaseHelper()->resolveTables(
409
            explode(' ', $input->getOption('strip')),
410
            $this->getDatabaseHelper()->getTableDefinitions($this->getCommandConfig())
411
        );
412
413
        if ($this->nonCommandOutput($input)) {
414
            $output->writeln(
415
                sprintf('<comment>No-data export for: <info>%s</info></comment>', implode(' ', $stripTables))
416
            );
417
        }
418
419
        return $stripTables;
420
    }
421
422
    /**
423
     * Commands which filter mysql data. Piped to mysqldump command
424
     *
425
     * @return string
426
     */
427
    protected function postDumpPipeCommands()
428
    {
429
        return ' | LANG=C LC_CTYPE=C LC_ALL=C sed -e ' . escapeshellarg('s/DEFINER[ ]*=[ ]*[^*]*\*/\*/');
430
    }
431
432
    /**
433
     * @param InputInterface $input
434
     * @param OutputInterface $output
435
     * @param Compressor $compressor
436
     *
437
     * @return string
438
     */
439
    protected function getFileName(InputInterface $input, OutputInterface $output, Compressor $compressor)
440
    {
441
        if ($input->getOption('xml')) {
442
            $nameExtension = '.xml';
443
        } else {
444
            $nameExtension = '.sql';
445
        }
446
447
        $optionAddTime = $input->getOption('add-time');
448
        $namePrefix = '';
449
        $nameSuffix = '';
450
        if ($optionAddTime !== null) {
451
            $timeStamp = date('Y-m-d_His');
452
453
            if (in_array($optionAddTime, array('suffix', true), true)) {
454
                $nameSuffix = '_' . $timeStamp;
455
            } elseif ($optionAddTime === 'prefix') {
456
                $namePrefix = $timeStamp . '_';
457
            } elseif ($optionAddTime !== 'no') {
458
                throw new InvalidArgumentException(
459
                    sprintf(
460
                        'Invalid --add-time value %s, possible values are none (for) "suffix", "prefix" or "no"',
461
                        var_export($optionAddTime, true)
462
                    )
463
                );
464
            }
465
        }
466
467
        if (
468
            (
469
                ($fileName = $input->getArgument('filename')) === null
470
                || ($isDir = is_dir($fileName))
471
            )
472
            && !$input->getOption('stdout')
473
        ) {
474
            $defaultName = VerifyOrDie::filename(
475
                $namePrefix . $this->dbSettings['dbname'] . $nameSuffix . $nameExtension
476
            );
477
            if (isset($isDir) && $isDir) {
478
                $defaultName = rtrim($fileName, '/') . '/' . $defaultName;
479
            }
480
            if (!$input->getOption('force')) {
481
                /** @var DialogHelper $dialog */
482
                $dialog = $this->getHelper('dialog');
483
                $fileName = $dialog->ask(
484
                    $output,
485
                    '<question>Filename for SQL dump:</question> [<comment>' . $defaultName . '</comment>]',
486
                    $defaultName
487
                );
488
            } else {
489
                $fileName = $defaultName;
490
            }
491
        } else {
492
            if ($optionAddTime) {
493
                $pathParts = pathinfo($fileName);
494
                $fileName = ($pathParts['dirname'] == '.' ? '' : $pathParts['dirname'] . '/') .
495
                    $namePrefix . $pathParts['filename'] . $nameSuffix . '.' . $pathParts['extension'];
496
            }
497
        }
498
499
        $fileName = $compressor->getFileName($fileName);
500
501
        return $fileName;
502
    }
503
504
    /**
505
     * @param InputInterface $input
506
     * @return bool
507
     */
508
    private function nonCommandOutput(InputInterface $input)
509
    {
510
        return
511
            !$input->getOption('stdout')
512
            && !$input->getOption('only-command')
513
            && !$input->getOption('print-only-filename');
514
    }
515
}
516