Completed
Push — master ( 2e4bb9...57d19c )
by Gaetano
07:25
created

MigrateCommand   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 371
Duplicated Lines 14.82 %

Coupling/Cohesion

Components 2
Dependencies 14

Test Coverage

Coverage 9.88%

Importance

Changes 0
Metric Value
dl 55
loc 371
c 0
b 0
f 0
wmc 51
lcom 2
cbo 14
ccs 17
cts 172
cp 0.0988
rs 7.92

9 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 30 1
F execute() 20 153 20
B buildMigrationsList() 16 38 11
A printMigrationsList() 0 26 4
A askForConfirmation() 19 19 4
A writeln() 0 6 2
A setOutput() 0 4 1
A setVerbosity() 0 4 1
B createChildProcessArgs() 0 35 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MigrateCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MigrateCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Kaliop\eZMigrationBundle\Core\MigrationService;
6
use Symfony\Component\Console\Input\ArrayInput;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Output\OutputInterface;
9
use Symfony\Component\Console\Input\InputOption;
10
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
11
use Kaliop\eZMigrationBundle\API\Value\Migration;
12
use Kaliop\eZMigrationBundle\API\Exception\AfterMigrationExecutionException;
13
use Symfony\Component\Process\ProcessBuilder;
14
use Symfony\Component\Process\PhpExecutableFinder;
15
use Symfony\Component\Console\Helper\Table;
16
use Symfony\Component\Console\Question\ConfirmationQuestion;
17
18
/**
19
 * Command to execute the available migration definitions.
20
 */
21
class MigrateCommand extends AbstractCommand
22
{
23
    // in between QUIET and NORMAL
24
    const VERBOSITY_CHILD = 0.5;
25
    /** @var OutputInterface $output */
26
    protected $output;
27
    protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
28
29
    const COMMAND_NAME = 'kaliop:migration:migrate';
30
31
    /**
32
     * Set up the command.
33
     *
34
     * Define the name, options and help text.
35
     */
36 76
    protected function configure()
37
    {
38 76
        parent::configure();
39
40
        $this
41 76
            ->setName(self::COMMAND_NAME)
42 76
            ->setAliases(array('kaliop:migration:update'))
43 76
            ->setDescription('Execute available migration definitions.')
44 76
            // nb: when adding options, remember to forward them to sub-commands executed in 'separate-process' mode
45
            ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)")
46 76
            ->addOption('clear-cache', 'c', InputOption::VALUE_NONE, "Clear the cache after the command finishes")
47 76
            ->addOption('default-language', 'l', InputOption::VALUE_REQUIRED, "Default language code that will be used if no language is provided in migration steps")
48 76
            ->addOption('force', 'f', InputOption::VALUE_NONE, "Force (re)execution of migrations already DONE, SKIPPED or FAILED. Use with great care!")
49 76
            ->addOption('ignore-failures', 'i', InputOption::VALUE_NONE, "Keep executing migrations even if one fails")
50 76
            ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question")
51 76
            ->addOption('no-transactions', 'u', InputOption::VALUE_NONE, "Do not use a repository transaction to wrap each migration. Unsafe, but needed for legacy slot handlers")
52 76
            ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "The directory or file to load the migration definitions from")
53 76
            ->addOption('separate-process', 'p', InputOption::VALUE_NONE, "Use a separate php process to run each migration. Safe if your migration leak memory. A tad slower")
54 76
            ->addOption('child', null, InputOption::VALUE_NONE, "*DO NOT USE* Internal option for when forking separate processes")
55 76
            ->setHelp(<<<EOT
56
The <info>kaliop:migration:migrate</info> command loads and executes migrations:
57
58
    <info>./ezpublish/console kaliop:migration:migrate</info>
59
60
You can optionally specify the path to migration definitions with <info>--path</info>:
61
62
    <info>./ezpublish/console kaliop:migrations:migrate --path=/path/to/bundle/version_directory --path=/path/to/bundle/version_directory/single_migration_file</info>
63
EOT
64 76
            );
65
    }
66
67
    /**
68
     * Execute the command.
69
     *
70
     * @param InputInterface $input
71
     * @param OutputInterface $output
72
     * @return null|int null or 0 if everything went fine, or an error code
73
     */
74
    protected function execute(InputInterface $input, OutputInterface $output)
75
    {
76
        $start = microtime(true);
77
78
        $this->setOutput($output);
79
        $this->setVerbosity($output->getVerbosity());
80
81
        if ($input->getOption('child')) {
82
            $this->setVerbosity(self::VERBOSITY_CHILD);
83
        }
84
85
        $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output);
86
87
        $migrationService = $this->getMigrationService();
88
89
        $toExecute = $this->buildMigrationsList($input->getOption('path'), $migrationService);
90
91
        if (!count($toExecute)) {
92
            $output->writeln('<info>No migrations to execute</info>');
93
            return 0;
94
        }
95
96
        $this->printMigrationsList($toExecute, $input, $output);
97
98
        // ask user for confirmation to make changes
