Completed
Push — master ( 561e2a...f3a4fe )
by Christian
04:31
created

DumpCommand::excludeTables()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 16
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 16
loc 16
rs 9.4285
cc 3
eloc 8
nc 3
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\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
        parent::configure();
32
        $this
33
            ->setName('db:dump')
34
            ->addArgument('filename', InputArgument::OPTIONAL, 'Dump filename')
35
            ->addOption(
36
                'add-time',
37
                't',
38
                InputOption::VALUE_OPTIONAL,
39
                'Append or prepend a timestamp to filename if a filename is provided. ' .
40
                'Possible values are "suffix", "prefix" or "no".'
41
            )
42
            ->addOption(
43
                'compression',
44
                'c',
45
                InputOption::VALUE_REQUIRED,
46
                'Compress the dump file using one of the supported algorithms'
47
            )
48
            ->addOption(
49
                'only-command',
50
                null,
51
                InputOption::VALUE_NONE,
52
                'Print only mysqldump command. Do not execute'
53
            )
54
            ->addOption(
55
                'print-only-filename',
56
                null,
57
                InputOption::VALUE_NONE,
58
                'Execute and prints no output except the dump filename'
59
            )
60
            ->addOption(
61
                'dry-run',
62
                null,
63
                InputOption::VALUE_NONE,
64
                'do everything but the dump'
65
            )
66
            ->addOption(
67
                'no-single-transaction',
68
                null,
69
                InputOption::VALUE_NONE,
70
                'Do not use single-transaction (not recommended, this is blocking)'
71
            )
72
            ->addOption(
73
                'human-readable',
74
                null,
75
                InputOption::VALUE_NONE,
76
                'Use a single insert with column names per row. Useful to track database differences. Use db:import ' .
77
                '--optimize for speeding up the import.'
78
            )
79
            ->addOption(
80
                'add-routines',
81
                null,
82
                InputOption::VALUE_NONE,
83
                'Include stored routines in dump (procedures & functions)'
84
            )
85
            ->addOption('stdout', null, InputOption::VALUE_NONE, 'Dump to stdout')
86
            ->addOption(
87
                'strip',
88
                's',
89
                InputOption::VALUE_OPTIONAL,
90
                'Tables to strip (dump only structure of those tables)'
91
            )
92
            ->addOption(
93
                'exclude',
94
                'e',
95
                InputOption::VALUE_OPTIONAL,
96
                'Tables to exclude from the dump'
97
            )
98
            ->addOption(
99
                'force',
100
                'f',
101
                InputOption::VALUE_NONE,
102
                'Do not prompt if all options are defined'
103
            )
104
            ->setDescription('Dumps database with mysqldump cli client');
105
106
        $help = <<<HELP
107
Dumps configured magento database with `mysqldump`. You must have installed
108
the MySQL client tools.
109
110
On debian systems run `apt-get install mysql-client` to do that.
111
112
The command reads app/etc/env.php to find the correct settings.
113
114
See it in action: http://youtu.be/ttjZHY6vThs
115
116
- If you like to prepend a timestamp to the dump name the --add-time option
117
  can be used.
118
119
- The command comes with a compression function. Add i.e. `--compression=gz`
120
  to dump directly in gzip compressed file.
121
122
HELP;
123
        $this->setHelp($help);
124
    }
125
126
    /**
127
     * @return array
128
     *
129
     * @deprecated Use database helper
130
     */
131
    private function getTableDefinitions()
132
    {
133
        $this->commandConfig = $this->getCommandConfig();
134
135
        if (is_null($this->tableDefinitions)) {
136
            /* @var $dbHelper DatabaseHelper */
137
            $dbHelper = $this->getHelper('database');
138
139
            $this->tableDefinitions = $dbHelper->getTableDefinitions($this->commandConfig);
140
        }
141
142
        return $this->tableDefinitions;
143
    }
144
145
    /**
146
     * Generate help for table definitions
147
     *
148
     * @return string
149
     */
150
    public function getTableDefinitionHelp()
