Completed
Pull Request — master (#91)
by
unknown
06:19
created

MigrateCommand::buildMigrationsList()   D

Complexity

Conditions 10
Paths 6

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 13.2218

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 33
ccs 15
cts 22
cp 0.6818
rs 4.8196
cc 10
eloc 17
nc 6
nop 2
crap 13.2218

How to fix   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 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
    /** @var OutputInterface $output */
23
    protected $output;
24
    protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
25
26
    const COMMAND_NAME='kaliop:migration:migrate';
27
28
    /**
29
     * Set up the command.
30
     *
31
     * Define the name, options and help text.
32
     */
33 27
    protected function configure()
34
    {
35 27
        parent::configure();
36
37 27
        $this
38 27
            ->setName(self::COMMAND_NAME)
39 27
            ->setAliases(array('kaliop:migration:update'))
40 27
            ->setDescription('Execute available migration definitions.')
41 27
            ->addOption(
42 27
                'path',
43 27
                null,
44 27
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
45
                "The directory or file to load the migration definitions from"
46 27
            )
47
            // nb: when adding options, remember to forward them to sub-commands executed in 'separate-process' mode
48 27
            ->addOption('default-language', null, InputOption::VALUE_REQUIRED, "Default language code that will be used if no language is provided in migration steps")
49 27
            ->addOption('ignore-failures', null, InputOption::VALUE_NONE, "Keep executing migrations even if one fails")
50 27
            ->addOption('clear-cache', null, InputOption::VALUE_NONE, "Clear the cache after the command finishes")
51 27
            ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question")
52 27
            ->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 27
            ->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 27
            ->addOption('child', null, InputOption::VALUE_NONE, "*DO NOT USE* Internal option for when forking separate processes")
55 27
            ->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 27
            );
65 27
    }
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 26
    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
    {
76 26
        $this->setOutput($output);
77 26
        $this->setVerbosity($output->getVerbosity());
78
79 26
        if ($input->getOption('child')) {
80
            $this->setVerbosity(self::VERBOSITY_CHILD);
81
        }
82
83 26
        $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output);
84
85 26
        $migrationService = $this->getMigrationService();
86
87 26
        $toExecute = $this->buildMigrationsList($input->getOption('path'), $migrationService);
88
89 26
        if (!count($toExecute)) {
90
            $output->writeln('<info>No migrations to execute</info>');
91
            return 0;
92
        }
93
94 26
        $this->printMigrationsList($toExecute, $input, $output);
95
96
        // ask user for confirmation to make changes
97 26
        if (!$this->askForConfirmation($input, $output)) {
98
            return 0;
99
        }
100
101 26
        if ($input->getOption('separate-process')) {
102
            $builder = new ProcessBuilder();
103
            $executableFinder = new PhpExecutableFinder();
104
            if (false !== $php = $executableFinder->find()) {
105
                $builder->setPrefix($php);
106
            }
107
            // mandatory args and options
108
            $builderArgs = array(
109
                $_SERVER['argv'][0], // sf console
110
                self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
111
                '--env=' . $this->getContainer()->get('kernel')->getEnvironment(), // sf env
112
                '--child'
113
            );
114
            // 'optional' options
115
            // note: options 'clear-cache', 'ignore-failures' and 'no-transactions' we never propagate
116
            if ($input->getOption('default-language')) {
117
                $builderArgs[]='--default-language='.$input->getOption('default-language');
118
            }
119
            if ($input->getOption('no-transactions')) {
120
                $builderArgs[]='--no-transactions';
121
            }
122
        }
123
124
        /** @var MigrationDefinition $migrationDefinition */
125 26
        foreach($toExecute as $name => $migrationDefinition) {
126
127
            // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
128 26
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
129 9
                $output->writeln("<comment>Skipping $name</comment>\n");
130 9
                continue;
131
            }
132
133 17
            $this->writeln("<info>Processing $name</info>");
134
135 17
            if ($input->getOption('separate-process')) {
136
137
                $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...
138
                    ->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...
139
                    ->getProcess();
140
141
                $this->writeln('<info>Executing: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
142
143
                // allow long migrations processes by default
144
                $process->setTimeout(86400);
145
                // and give immediate feedback to the user
146
                $process->run(
147
                    function ($type, $buffer) {
148
                        echo $buffer;
149
                    }
150
                );
151
152
                try {
153
154
                    if (!$process->isSuccessful()) {
155
                        throw new \Exception($process->getErrorOutput());
156
                    }
157
158
                    // There are cases where the separate process dies halfway but does not return a non-zero code.
159
                    // That's why we should double-check here if the migration is still tagged as 'started'...
160
                    /** @var Migration $migration */
161
                    $migration = $migrationService->getMigration($migrationDefinition->name);
162
163
                    if (!$migration) {
164
                        // q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace...
165
                        throw new \Exception("After the separate process charged to execute the migration finished, the migration can not be found in the database any more.");
166
                    } else if ($migration->status == Migration::STATUS_STARTED) {
167
                        $errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution.";
168
                        $migrationService->endMigration(New Migration(
169
                            $migration->name,
170
                            $migration->md5,
171
                            $migration->path,
172
                            $migration->executionDate,
173
                            Migration::STATUS_FAILED,
174 26
                            ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg)
175
                        ));
176
                        throw new \Exception($errorMsg);
177
                    }
178
179 26
                } catch(\Exception $e) {
180 17
                    if ($input->getOption('ignore-failures')) {
181
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
182
                        continue;
183
                    }
184
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
185
                    return 1;
186
                }
187
188
            } else {
189
190
                try {
191 17
                    $migrationService->executeMigration(
192 17
                        $migrationDefinition,
193 17
                        !$input->getOption('no-transactions'),
194 17
                        $input->getOption('default-language')
195 17
                    );
196 17
                } catch(\Exception $e) {
197 3
                    if ($input->getOption('ignore-failures')) {
198
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
199
                        continue;
200
                    }
201 3
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
202 3
                    return 1;
203
                }
204
205
            }