99
        if (!$this->askForConfirmation($input, $output)) {
100
            return 0;
101
        }
102
103
        if ($input->getOption('separate-process')) {
104
            $kernel = $this->getContainer()->get('kernel');
0 ignored issues
show
Unused Code introduced by
$kernel 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...
105
            $builder = new ProcessBuilder();
106
            $executableFinder = new PhpExecutableFinder();
107
            if (false !== $php = $executableFinder->find()) {
108
                $builder->setPrefix($php);
109
            }
110
            $builderArgs = $this->createChildProcessArgs($input);
111
        }
112
113
        $executed = 0;
114
        $failed = 0;
115
        $skipped = 0;
116
117
        /** @var MigrationDefinition $migrationDefinition */
118
        foreach ($toExecute as $name => $migrationDefinition) {
119
120
            // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
121
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
122
                $output->writeln("<comment>Skipping $name</comment>\n");
123
                $skipped++;
124
                continue;
125
            }
126
127
            $this->writeln("<info>Processing $name</info>");
128
129
            if ($input->getOption('separate-process')) {
130
131
                $process = $builder
0 ignored issues
show
Bug introduced by
The variable $builder 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...
132
                    ->setArguments(array_merge($builderArgs, array('--path=' . $migrationDefinition->path)))
0 ignored issues
show
Bug introduced by
The variable $builderArgs 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...
133
                    ->getProcess();
134
135
                $this->writeln('<info>Executing: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
136
137
                // allow long migrations processes by default
138
                $process->setTimeout(86400);
139
                // and give immediate feedback to the user
140
                $process->run(
141
                    function($type, $buffer) {
142
                        echo $buffer;
143
                    }
144
                );
145
146
                try {
147
148
                    if (!$process->isSuccessful()) {
149
                        throw new \Exception($process->getErrorOutput());
150
                    }
151
152
                    // There are cases where the separate process dies halfway but does not return a non-zero code.
153
                    // That's why we should double-check here if the migration is still tagged as 'started'...
154
                    /** @var Migration $migration */
155
                    $migration = $migrationService->getMigration($migrationDefinition->name);
156
157 View Code Duplication
                    if (!$migration) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
158
                        // q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace...
159
                        throw new \Exception("After the separate process charged to execute the migration finished, the migration can not be found in the database any more.");
160
                    } else if ($migration->status == Migration::STATUS_STARTED) {
161
                        $errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution.";
162
                        $migrationService->endMigration(New Migration(
163
                            $migration->name,
164
                            $migration->md5,
165
                            $migration->path,
166
                            $migration->executionDate,
167
                            Migration::STATUS_FAILED,
168
                            ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg)
169
                        ));
170
                        throw new \Exception($errorMsg);
171
                    }
172
173
                    $executed++;
174
175
                } catch (\Exception $e) {
176
                    if ($input->getOption('ignore-failures')) {
177
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
178
                        $failed++;
179
                        continue;
180
                    }
181
                    if ($e instanceof AfterMigrationExecutionException) {
182
                        $output->writeln("\n<error>Failure after migration end! Reason: " . $e->getMessage() . "</error>");
183
                    } else {
184
                        $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
185
                    }
186
                    return 1;
187
                }
188
189
            } else {
190
191
                try {
192
                    $migrationService->executeMigration(
193
                        $migrationDefinition,
194
                        !$input->getOption('no-transactions'),
195
                        $input->getOption('default-language'),
196
                        $input->getOption('admin-login')
197
                    );
198
                    $executed++;
199
                } catch (\Exception $e) {
200
                    if ($input->getOption('ignore-failures')) {
201
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
202
                        $failed++;
203
                        continue;
204
                    }
205
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
206
                    return 1;
207
                }
208
209
            }
210
        }
211
212 View Code Duplication
        if ($input->getOption('clear-cache')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
213
            $command = $this->getApplication()->find('cache:clear');
214
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
215
            $command->run($inputArray, $output);
216
        }
217
218
        $time = microtime(true) - $start;
219
        $this->writeln("Executed $executed migrations, failed $failed, skipped $skipped");
220
        if ($input->getOption('separate-process')) {
221
            // in case of using subprocesses, we can not measure max memory used
222
            $this->writeln("Time taken: ".sprintf('%.2f', $time)." secs");
223
        } else {
224
            $this->writeln("Time taken: ".sprintf('%.2f', $time)." secs, memory: ".sprintf('%.2f', (memory_get_peak_usage(true) / 1000000)). ' MB');
225
        }
226
    }
227
228
    /**
229
     * @param string[] $paths
230
     * @param MigrationService $migrationService
231
     * @param bool $force when true, look not only for TODO migrations, but also DONE, SKIPPED, FAILED ones (we still omit STARTED and SUSPENDED ones)
232
     * @return MigrationDefinition[]
233
     *
234
     * @todo this does not scale well with many definitions or migrations
235
     */
236
    protected function buildMigrationsList($paths, $migrationService, $force = false)