151
    {
152
        $messages = PHP_EOL;
153
        $this->commandConfig = $this->getCommandConfig();
154
        $messages .= <<<HELP
155
<comment>Strip option</comment>
156
 If you like to skip data of some tables you can use the --strip option.
157
 The strip option creates only the structure of the defined tables and
158
 forces `mysqldump` to skip the data.
159
160
 Separate each table to strip by a space.
161
 You can use wildcards like * and ? in the table names to strip multiple
162
 tables. In addition you can specify pre-defined table groups, that start
163
 with an
164
165
 Example: "dataflow_batch_export unimportant_module_* @log
166
167
    $ n98-magerun.phar db:dump --strip="@stripped"
168
169
<comment>Available Table Groups</comment>
170
171
HELP;
172
173
        $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...
174
        $list = array();
175
        $maxNameLen = 0;
176
        foreach ($definitions as $id => $definition) {
177
            $name = '@' . $id;
178
            $description = isset($definition['description']) ? $definition['description'] . '.' : '';
179
            $nameLen = strlen($name);
180
            if ($nameLen > $maxNameLen) {
181
                $maxNameLen = $nameLen;
182
            }
183
            $list[] = array($name, $description);
184
        }
185
186
        $decrSize = 78 - $maxNameLen - 3;
187
188
        foreach ($list as $entry) {
189
            list($name, $description) = $entry;
190
            $delta = max(0, $maxNameLen - strlen($name));
191
            $spacer = $delta ? str_repeat(' ', $delta) : '';
192
            $buffer = wordwrap($description, $decrSize);
193
            $buffer = strtr($buffer, array("\n" => "\n" . str_repeat(' ', 3 + $maxNameLen)));
194
            $messages .= sprintf(" <info>%s</info>%s  %s\n", $name, $spacer, $buffer);
195
        }
196
197
        $messages .= <<<HELP
198
199
Extended: https://github.com/netz98/n98-magerun/wiki/Stripped-Database-Dumps
200
HELP;
201
202
        return $messages;
203
    }
204
205
    public function getHelp()
206
    {
207
        return
208
            parent::getHelp() . PHP_EOL
209
            . $this->getCompressionHelp() . PHP_EOL
210
            . $this->getTableDefinitionHelp();
211
    }
212
213
    /**
214
     * @param InputInterface $input
215
     * @param OutputInterface $output
216
     *
217
     * @return int|void
218
     */
219
    protected function execute(InputInterface $input, OutputInterface $output)
220
    {
221
        // communicate early what is required for this command to run (is enabled)
222
        $enabler = new Enabler($this);
223
        $enabler->functionExists('exec');
224
        $enabler->functionExists('passthru');
225
        $enabler->operatingSystemIsNotWindows();
226
227
        $this->detectDbSettings($output);
228
229
        if ($this->nonCommandOutput($input)) {
230
            $this->writeSection($output, 'Dump MySQL Database');
231
        }
232
233
        $execs = $this->createExecs($input, $output);
234
235
        $this->runExecs($execs, $input, $output);
236
    }
237
238
    /**
239
     * @param InputInterface $input
240
     * @param OutputInterface $output
241
     * @return Execs
242
     */
243
    private function createExecs(InputInterface $input, OutputInterface $output)
244
    {
245
        $execs = new Execs('mysqldump');
246
        $execs->setCompression($input->getOption('compression'));
247
        $execs->setFileName($this->getFileName($input, $output, $execs->getCompressor()));
248
249
        if (!$input->getOption('no-single-transaction')) {
250
            $execs->addOptions('--single-transaction --quick');
251
        }
252
253
        if ($input->getOption('human-readable')) {
254
            $execs->addOptions('--complete-insert --skip-extended-insert ');
255
        }
256
257
        if ($input->getOption('add-routines')) {
258
            $execs->addOptions('--routines ');
259
        }
260
261
        /* @var $database DatabaseHelper */
262
        $database = $this->getDatabaseHelper();
263
264
        $mysqlClientToolConnectionString = $database->getMysqlClientToolConnectionString();
265
266
        $stripTables = $this->stripTables($input, $output);
267
        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...
268
            // dump structure for strip-tables
269
            $execs->add(
270
                '--no-data ' . $mysqlClientToolConnectionString .
271
                ' ' . implode(' ', $stripTables) . $this->postDumpPipeCommands()
272
            );
273
        }
