Completed
Branch master (e35419)
by Gaetano
06:40
created

MigrateCommand::execute()   C

Complexity

Conditions 16
Paths 166

Size

Total Lines 108
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 38.1959

Importance

Changes 0
Metric Value
cc 16
eloc 65
nc 166
nop 2
dl 0
loc 108
ccs 34
cts 61
cp 0.5574
crap 38.1959
rs 5.0166
c 0
b 0
f 0

How to fix   Long Method    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 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 78
    protected function configure()
37
    {
38 78
        parent::configure();
39
40
        $this
41 78
            ->setName(self::COMMAND_NAME)
42 78
            ->setAliases(array('kaliop:migration:update'))
43 78
            ->setDescription('Execute available migration definitions.')
44
            // nb: when adding options, remember to forward them to sub-commands executed in 'separate-process' mode
45 78
            ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)")
46 78
            ->addOption('clear-cache', 'c', InputOption::VALUE_NONE, "Clear the cache after the command finishes")
47 78
            ->addOption('default-language', 'l', InputOption::VALUE_REQUIRED, "Default language code that will be used if no language is provided in migration steps")
48 78
            ->addOption('force', 'f', InputOption::VALUE_NONE, "Force (re)execution of migrations already DONE, SKIPPED or FAILED. Use with great care!")
49 78
            ->addOption('ignore-failures', 'i', InputOption::VALUE_NONE, "Keep executing migrations even if one fails")
50 78
            ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question")
51 78
            ->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 78
            ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "The directory or file to load the migration definitions from")
53 78
            ->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 78
            ->addOption('child', null, InputOption::VALUE_NONE, "*DO NOT USE* Internal option for when forking separate processes")
55 78
            ->setHelp(<<<EOT
56 78
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
            );
65 78
    }
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 47
    protected function execute(InputInterface $input, OutputInterface $output)
75
    {
76 47
        $start = microtime(true);
77
78 47
        $this->setOutput($output);
79 47
        $this->setVerbosity($output->getVerbosity());
80
81 47
        if ($input->getOption('child')) {
82
            $this->setVerbosity(self::VERBOSITY_CHILD);
83
        }
84
85 47
        $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output);
86
87 47
        $migrationService = $this->getMigrationService();
88
89 47
        $force = $input->getOption('force');
90
91 47
        $toExecute = $this->buildMigrationsList($input->getOption('path'), $migrationService, $force);
0 ignored issues
show
Bug introduced by
It seems like $input->getOption('path') can also be of type boolean and string; however, parameter $paths of Kaliop\eZMigrationBundle...::buildMigrationsList() does only seem to accept string[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

91
        $toExecute = $this->buildMigrationsList(/** @scrutinizer ignore-type */ $input->getOption('path'), $migrationService, $force);
Loading history...
92
93 47
        if (!count($toExecute)) {
94
            $output->writeln('<info>No migrations to execute</info>');
95
            return 0;
96
        }
97
98 47
        $this->printMigrationsList($toExecute, $input, $output);
99
100
        // ask user for confirmation to make changes
101 47
        if (!$this->askForConfirmation($input, $output)) {
102
            return 0;
103
        }
104
105 47
        if ($input->getOption('separate-process')) {
106
            $builder = new ProcessBuilder();
107
            $executableFinder = new PhpExecutableFinder();
108
            if (false !== $php = $executableFinder->find()) {
109
                $builder->setPrefix($php);
110
            }
111
            $builderArgs = $this->createChildProcessArgs($input);
112
        }
113
114 47
        $executed = 0;
115 47
        $failed = 0;
116 47
        $skipped = 0;
117
118
        /** @var MigrationDefinition $migrationDefinition */
119 47
        foreach ($toExecute as $name => $migrationDefinition) {
120
121
            // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
122 47
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
123 9
                $output->writeln("<comment>Skipping $name</comment>\n");
124 9
                $skipped++;
125 9
                continue;
126
            }
127
128 38
            $this->writeln("<info>Processing $name</info>");
129
130 38
            if ($input->getOption('separate-process')) {
131
132
                try {
133
                    $this->executeMigrationInSeparateProcess($migrationDefinition, $migrationService, $builder, $builderArgs);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $builder does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $builderArgs does not seem to be defined for all execution paths leading up to this point.
Loading history...
134
135
                    $executed++;
136
                } catch (\Exception $e) {
137
                    if ($input->getOption('ignore-failures')) {
138
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
139
                        $failed++;
140
                        continue;
141
                    }
142
                    if ($e instanceof AfterMigrationExecutionException) {
143
                        $output->writeln("\n<error>Failure after migration end! Reason: " . $e->getMessage() . "</error>");
144
                    } else {
145
                        $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
146
                    }
147
                    return 1;
148
                }
149
150
            } else {
151
152
                try {
153 38
                    $this->executeMigrationInProcess($migrationDefinition, $force, $migrationService, $input);
154
155 30
                    $executed++;
156 8
                } catch (\Exception $e) {
157 8
                    if ($input->getOption('ignore-failures')) {
158
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
159
                        $failed++;
160
                        continue;
161
                    }
162 8
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
163 38
                    return 1;
164
                }
165
166
            }
167
        }