237
    {
238
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
239
        $migrations = $migrationService->getMigrations();
240
241
        $allowedStatuses = array(Migration::STATUS_TODO);
242
        if ($force) {
243
            $allowedStatuses = array_merge($allowedStatuses, array(Migration::STATUS_DONE, Migration::STATUS_FAILED, Migration::STATUS_SKIPPED));
244
        }
245
246
        // filter away all migrations except 'to do' ones
247
        $toExecute = array();
248
        foreach ($migrationDefinitions as $name => $migrationDefinition) {
249 View Code Duplication
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && in_array($migration->status, $allowedStatuses))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
250
                $toExecute[$name] = $migrationService->parseMigrationDefinition($migrationDefinition);
251
            }
252
        }
253
254
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
255
        // found by the loader
256 View Code Duplication
        if (empty($paths)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
257
            foreach ($migrations as $migration) {
258
                if (in_array($migration->status, $allowedStatuses) && !isset($toExecute[$migration->name])) {
259
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
260
                    if (count($migrationDefinitions)) {
261
                        $migrationDefinition = reset($migrationDefinitions);
262
                        $toExecute[$migration->name] = $migrationService->parseMigrationDefinition($migrationDefinition);
263
                    } else {
264
                        // q: shall we raise a warning here ?
265
                    }
266
                }
267
            }
268
        }
269
270
        ksort($toExecute);
271
272
        return $toExecute;
273
    }
274
275
    /**
276
     * @param MigrationDefinition[] $toExecute
277
     * @param InputInterface $input
278
     * @param OutputInterface $output
279
     *
280
     * @todo use a more compact output when there are *many* migrations
281
     */
282
    protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output)
283
    {
284
        $data = array();
285
        $i = 1;
286
        foreach ($toExecute as $name => $migrationDefinition) {
287
            $notes = '';
288
            if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
289
                $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
290
            }
291
            $data[] = array(
292
                $i++,
293
                $name,
294
                $notes
295
            );
296
        }
297
298
        if (!$input->getOption('child')) {
299
            $table = new Table($output);
300
            $table
301
                ->setHeaders(array('#', 'Migration', 'Notes'))
302
                ->setRows($data);
303
            $table->render();
304
        }
305
306
        $this->writeln('');
307
    }
308
309 View Code Duplication
    protected function askForConfirmation(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...
310
    {
311
        if ($input->isInteractive() && !$input->getOption('no-interaction')) {
312
            $dialog = $this->getHelperSet()->get('question');
313
            if (!$dialog->ask(
314
                $input,
315
                $output,
316
                new ConfirmationQuestion('<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>', false)
317
            )
318
            ) {
319
                $output->writeln('<error>Migration execution cancelled!</error>');
320
                return 0;
321
            }
322
        } else {
323
            $this->writeln("=============================================\n");
324
        }
325
326
        return 1;
327
    }
328
329
    /**
330
     * Small tricks to allow us to lower verbosity between NORMAL and QUIET and have a decent writeln API, even with old SF versions
331
     * @param $message
332
     * @param int $verbosity
333
     */
334
    protected function writeln($message, $verbosity = OutputInterface::VERBOSITY_NORMAL)
335
    {
336
        if ($this->verbosity >= $verbosity) {
337
            $this->output->writeln($message);
338
        }
339
    }
340
341
    protected function setOutput(OutputInterface $output)
342
    {
343
        $this->output = $output;
344
    }
345
346
    protected function setVerbosity($verbosity)
347
    {
348
        $this->verbosity = $verbosity;
349
    }
350
351
    /**
352
     * Returns the command-line arguments needed to execute a migration in a separate subprocess (omitting 'path')
353
     * @param InputInterface $input
354
     * @return array
355
     */
356
    protected function createChildProcessArgs(InputInterface $input)
357
    {
358
        $kernel = $this->getContainer()->get('kernel');
359
360
        // mandatory args and options
361
        $builderArgs = array(
362
            $_SERVER['argv'][0], // sf console
363
            self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
364
            '--env=' . $kernel->getEnvironment(), // sf env
365
            '--child'
366
        );
367
        // sf/ez env options
368
        if (!$kernel->isDebug()) {
369
            $builderArgs[] = '--no-debug';
370
        }
371
        if ($input->getOption('siteaccess')) {
372
            $builderArgs[]='--siteaccess='.$input->getOption('siteaccess');
373
        }
374
        // 'optional' options
375
        // note: options 'clear-cache', 'ignore-failures', 'no-interaction', 'path' and 'separate-process' we never propagate
376
        if ($input->getOption('admin-login')) {
377
            $builderArgs[] = '--admin-login=' . $input->getOption('admin-login');
378
        }
379
        if ($input->getOption('default-language')) {
380
            $builderArgs[] = '--default-language=' . $input->getOption('default-language');
381
        }
382
        if ($input->getOption('force')) {
383
            $builderArgs[] = '--force';
384
        }
385
        if ($input->getOption('no-transactions')) {
386
            $builderArgs[] = '--no-transactions';
387
        }
388
389
        return $builderArgs;
390
    }
391
}
392