Completed
Push — master ( 8c6bbe...f0215b )
by Gaetano
10:40
created

MigrateCommand::buildMigrationsList()   D

Complexity

Conditions 10
Paths 6

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

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

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 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
        $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output);
84
85
        $migrationService = $this->getMigrationService();
86
87
        $toExecute = $this->buildMigrationsList($input->getOption('path'), $migrationService);
88
89
        if (!count($toExecute)) {
90
            $output->writeln('<info>No migrations to execute</info>');
91
            return 0;
92
        }
93
94 19
        $this->printMigrationsList($toExecute, $input, $output);
95
96 19
        // ask user for confirmation to make changes
97
        if (!$this->askForConfirmation($input, $output)) {
98
            return 0;
99
        }
100
101 19
        if ($input->getOption('separate-process')) {
102
            $builder = new ProcessBuilder();
103 19
            $executableFinder = new PhpExecutableFinder();
104 19
            if (false !== $php = $executableFinder->find()) {
105 19
                $builder->setPrefix($php);
106 19
            }
107 19
            // mandatory args and options
108 7
            $builderArgs = array(
109 7
                $_SERVER['argv'][0], // sf console
110 19
                self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
111 19
                '--env=' . $this->getContainer()->get('kernel')->getEnvironment(), // sf env
112 19
                '--child'
113
            );
114 19
            // 'optional' options
115 19
            // note: options 'clear-cache', 'ignore-failures' and 'no-transactions' we never propagate
116
            if ($input->getOption('default-language')) {
117 19
                $builderArgs[]='--default-language='.$input->getOption('default-language');
118
            }
119 19
            if ($input->getOption('no-transactions')) {
120 19
                $builderArgs[]='--no-transactions';
121 19
            }
122
        }
123 19
124
        /** @var MigrationDefinition $migrationDefinition */
125 19
        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
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
129
                $output->writeln("<comment>Skipping $name</comment>\n");
130
                continue;
131
            }
132
133 19
            $this->writeln("<info>Processing $name</info>");
134
135
            if ($input->getOption('separate-process')) {
136
137 19
                $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 19
                    ->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 19
141
                $this->writeln('<info>Executing: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
142
143 19
                // allow long migrations processes by default
144 7
                $process->setTimeout(86400);
145 7
                // and give immediate feedback to the user
146
                $process->run(
147
                    function ($type, $buffer) {
148 12
                        echo $buffer;
149
                    }
150
                );
151 12
152 12
                try {
153 12
154 12
                    if (!$process->isSuccessful()) {
155 12
                        throw new \Exception($process->getErrorOutput());
156 12
                    }
157 3
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 3
                    $migration = $migrationService->getMigration($migrationDefinition->name);
162 3
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 9
                        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 16
                    } 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 19
                        $migrationService->endMigration(New Migration(
169
                            $migration->name,
170
                            $migration->md5,
171
                            $migration->path,
172 12
                            $migration->executionDate,
173 16
                            Migration::STATUS_FAILED,
174
                            ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg)
175
                        ));
176
                        throw new \Exception($errorMsg);
177
                    }
178
179
                } catch(\Exception $e) {
180
                    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
                    $migrationService->executeMigration(
192
                        $migrationDefinition,
193
                        !$input->getOption('no-transactions'),
194
                        $input->getOption('default-language')
195
                    );
196
                } catch(\Exception $e) {
197
                    if ($input->getOption('ignore-failures')) {
198
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
199
                        continue;
200
                    }
201
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
202
                    return 1;
203
                }
204
205
            }
206
207
            $this->writeln('');
208
        }
209
210
        if ($input->getOption('clear-cache')) {
211
            $command = $this->getApplication()->find('cache:clear');
212
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
213
            $command->run($inputArray, $output);
214
        }
215
    }
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
    protected function buildMigrationsList($paths, $migrationService)
225
    {
226
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
227
        $migrations = $migrationService->getMigrations();
228
229
        // filter away all migrations except 'to do' ones
230
        $toExecute = array();
231
        foreach($migrationDefinitions as $name => $migrationDefinition) {
232
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && $migration->status == Migration::STATUS_TODO)) {
233
                $toExecute[$name] = $migrationService->parseMigrationDefinition($migrationDefinition);
234
            }
235
        }
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
        if (empty($paths)) {
240
            foreach ($migrations as $migration) {
241
                if ($migration->status == Migration::STATUS_TODO && !isset($toExecute[$migration->name])) {
242
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
243
                    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
        }
252
253
        ksort($toExecute);
254
255
        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
    protected function printMigrationsList($toExecute , InputInterface $input, OutputInterface $output)
266
    {
267
        $data = array();
268
        $i = 1;
269
        foreach($toExecute as $name => $migrationDefinition) {
270
            $notes = '';
271
            if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
272
                $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
273
            }
274
            $data[] = array(
275
                $i++,
276
                $name,
277
                $notes
278
            );
279
        }
280
281
        if (!$input->getOption('child')) {
282
            $table = $this->getHelperSet()->get('table');
283
            $table
284
                ->setHeaders(array('#', 'Migration', 'Notes'))
285
                ->setRows($data);
286
            $table->render($output);
287
        }
288
289
        $this->writeln('');
290
    }
291
292
    protected function askForConfirmation(InputInterface $input, OutputInterface $output)
293
    {
294 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
            $this->writeln("=============================================\n");
307
        }
308
309
        return 1;
310
    }
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
    protected function writeln($message, $verbosity=OutputInterface::VERBOSITY_NORMAL)
318
    {
319
        if ($this->verbosity >= $verbosity) {
320
            $this->output->writeln($message);
321
        }
322
    }
323
324
    protected function setOutput(OutputInterface $output)
325
    {
326
        $this->output = $output;
327
    }
328
329
    protected function setVerbosity($verbosity)
330
    {
331
        $this->verbosity = $verbosity;
332
    }
333
334
}
335