206
207 14
            $this->writeln('');
208 23
        }
209
210 23
        if ($input->getOption('clear-cache')) {
211
            $command = $this->getApplication()->find('cache:clear');
212
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
213 17
            $command->run($inputArray, $output);
214
        }
215 23
    }
216
217
    /**
218
     * @param string $paths
219
     * @param $migrationService
220
     * @return MigrationDefinition[]
221
     *
222
     * @todo this does not scale well with many definitions or migrations
223
     */
224 26
    protected function buildMigrationsList($paths, $migrationService)
225
    {
226 26
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
227 26
        $migrations = $migrationService->getMigrations();
228
229
        // filter away all migrations except 'to do' ones
230 26
        $toExecute = array();
231 26
        foreach($migrationDefinitions as $name => $migrationDefinition) {
232 26
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && $migration->status == Migration::STATUS_TODO)) {
233 26
                $toExecute[$name] = $migrationService->parseMigrationDefinition($migrationDefinition);
234 26
            }
235 26
        }
236
237
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
238
        // found by the loader
239 26
        if (empty($paths)) {
240
            foreach ($migrations as $migration) {
241 17
                if ($migration->status == Migration::STATUS_TODO && !isset($toExecute[$migration->name])) {
242
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
243 1
                    if (count($migrationDefinitions)) {
244
                        $migrationDefinition = reset($migrationDefinitions);
245
                        $toExecute[$migration->name] = $migrationService->parseMigrationDefinition($migrationDefinition);
246
                    } 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...
247
                        // q: shall we raise a warning here ?
248
                    }
249
                }
250
            }
251 1
        }
252
253 26
        ksort($toExecute);
254
255 26
        return $toExecute;
256
    }
257
258
    /**
259
     * @param MigrationDefinition[] $toExecute
260
     * @param InputInterface $input
261
     * @param OutputInterface $output
262
     *
263
     * @todo use a more compact output when there are *many* migrations
264
     */
265 26
    protected function printMigrationsList($toExecute , InputInterface $input, OutputInterface $output)
266
    {
267 26
        $data = array();
268 26
        $i = 1;
269 26
        foreach($toExecute as $name => $migrationDefinition) {
270 26
            $notes = '';
271 26
            if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
272 9
                $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
273 9
            }
274 26
            $data[] = array(
275 26
                $i++,
276 26
                $name,
277
                $notes
278 26
            );
279 26
        }
280
281 26
        if (!$input->getOption('child')) {
282 26
            $table = $this->getHelperSet()->get('table');
283
            $table
284 26
                ->setHeaders(array('#', 'Migration', 'Notes'))
285 26
                ->setRows($data);
286 26
            $table->render($output);
287 26
        }
288
289 26
        $this->writeln('');
290 26
    }
291
292 26
    protected function askForConfirmation(InputInterface $input, OutputInterface $output)
293
    {
294 26 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...
295
            $dialog = $this->getHelperSet()->get('dialog');
296
            if (!$dialog->askConfirmation(
297
                $output,
298
                '<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>',
299
                false
300
            )
301
            ) {
302
                $output->writeln('<error>Migration execution cancelled!</error>');
303
                return 0;
304
            }
305
        } else {
306 26
            $this->writeln("=============================================\n");
307
        }
308
309 26
        return 1;
310 3
    }
311
312
    /**
313
     * Small tricks to allow us to lower verbosity between NORMAL and QUIET and have a decent writeln API, even with old SF versions
314
     * @param $message
315
     * @param int $verbosity
316
     */
317 26
    protected function writeln($message, $verbosity=OutputInterface::VERBOSITY_NORMAL)
318 1
    {
319 26
        if ($this->verbosity >= $verbosity) {
320 26
            $this->output->writeln($message);
321 26
        }
322 26
    }
323
324 26
    protected function setOutput(OutputInterface $output)
325
    {
326 26
        $this->output = $output;
327 26
    }
328
329 26
    protected function setVerbosity($verbosity)
330
    {
331 26
        $this->verbosity = $verbosity;
332 26
    }
333
334
}
335