274
275
        $excludeTables = $this->excludeTables($input, $output);
276
277
        // dump data for all other tables
278
        $ignore = '';
279
        foreach (array_merge($excludeTables, $stripTables) as $ignoreTable) {
280
            $ignore .= '--ignore-table=' . $this->dbSettings['dbname'] . '.' . $ignoreTable . ' ';
281
        }
282
283
        $execs->add($ignore . $mysqlClientToolConnectionString . $this->postDumpPipeCommands());
284
285
        return $execs;
286
    }
287
288
    /**
289
     * @param Execs $execs
290
     * @param InputInterface $input
291
     * @param OutputInterface $output
292
     */
293
    private function runExecs(Execs $execs, InputInterface $input, OutputInterface $output)
294
    {
295
        if ($input->getOption('only-command') && !$input->getOption('print-only-filename')) {
296
            foreach ($execs->getCommands() as $command) {
297
                $output->writeln($command);
298
            }
299
        } else {
300
            if ($this->nonCommandOutput($input)) {
301
                $output->writeln(
302
                    '<comment>Start dumping database <info>' . $this->dbSettings['dbname'] .
303
                    '</info> to file <info>' . $execs->getFileName() . '</info>'
304
                );
305
            }
306
307
            $commands = $input->getOption('dry-run') ? array() : $execs->getCommands();
308
309
            foreach ($commands as $command) {
310
                if (!$this->runExec($command, $input, $output)) {
311
                    return;
312
                }
313
            }
314
315
            if (!$input->getOption('stdout') && !$input->getOption('print-only-filename')) {
316
                $output->writeln('<info>Finished</info>');
317
            }
318
        }
319
320
        if ($input->getOption('print-only-filename')) {
321
            $output->writeln($execs->getFileName());
322
        }
323
    }
324
325
    /**
326
     * @param string $command
327
     * @param InputInterface $input
328
     * @param OutputInterface $output
329
     * @return bool
330
     */
331
    private function runExec($command, InputInterface $input, OutputInterface $output)
332
    {
333
        $commandOutput = '';
334
335
        if ($input->getOption('stdout')) {
336
            passthru($command, $returnValue);
337
        } else {
338
            Exec::run($command, $commandOutput, $returnValue);
339
        }
340
341
        if ($returnValue > 0) {
342
            $output->writeln('<error>' . $commandOutput . '</error>');
343
            $output->writeln('<error>Return Code: ' . $returnValue . '. ABORTED.</error>');
344
345
            return false;
346
        }
347
348
        return true;
349
    }
350
351
    /**
352
     * @param InputInterface $input
353
     * @param OutputInterface $output
354
     * @return array
355
     */
356 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...
357
    {
358
        if (!$input->getOption('strip')) {
359
            return array();
360
        }
361
362
        $stripTables = $this->resolveDatabaseTables($input->getOption('strip'));
363
364
        if ($this->nonCommandOutput($input)) {
365
            $output->writeln(
366
                sprintf('<comment>No-data export for: <info>%s</info></comment>', implode(' ', $stripTables))
367
            );
368
        }
369
370
        return $stripTables;
371
    }
372
373
    /**
374
     * @param InputInterface $input
375
     * @param OutputInterface $output
376
     * @return array
377
     */
378 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...
379
    {
380
        if (!$input->getOption('exclude')) {
381
            return array();
382
        }
383
384
        $excludeTables = $this->resolveDatabaseTables($input->getOption('exclude'));
385
386
        if ($this->nonCommandOutput($input)) {
387
            $output->writeln(
388
                sprintf('<comment>Excluded: <info>%s</info></comment>', implode(' ', $excludeTables))
389
            );
390
        }
391
392
        return $excludeTables;
393
    }
394
395
    /**
396
     * @param string $list space separated list of tables
397
     * @return array
398
     */
