Completed
Push — master ( b8e7fb...6973a3 )
by Gaetano
24:01
created

MigrateCommand::setOutput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 2
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
14
/**
15
 * Command to execute the available migration definitions.
16
 */
17
class MigrateCommand extends AbstractCommand
18
{
19
    // in between QUIET and NORMAL
20
    const VERBOSITY_CHILD = 0.5;
21
    /** @var OutputInterface $output */
22 20
    protected $output;
23
    protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
24 20
25
    /**
26 20
     * Set up the command.
27 20
     *
28 20
     * Define the name, options and help text.
29 20
     */
30 20
    protected function configure()
31 20
    {
32 20
        parent::configure();
33 20
34
        $this
35 20
            ->setName('kaliop:migration:migrate')
36 20
            ->setAliases(array('kaliop:migration:update'))
37 20
            ->setDescription('Execute available migration definitions.')
38 20
            ->addOption(
39 20
                'path',
40 20
                null,
41 20
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
42
                "The directory or file to load the migration definitions from"
43
            )
44
            // nb: when adding options, remember to forward them to sub-commands executed in 'separate-process' mode
45
            ->addOption('default-language', null, InputOption::VALUE_REQUIRED, "Default language code that will be used if no language is provided in migration steps")
46
            ->addOption('ignore-failures', null, InputOption::VALUE_NONE, "Keep executing migrations even if one fails")
47
            ->addOption('clear-cache', null, InputOption::VALUE_NONE, "Clear the cache after the command finishes")
48
            ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question")
49
            ->addOption('no-transactions', 'u', InputOption::VALUE_NONE, "Do not use a repository transaction to wrap each migration. Unsafe, but needed for legacy slot handlers")
50 20
            ->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")
51 20
            ->addOption('child', null, InputOption::VALUE_NONE, "*DO NOT USE* Internal option for when forking separate processes")
52
            ->setHelp(<<<EOT
53
The <info>kaliop:migration:migrate</info> command loads and executes migrations:
54
55
    <info>./ezpublish/console kaliop:migration:migrate</info>
56
57
You can optionally specify the path to migration definitions with <info>--path</info>:
58
59
    <info>./ezpublish/console kaliop:migrations:migrate --path=/path/to/bundle/version_directory --path=/path/to/bundle/version_directory/single_migration_file</info>
60
EOT
61
            );
62 19
    }
63
64 19
    /**
65
     * Execute the command.
66 19
     *
67 19
     * @param InputInterface $input
68 19
     * @param OutputInterface $output
69
     * @return null|int null or 0 if everything went fine, or an error code
70
     */
71 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...
72 19
    {
73 19
        $this->setOutput($output);
74 19
        $this->setVerbosity($output->getVerbosity());
75 19
76 19
        if ($input->getOption('child')) {
77
            $this->setVerbosity(self::VERBOSITY_CHILD);
78
        }
79
80 19
        $migrationsService = $this->getMigrationService();
81
82
        $paths = $input->getOption('path');
83
        $migrationDefinitions = $migrationsService->getMigrationsDefinitions($paths);
84
        $migrations = $migrationsService->getMigrations();
85
86
        // filter away all migrations except 'to do' ones
87
        $toExecute = array();
88
        foreach($migrationDefinitions as $name => $migrationDefinition) {
89
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && $migration->status == Migration::STATUS_TODO)) {
90
                $toExecute[$name] = $migrationsService->parseMigrationDefinition($migrationDefinition);
91
            }
92
        }
93
94 19
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
95
        // found by the loader
96 19
        if (empty($paths)) {
97
            foreach ($migrations as $migration) {
98
                if ($migration->status == Migration::STATUS_TODO && !isset($toExecute[$migration->name])) {
99
                    $migrationDefinitions = $migrationsService->getMigrationsDefinitions(array($migration->path));
100
                    if (count($migrationDefinitions)) {
101 19
                        $migrationDefinition = reset($migrationDefinitions);
102
                        $toExecute[$migration->name] = $migrationsService->parseMigrationDefinition($migrationDefinition);
103 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...
104 19
                        // q: shall we raise a warning here ?
105 19
                    }
106 19
                }
107 19
            }
108 7
        }
109 7
110 19
        ksort($toExecute);
111 19
112 19
        if (!count($toExecute)) {
113
            $output->writeln('<info>No migrations to execute</info>');
114 19
            return;
115 19
        }
116
117 19
        $this->writeln("\n <info>==</info> Migrations to be executed\n");
118
119 19
        $data = array();