168
169 39
        if ($input->getOption('clear-cache')) {
170
            $command = $this->getApplication()->find('cache:clear');
171
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
172
            $command->run($inputArray, $output);
173
        }
174
175 39
        $time = microtime(true) - $start;
176 39
        $this->writeln("Executed $executed migrations, failed $failed, skipped $skipped");
177 39
        if ($input->getOption('separate-process')) {
178
            // in case of using subprocesses, we can not measure max memory used
179
            $this->writeln("Time taken: ".sprintf('%.2f', $time)." secs");
180
        } else {
181 39
            $this->writeln("Time taken: ".sprintf('%.2f', $time)." secs, memory: ".sprintf('%.2f', (memory_get_peak_usage(true) / 1000000)). ' MB');
182
        }
183 39
    }
184
185 38
    protected function executeMigrationInProcess($migrationDefinition, $force, $migrationService, $input)
186
    {
187 38
        $migrationService->executeMigration(
188 38
            $migrationDefinition,
189 38
            !$input->getOption('no-transactions'),
190 38
            $input->getOption('default-language'),
191 38
            $input->getOption('admin-login'),
192 38
            $force
193
        );
194 30
    }
195
196
    protected function executeMigrationInSeparateProcess($migrationDefinition, $migrationService, $builder, $builderArgs, $feedback = true)
197
    {
198
        $process = $builder
199
            ->setArguments(array_merge($builderArgs, array('--path=' . $migrationDefinition->path)))
200
            ->getProcess();
201
202
        if ($feedback) {
203
            $this->writeln('<info>Executing: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
204
        }
205
206
        // allow long migrations processes by default
207
        $process->setTimeout(86400);
208
        // and give immediate feedback to the user
209
        $process->run(
210
            $feedback ?
211
                function($type, $buffer) {
212
                    echo $buffer;
213
                }
214
                :
215
                function($type, $buffer) {
0 ignored issues
show
Unused Code introduced by
The parameter $buffer is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

215
                function($type, /** @scrutinizer ignore-unused */ $buffer) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

215
                function(/** @scrutinizer ignore-unused */ $type, $buffer) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
216
                }
217
        );
218
219
        if (!$process->isSuccessful()) {
220
            throw new \Exception($process->getErrorOutput());
221
        }
222
223
        // There are cases where the separate process dies halfway but does not return a non-zero code.
224
        // That's why we double-check here if the migration is still tagged as 'started'...
225
        /** @var Migration $migration */
226
        $migration = $migrationService->getMigration($migrationDefinition->name);
227
228
        if (!$migration) {
0 ignored issues
show
introduced by
$migration is of type Kaliop\eZMigrationBundle\API\Value\Migration, thus it always evaluated to true.
Loading history...
229
            // q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace...
230
            throw new \Exception("After the separate process charged to execute the migration finished, the migration can not be found in the database any more.");
231
        } else if ($migration->status == Migration::STATUS_STARTED) {
232
            $errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution.";
233
            $migrationService->endMigration(New Migration(
234
                $migration->name,
235
                $migration->md5,
236
                $migration->path,
237
                $migration->executionDate,
238
                Migration::STATUS_FAILED,
239
                ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg)
240
            ));
241
            throw new \Exception($errorMsg);
242
        }
243
    }
244
245
    /**
246
     * @param string[] $paths
247
     * @param MigrationService $migrationService
248
     * @param bool $force when true, look not only for TODO migrations, but also DONE, SKIPPED, FAILED ones (we still omit STARTED and SUSPENDED ones)
249
     * @return MigrationDefinition[]
250
     *
251
     * @todo this does not scale well with many definitions or migrations
252
     */
253 47
    protected function buildMigrationsList($paths, $migrationService, $force = false)
254
    {
255 47
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
256 47
        $migrations = $migrationService->getMigrations();
257
258 47
        $allowedStatuses = array(Migration::STATUS_TODO);
259 47
        if ($force) {
260
            $allowedStatuses = array_merge($allowedStatuses, array(Migration::STATUS_DONE, Migration::STATUS_FAILED, Migration::STATUS_SKIPPED));
261
        }
262
263
        // filter away all migrations except 'to do' ones
264 47
        $toExecute = array();
265 47
        foreach ($migrationDefinitions as $name => $migrationDefinition) {
266 47
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && in_array($migration->status, $allowedStatuses))) {
267 47
                $toExecute[$name] = $migrationService->parseMigrationDefinition($migrationDefinition);
268
            }
269
        }
270
271
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
272
        // found by the loader
