Completed
Push — master ( 7d01eb...09ac33 )
by Gaetano
37:26
created

MigrateCommand   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 283
Duplicated Lines 4.95 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 74.75%

Importance

Changes 0
Metric Value
dl 14
loc 283
c 0
b 0
f 0
wmc 39
lcom 1
cbo 10
ccs 74
cts 99
cp 0.7475
rs 8.2857

5 Methods

Rating   Name   Duplication   Size   Complexity  
B configure() 0 33 1
F execute() 14 203 34
A writeln() 0 6 2
A setOutput() 0 4 1
A setVerbosity() 0 4 1

How to fix   Duplicated Code   

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:

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Symfony\Component\Console\Input\ArrayInput;
6
use Symfony\Component\Console\Input\InputInterface;
7
use Symfony\Component\Console\Output\OutputInterface;
8
use Symfony\Component\Console\Input\InputOption;
9
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
10
use Kaliop\eZMigrationBundle\API\Value\Migration;
11
use Symfony\Component\Process\ProcessBuilder;
12
use Symfony\Component\Process\PhpExecutableFinder;
13
use Symfony\Component\Process\Process;
14
15
/**
16
 * Command to execute the available migration definitions.
17
 */
18
class MigrateCommand extends AbstractCommand
19
{
20
    // in between QUIET and NORMAL
21
    const VERBOSITY_CHILD = 0.5;
22 20
    /** @var OutputInterface $output */
23
    protected $output;
24 20
    protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
25
26 20
    const COMMAND_NAME='kaliop:migration:migrate';
27 20
28 20
    /**
29 20
     * Set up the command.
30 20
     *
31 20
     * Define the name, options and help text.
32 20
     */
33 20
    protected function configure()
34
    {
35 20
        parent::configure();
36 20
37 20
        $this
38 20
            ->setName(self::COMMAND_NAME)
39 20
            ->setAliases(array('kaliop:migration:update'))
40 20
            ->setDescription('Execute available migration definitions.')
41 20
            ->addOption(
42
                'path',
43
                null,
44
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
45
                "The directory or file to load the migration definitions from"
46
            )
47
            // nb: when adding options, remember to forward them to sub-commands executed in 'separate-process' mode
48
            ->addOption('default-language', null, InputOption::VALUE_REQUIRED, "Default language code that will be used if no language is provided in migration steps")
49
            ->addOption('ignore-failures', null, InputOption::VALUE_NONE, "Keep executing migrations even if one fails")
50 20
            ->addOption('clear-cache', null, InputOption::VALUE_NONE, "Clear the cache after the command finishes")
51 20
            ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question")
52
            ->addOption('no-transactions', 'u', InputOption::VALUE_NONE, "Do not use a repository transaction to wrap each migration. Unsafe, but needed for legacy slot handlers")
53
            ->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
            ->addOption('child', null, InputOption::VALUE_NONE, "*DO NOT USE* Internal option for when forking separate processes")
55
            ->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 19
    <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 19
            );
65
    }
66 19
67 19
    /**
68 19
     * Execute the command.
69
     *
70
     * @param InputInterface $input
71 19
     * @param OutputInterface $output
72 19
     * @return null|int null or 0 if everything went fine, or an error code
73 19
     */
74 19
    protected function execute(InputInterface $input, OutputInterface $output)
0 ignored issues
show
Coding Style introduced by
execute uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
75 19
    {
76 19
        $this->setOutput($output);
77
        $this->setVerbosity($output->getVerbosity());
78
79
        if ($input->getOption('child')) {
80 19
            $this->setVerbosity(self::VERBOSITY_CHILD);
81
        }
82
83
        $migrationService = $this->getMigrationService();
84
85
        $paths = $input->getOption('path');
86
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
87
        $migrations = $migrationService->getMigrations();
88
89
        // filter away all migrations except 'to do' ones
90
        $toExecute = array();
91
        foreach($migrationDefinitions as $name => $migrationDefinition) {
92
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && $migration->status == Migration::STATUS_TODO)) {
93
                $toExecute[$name] = $migrationService->parseMigrationDefinition($migrationDefinition);
94 19
            }
95
        }
96 19
97
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
98
        // found by the loader
99
        if (empty($paths)) {
100
            foreach ($migrations as $migration) {
101 19
                if ($migration->status == Migration::STATUS_TODO && !isset($toExecute[$migration->name])) {
102
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
103 19
                    if (count($migrationDefinitions)) {
104 19
                        $migrationDefinition = reset($migrationDefinitions);
105 19
                        $toExecute[$migration->name] = $migrationService->parseMigrationDefinition($migrationDefinition);
106 19
                    } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
107 19
                        // q: shall we raise a warning here ?
108 7
                    }
109 7
                }
110 19
            }
111 19
        }
112 19
113
        ksort($toExecute);
114 19
115 19
        if (!count($toExecute)) {
116
            $output->writeln('<info>No migrations to execute</info>');
117 19
            return;
118
        }
119 19
120 19
        $this->writeln("\n <info>==</info> Migrations to be executed\n");
121 19
122
        $data = array();
123 19
        $i = 1;
124
        foreach($toExecute as $name => $migrationDefinition) {
125 19
            $notes = '';
126
            if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
127
                $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
128
            }