120 19
        $i = 1;
121 19
        foreach($toExecute as $name => $migrationDefinition) {
122
            $notes = '';
123 19
            if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
124
                $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
125 19
            }
126
            $data[] = array(
127
                $i++,
128
                $name,
129
                $notes
130
            );
131
        }
132
133 19
        if (!$input->getOption('child')) {
134
            $table = $this->getHelperSet()->get('table');
135
            $table
136
                ->setHeaders(array('#', 'Migration', 'Notes'))
137 19
                ->setRows($data);
138 19
            $table->render($output);
139
        }
140 19
141
        $this->writeln('');
142
143 19
        // ask user for confirmation to make changes
144 7 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...
145 7
            $dialog = $this->getHelperSet()->get('dialog');
146
            if (!$dialog->askConfirmation(
147
                $output,
148 12
                '<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>',
149
                false
150
            )
151 12
            ) {
152 12
                $output->writeln('<error>Migration execution cancelled!</error>');
153 12
                return 0;
154 12
            }
155 12
        } else {
156 12
            $this->writeln("=============================================\n");
157 3
        }
158
159
        if ($input->getOption('separate-process')) {
160
            $builder = new ProcessBuilder();
161 3
            $executableFinder = new PhpExecutableFinder();
162 3
            if (false !== $php = $executableFinder->find()) {
163
                $builder->setPrefix($php);
164
            }
165 9
            // mandatory args and options
166 16
            $builderArgs = array(
167
                $_SERVER['argv'][0], // sf console
168 19
                $_SERVER['argv'][1], // name of sf command
169
                '--env=' . $this->getContainer()->get('kernel')->getEnvironment(), // sf env
170
                '--child'
171
            );
172 12
            // 'optional' options
173 16
            // note: options 'clear-cache', 'ignore-failures' and 'no-transactions' we never propagate
174
            if ($input->getOption('default-language')) {
175
                $builderArgs[]='--default-language='.$input->getOption('default-language');
176
            }
177
            if ($input->getOption('no-transactions')) {
178
                $builderArgs[]='--no-transactions';
179
            }
180
        }
181
182
        foreach($toExecute as $name => $migrationDefinition) {
183
184
            // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
185
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
186
                $output->writeln("<comment>Skipping $name</comment>\n");
187
                continue;
188
            }
189
190
            $this->writeln("<info>Processing $name</info>");
191
192
            if ($input->getOption('separate-process')) {
193
194
                $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...
195
                    ->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...
196
                    ->getProcess();
197
198
                $this->writeln('<info>Executing: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
199
200
                $process->run();
201
202
                $output->write($process->getOutput());
203
                if (!$process->isSuccessful()) {
204
                    $err = $process->getErrorOutput();
205
                    if ($input->getOption('ignore-failures')) {
206
                        $output->writeln("\n<error>Migration failed! Reason: " . $err . "</error>\n");
207
                        continue;
208
                    }
209
                    $output->writeln("\n<error>Migration aborted! Reason: " . $err . "</error>");
210
                    return 1;
211
                }
212
213
            } else {
214
215
                try {
216
                    $migrationsService->executeMigration(
217
                        $migrationDefinition,
218
                        !$input->getOption('no-transactions'),
219
                        $input->getOption('default-language')
220
                    );
221
                } catch(\Exception $e) {
222
                    if ($input->getOption('ignore-failures')) {
223
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
224
                        continue;
225
                    }
226
                    $output->writeln("\n<error>Migration aborted! Reason: " . $e->getMessage() . "</error>");
227
                    return 1;
228
                }
229
230
            }
231
232
            $this->writeln('');
233
        }
234
235
        if ($input->getOption('clear-cache')) {
236
            $command = $this->getApplication()->find('cache:clear');
237
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
238
            $command->run($inputArray, $output);
239
        }
240
    }
241
242
    /**
243
     * Small tricks to allow us to lower verbosity between NORMAL and QUIET and have a decent writeln API, even with old SF versions
244
     * @param $message
245
     * @param int $verbosity
246
     */
247
    protected function writeln($message, $verbosity=OutputInterface::VERBOSITY_NORMAL)
248
    {
249
        if ($this->verbosity >= $verbosity) {
250
            $this->output->writeln($message);
251
        }
252
    }
253
254
    protected function setOutput(OutputInterface $output)
255
    {
256
        $this->output = $output;
257
    }
258
259
    protected function setVerbosity($verbosity)
260
    {
261
        $this->verbosity = $verbosity;
262
    }
263
264
}
265