273 47
        if (empty($paths)) {
274
            foreach ($migrations as $migration) {
275
                if (in_array($migration->status, $allowedStatuses) && !isset($toExecute[$migration->name])) {
276
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
277
                    if (count($migrationDefinitions)) {
278
                        $migrationDefinition = reset($migrationDefinitions);
279
                        $toExecute[$migration->name] = $migrationService->parseMigrationDefinition($migrationDefinition);
280
                    } else {
281
                        // q: shall we raise a warning here ?
282
                    }
283
                }
284
            }
285
        }
286
287 47
        ksort($toExecute);
288
289 47
        return $toExecute;
290
    }
291
292
    /**
293
     * @param MigrationDefinition[] $toExecute
294
     * @param InputInterface $input
295
     * @param OutputInterface $output
296
     *
297
     * @todo use a more compact output when there are *many* migrations
298
     */
299 47
    protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output)
300
    {
301 47
        $data = array();
302 47
        $i = 1;
303 47
        foreach ($toExecute as $name => $migrationDefinition) {
304 47
            $notes = '';
305 47
            if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
306 9
                $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
307
            }
308 47
            $data[] = array(
309 47
                $i++,
310 47
                $name,
311 47
                $notes
312
            );
313
        }
314
315 47
        if (!$input->getOption('child')) {
316 47
            $table = new Table($output);
317
            $table
318 47
                ->setHeaders(array('#', 'Migration', 'Notes'))
319 47
                ->setRows($data);
320 47
            $table->render();
321
        }
322
323 47
        $this->writeln('');
324 47
    }
325
326 47
    protected function askForConfirmation(InputInterface $input, OutputInterface $output, $nonIteractiveOutput = "=============================================\n")
327
    {
328 47
        if ($input->isInteractive() && !$input->getOption('no-interaction')) {
329
            $dialog = $this->getHelperSet()->get('question');
330
            if (!$dialog->ask(
0 ignored issues
show
Bug introduced by
The method ask() does not exist on Symfony\Component\Console\Helper\Helper. It seems like you code against a sub-type of Symfony\Component\Console\Helper\Helper such as Symfony\Component\Console\Helper\QuestionHelper or Symfony\Component\Console\Helper\DialogHelper. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

330
            if (!$dialog->/** @scrutinizer ignore-call */ ask(
Loading history...
331
                $input,
332
                $output,
333
                new ConfirmationQuestion('<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>', false)
334
            )
335
            ) {
336
                $output->writeln('<error>Migration execution cancelled!</error>');
337
                return 0;
338
            }
339
        } else {
340 47
            if ($nonIteractiveOutput != '') {
341 47
                $this->writeln("$nonIteractiveOutput");
342
            }
343
        }
344
345 47
        return 1;
346
    }
347
348
    /**
349
     * Small tricks to allow us to lower verbosity between NORMAL and QUIET and have a decent writeln API, even with old SF versions
350
     * @param $message
351
     * @param int $verbosity
352
     */
353 47
    protected function writeln($message, $verbosity = OutputInterface::VERBOSITY_NORMAL)
354
    {
355 47
        if ($this->verbosity >= $verbosity) {
356 47
            $this->output->writeln($message);
357
        }
358 47
    }
359
360 47
    protected function setOutput(OutputInterface $output)
361
    {
362 47
        $this->output = $output;
363 47
    }
364
365 47
    protected function setVerbosity($verbosity)
366
    {
367 47
        $this->verbosity = $verbosity;
368 47
    }
369
370
    /**
371
     * Returns the command-line arguments needed to execute a migration in a separate subprocess (omitting 'path')
372
     * @param InputInterface $input
373
     * @return array
374
     */
375
    protected function createChildProcessArgs(InputInterface $input)
376
    {
377
        $kernel = $this->getContainer()->get('kernel');
378
379
        // mandatory args and options
380
        $builderArgs = array(
381
            $_SERVER['argv'][0], // sf console
382
            self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
383
            '--env=' . $kernel->getEnvironment(), // sf env
384
            '--child'
385
        );
386
        // sf/ez env options
387
        if (!$kernel->isDebug()) {
388
            $builderArgs[] = '--no-debug';
389
        }
390
        if ($input->getOption('siteaccess')) {
391
            $builderArgs[]='--siteaccess='.$input->getOption('siteaccess');
392
        }
393
        // 'optional' options
394
        // note: options 'clear-cache', 'ignore-failures', 'no-interaction', 'path' and 'separate-process' we never propagate
395
        if ($input->getOption('admin-login')) {
396
            $builderArgs[] = '--admin-login=' . $input->getOption('admin-login');
397
        }
398
        if ($input->getOption('default-language')) {
399
            $builderArgs[] = '--default-language=' . $input->getOption('default-language');
400
        }
401
        if ($input->getOption('force')) {
402
            $builderArgs[] = '--force';
403
        }
404
        if ($input->getOption('no-transactions')) {
405
            $builderArgs[] = '--no-transactions';
406
        }
407
408
        return $builderArgs;
409
    }
410
}
411