399
    private function resolveDatabaseTables($list)
400
    {
401
        $database = $this->getDatabaseHelper();
402
403
        return $database->resolveTables(
404
            explode(' ', $list),
405
            $database->getTableDefinitions($this->getCommandConfig())
406
        );
407
    }
408
409
    /**
410
     * Commands which filter mysql data. Piped to mysqldump command
411
     *
412
     * @return string
413
     */
414
    protected function postDumpPipeCommands()
415
    {
416
        return ' | LANG=C LC_CTYPE=C LC_ALL=C sed -e ' . escapeshellarg('s/DEFINER[ ]*=[ ]*[^*]*\*/\*/');
417
    }
418
419
    /**
420
     * @param InputInterface $input
421
     * @param OutputInterface $output
422
     * @param Compressor $compressor
423
     *
424
     * @return string
425
     */
426
    protected function getFileName(InputInterface $input, OutputInterface $output, Compressor $compressor)
427
    {
428
        $nameExtension = '.sql';
429
430
        $optionAddTime = $input->getOption('add-time');
431
        list($namePrefix, $nameSuffix) = $this->getFileNamePrefixSuffix($optionAddTime);
432
433
        if (
434
            (
435
                ($fileName = $input->getArgument('filename')) === null
436
                || ($isDir = is_dir($fileName))
437
            )
438
            && !$input->getOption('stdout')
439
        ) {
440
            $defaultName = VerifyOrDie::filename(
441
                $namePrefix . $this->dbSettings['dbname'] . $nameSuffix . $nameExtension
442
            );
443
            if (isset($isDir) && $isDir) {
444
                $defaultName = rtrim($fileName, '/') . '/' . $defaultName;
445
            }
446
            if (!$input->getOption('force')) {
447
                /** @var DialogHelper $dialog */
448
                $dialog = $this->getHelper('dialog');
449
                $fileName = $dialog->ask(
450
                    $output,
451
                    '<question>Filename for SQL dump:</question> [<comment>' . $defaultName . '</comment>]',
452
                    $defaultName
453
                );
454
            } else {
455
                $fileName = $defaultName;
456
            }
457
        } else {
458
            if ($optionAddTime) {
459
                $pathParts = pathinfo($fileName);
460
                $fileName = ($pathParts['dirname'] == '.' ? '' : $pathParts['dirname'] . '/') .
461
                    $namePrefix . $pathParts['filename'] . $nameSuffix . '.' . $pathParts['extension'];
462
            }
463
        }
464
465
        $fileName = $compressor->getFileName($fileName);
466
467
        return $fileName;
468
    }
469
470
    /**
471
     * @param null|bool|string $optionAddTime [optional] true for default "suffix", other string values: "prefix", "no"
472
     * @return array
473
     */
474
    private function getFileNamePrefixSuffix($optionAddTime = null)
475
    {
476
        $namePrefix = '';
477
        $nameSuffix = '';
478
        if ($optionAddTime === null) {
479
            return array($namePrefix, $nameSuffix);
480
        }
481
482
        $timeStamp = date('Y-m-d_His');
483
484
        if (in_array($optionAddTime, array('suffix', true), true)) {
485
            $nameSuffix = '_' . $timeStamp;
486
        } elseif ($optionAddTime === 'prefix') {
487
            $namePrefix = $timeStamp . '_';
488
        } elseif ($optionAddTime !== 'no') {
489
            throw new InvalidArgumentException(
490
                sprintf(
491
                    'Invalid --add-time value %s, possible values are none (for) "suffix", "prefix" or "no"',
492
                    var_export($optionAddTime, true)
493
                )
494
            );
495
        }
496
497
        return array($namePrefix, $nameSuffix);
498
    }
499
500
    /**
501
     * @param InputInterface $input
502
     * @return bool
503
     */
504
    private function nonCommandOutput(InputInterface $input)
505
    {
506
        return
507
            !$input->getOption('stdout')
508
            && !$input->getOption('only-command')
509
            && !$input->getOption('print-only-filename');
510
    }
511
}
512