129
            $data[] = array(
130
                $i++,
131
                $name,
132
                $notes
133 19
            );
134
        }
135
136
        if (!$input->getOption('child')) {
137 19
            $table = $this->getHelperSet()->get('table');
138 19
            $table
139
                ->setHeaders(array('#', 'Migration', 'Notes'))
140 19
                ->setRows($data);
141
            $table->render($output);
142
        }
143 19
144 7
        $this->writeln('');
145 7
146
        // ask user for confirmation to make changes
147 View Code Duplication
        if ($input->isInteractive() && !$input->getOption('no-interaction')) {
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...
148 12
            $dialog = $this->getHelperSet()->get('dialog');
149
            if (!$dialog->askConfirmation(
150
                $output,
151 12
                '<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>',
152 12
                false
153 12
            )
154 12
            ) {
155 12
                $output->writeln('<error>Migration execution cancelled!</error>');
156 12
                return 0;
157 3
            }
158
        } else {
159
            $this->writeln("=============================================\n");
160
        }
161 3
162 3
        if ($input->getOption('separate-process')) {
163
            $builder = new ProcessBuilder();
164
            $executableFinder = new PhpExecutableFinder();
165 9
            if (false !== $php = $executableFinder->find()) {
166 16
                $builder->setPrefix($php);
167
            }
168 19
            // mandatory args and options
169
            $builderArgs = array(
170
                $_SERVER['argv'][0], // sf console
171
                self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
172 12
                '--env=' . $this->getContainer()->get('kernel')->getEnvironment(), // sf env
173 16
                '--child'
174
            );
175
            // 'optional' options
176
            // note: options 'clear-cache', 'ignore-failures' and 'no-transactions' we never propagate
177
            if ($input->getOption('default-language')) {
178
                $builderArgs[]='--default-language='.$input->getOption('default-language');
179
            }
180
            if ($input->getOption('no-transactions')) {
181
                $builderArgs[]='--no-transactions';
182
            }
183
        }
184
185
        /** @var MigrationDefinition $migrationDefinition */
186
        foreach($toExecute as $name => $migrationDefinition) {
187
188
            // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
189
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
190
                $output->writeln("<comment>Skipping $name</comment>\n");
191
                continue;
192
            }
193
194
            $this->writeln("<info>Processing $name</info>");
195
196
            if ($input->getOption('separate-process')) {
197
198
                $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...
199
                    ->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...
200
                    ->getProcess();
201
202
                $this->writeln('<info>Executing: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
203
204
                // allow long migrations processes by default
205
                $process->setTimeout(86400);
206
                // and give immediate feedback to the user
207
                $process->run(
208
                    function ($type, $buffer) {
209
                        echo $buffer;
210
                    }
211
                );
212
213
                try {
214
215
                    if (!$process->isSuccessful()) {
216
                        throw new \Exception($process->getErrorOutput());
217
                    }
218
219
                    // There are cases where the separate process dies halfway but does not return a non-zero code.
220
                    // That's why we should double-check here if the migration is still tagged as 'started'...
221
                    /** @var Migration $migration */
222
                    $migration = $migrationService->getMigration($migrationDefinition->name);
223
224
                    if (!$migration) {
225
                        // q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace...
226
                        throw new \Exception("After the separate process charged to execute the migration finished, the migration can not be found in the database any more.");
227
                    } else if ($migration->status == Migration::STATUS_STARTED) {
228
                        $errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution.";
229
                        $migrationService->endMigration(New Migration(
230
                            $migration->name,
231
                            $migration->md5,
232
                            $migration->path,
233
                            $migration->executionDate,
234
                            Migration::STATUS_FAILED,
235
                            ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg)
236
                        ));
237
                        throw new \Exception($errorMsg);
238
                    }
239
240
                } catch(\Exception $e) {
241
                    if ($input->getOption('ignore-failures')) {
242
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
243
                        continue;
244
                    }
245
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
246
                    return 1;
247
                }
248
249
            } else {
250
251
                try {
252
                    $migrationService->executeMigration(
253
                        $migrationDefinition,
254
                        !$input->getOption('no-transactions'),
255
                        $input->getOption('default-language')
256
                    );
257
                } catch(\Exception $e) {
258
                    if ($input->getOption('ignore-failures')) {
259
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
260
                        continue;
261
                    }
262
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
263
                    return 1;
264
                }
265
266
            }
267
268
            $this->writeln('');
269
        }
270
271
        if ($input->getOption('clear-cache')) {
272
            $command = $this->getApplication()->find('cache:clear');
273
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
274
            $command->run($inputArray, $output);
275
        }
276
    }
277
278
    /**
279
     * Small tricks to allow us to lower verbosity between NORMAL and QUIET and have a decent writeln API, even with old SF versions
280
     * @param $message
281
     * @param int $verbosity
282
     */
283
    protected function writeln($message, $verbosity=OutputInterface::VERBOSITY_NORMAL)
284
    {
285
        if ($this->verbosity >= $verbosity) {
286
            $this->output->writeln($message);
287
        }
288
    }
289
290
    protected function setOutput(OutputInterface $output)
291
    {
292
        $this->output = $output;
293
    }
294
295
    protected function setVerbosity($verbosity)
296
    {
297
        $this->verbosity = $verbosity;
298
    }
299
300
}
301