kaliop-uk /
ezmigrationbundle
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Kaliop\eZMigrationBundle\Command; |
||
| 4 | |||
| 5 | use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; |
||
| 6 | use Kaliop\eZMigrationBundle\API\Value\Migration; |
||
| 7 | use Kaliop\eZMigrationBundle\API\Exception\AfterMigrationExecutionException; |
||
| 8 | use Kaliop\eZMigrationBundle\API\Exception\MigrationBundleException; |
||
| 9 | use Kaliop\eZMigrationBundle\Core\MigrationService; |
||
| 10 | use Kaliop\eZMigrationBundle\Core\Process\Process; |
||
| 11 | use Kaliop\eZMigrationBundle\Core\Process\ProcessBuilder; |
||
| 12 | use Symfony\Component\Console\Input\ArrayInput; |
||
| 13 | use Symfony\Component\Console\Input\InputInterface; |
||
| 14 | use Symfony\Component\Console\Input\InputOption; |
||
| 15 | use Symfony\Component\Console\Output\OutputInterface; |
||
| 16 | use Symfony\Component\Console\Helper\Table; |
||
| 17 | use Symfony\Component\Console\Question\ConfirmationQuestion; |
||
| 18 | use Symfony\Component\Process\PhpExecutableFinder; |
||
| 19 | |||
| 20 | /** |
||
| 21 | * Command to execute the available migration definitions. |
||
| 22 | */ |
||
| 23 | class MigrateCommand extends AbstractCommand |
||
| 24 | { |
||
| 25 | // in between QUIET and NORMAL. Sadly the OutputInterface consts changed somewhere between Symfony 2 and 3 |
||
| 26 | static $VERBOSITY_CHILD = 0.5; |
||
| 27 | |||
| 28 | protected $subProcessTimeout = 86400; |
||
| 29 | protected $subProcessErrorString = ''; |
||
| 30 | |||
| 31 | const COMMAND_NAME = 'kaliop:migration:migrate'; |
||
| 32 | |||
| 33 | /** |
||
| 34 | * Set up the command. |
||
| 35 | * |
||
| 36 | * Define the name, options and help text. |
||
| 37 | */ |
||
| 38 | 148 | protected function configure() |
|
| 39 | { |
||
| 40 | 148 | parent::configure(); |
|
| 41 | |||
| 42 | $this |
||
| 43 | 148 | ->setName(self::COMMAND_NAME) |
|
| 44 | 148 | ->setAliases(array('kaliop:migration:update')) |
|
| 45 | 148 | ->setDescription('Execute available migration definitions.') |
|
| 46 | // nb: when adding options, remember to forward them to sub-commands executed in 'separate-process' mode |
||
| 47 | 148 | ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)") |
|
| 48 | 148 | ->addOption('clear-cache', 'c', InputOption::VALUE_NONE, "Clear the cache after the command finishes") |
|
| 49 | 148 | ->addOption('default-language', 'l', InputOption::VALUE_REQUIRED, "Default language code that will be used if no language is provided in migration steps") |
|
| 50 | 148 | ->addOption('force', 'f', InputOption::VALUE_NONE, "Force (re)execution of migrations already DONE, SKIPPED or FAILED. Use with great care!") |
|
| 51 | 148 | ->addOption('ignore-failures', 'i', InputOption::VALUE_NONE, "Keep executing migrations even if one fails") |
|
| 52 | 148 | ->addOption('no-interaction', 'n', InputOption::VALUE_NONE, "Do not ask any interactive question") |
|
| 53 | 148 | ->addOption('no-transactions', 'u', InputOption::VALUE_NONE, "Do not use a repository transaction to wrap each migration. Unsafe, but needed for legacy slot handlers") |
|
| 54 | 148 | ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "The directory or file to load the migration definitions from") |
|
| 55 | 148 | ->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") |
|
| 56 | 148 | ->addOption('force-sigchild-enabled', null, InputOption::VALUE_NONE, "When using a separate php process to run each migration, tell Symfony that php was compiled with --enable-sigchild option") |
|
| 57 | 148 | ->addOption('survive-disconnected-tty', null, InputOption::VALUE_NONE, "Keep on executing migrations even if the tty where output is written to gets removed. Useful if you run the command over an unstable ssh connection") |
|
| 58 | 148 | ->addOption('set-reference', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "Inject references into the migrations. Format: --set-reference refname:value --set-reference ref2name:value2") |
|
| 59 | 148 | ->addOption('child-process-php-ini-config', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "Passed using `-d` to child php processes when using separate-process. Format: --child-processes-php-ini-config memory_limit:-1 --child-processes-php-ini-config setting2:value2") |
|
| 60 | 148 | ->addOption('child', null, InputOption::VALUE_NONE, "*DO NOT USE* Internal option for when forking separate processes") |
|
| 61 | 148 | ->setHelp(<<<EOT |
|
| 62 | 148 | The <info>kaliop:migration:migrate</info> command loads and executes migrations: |
|
| 63 | |||
| 64 | <info>php bin/console kaliop:migration:migrate</info> |
||
| 65 | |||
| 66 | You can optionally specify the path to migration definitions with <info>--path</info>: |
||
| 67 | |||
| 68 | <info>php bin/console kaliop:migration:migrate --path=/path/to/bundle/version_directory --path=/path/to/bundle/version_directory/single_migration_file</info> |
||
| 69 | |||
| 70 | Use -v and -vv options to get troubleshooting information on the execution of each step in the migration(s). |
||
| 71 | EOT |
||
| 72 | ); |
||
| 73 | |||
| 74 | 148 | if (self::$VERBOSITY_CHILD <= OutputInterface::VERBOSITY_QUIET) { |
|
| 75 | 1 | self::$VERBOSITY_CHILD = (OutputInterface::VERBOSITY_QUIET + OutputInterface::VERBOSITY_NORMAL) / 2; |
|
| 76 | } |
||
| 77 | 148 | } |
|
| 78 | |||
| 79 | /** |
||
| 80 | * Execute the command. |
||
| 81 | * |
||
| 82 | * @param InputInterface $input |
||
| 83 | * @param OutputInterface $output |
||
| 84 | * @return null|int null or 0 if everything went fine, or an error code |
||
| 85 | */ |
||
| 86 | 98 | protected function execute(InputInterface $input, OutputInterface $output) |
|
| 87 | { |
||
| 88 | 98 | $start = microtime(true); |
|
| 89 | |||
| 90 | 98 | $this->setOutput($output); |
|
| 91 | 98 | $this->setVerbosity($output->getVerbosity()); |
|
| 92 | |||
| 93 | 98 | if ($input->getOption('child') && $output->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) { |
|
| 94 | $this->setVerbosity(self::$VERBOSITY_CHILD); |
||
| 95 | } |
||
| 96 | |||
| 97 | 98 | $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output); |
|
| 98 | |||
| 99 | 98 | $migrationService = $this->getMigrationService(); |
|
| 100 | 98 | $migrationService->setOutput($output); |
|
| 101 | |||
| 102 | 98 | $force = $input->getOption('force'); |
|
| 103 | |||
| 104 | 98 | $toExecute = $this->buildMigrationsList($this->normalizePaths($input->getOption('path')), $migrationService, $force); |
|
| 105 | |||
| 106 | 98 | if (!count($toExecute)) { |
|
| 107 | 1 | $output->writeln('<info>No migrations to execute</info>'); |
|
| 108 | 1 | return 0; |
|
| 109 | } |
||
| 110 | |||
| 111 | 97 | $this->printMigrationsList($toExecute, $input, $output); |
|
| 112 | |||
| 113 | 97 | if (!$input->getOption('child')) { |
|
| 114 | // ask user for confirmation to make changes |
||
| 115 | 97 | if (!$this->askForConfirmation($input, $output)) { |
|
| 116 | return 0; |
||
| 117 | } |
||
| 118 | } |
||
| 119 | |||
| 120 | 97 | if ($input->getOption('separate-process')) { |
|
| 121 | 5 | $builder = new ProcessBuilder(); |
|
| 122 | 5 | $executableFinder = new PhpExecutableFinder(); |
|
| 123 | 5 | if (false !== $php = $executableFinder->find()) { |
|
| 124 | 5 | $prefix = array($php); |
|
| 125 | |||
| 126 | 5 | if ($input->getOption('child-process-php-ini-config')) { |
|
| 127 | foreach ($input->getOption('child-process-php-ini-config') as $iniSpec) { |
||
| 128 | $ini = explode(':', $iniSpec, 2); |
||
| 129 | if (count($ini) < 2 || $ini[0] === '') { |
||
| 130 | throw new \InvalidArgumentException("Invalid php ini specification: '$iniSpec'"); |
||
| 131 | } |
||
| 132 | $prefix[] = '-d ' . $ini[0] . '=' . $ini[1]; |
||
| 133 | } |
||
| 134 | } |
||
| 135 | |||
| 136 | 5 | $builder->setPrefix($prefix); |
|
| 137 | } |
||
| 138 | 5 | $builderArgs = $this->createChildProcessArgs($input); |
|
| 139 | } else { |
||
| 140 | $forcedRefs = array(); |
||
| 141 | if ($input->getOption('set-reference')) { |
||
| 142 | 97 | foreach ($input->getOption('set-reference') as $refSpec) { |
|
| 143 | 1 | $ref = explode(':', $refSpec, 2); |
|
| 144 | if (count($ref) < 2 || $ref[0] === '') { |
||
| 145 | throw new \InvalidArgumentException("Invalid reference specification: '$refSpec'"); |
||
| 146 | } |
||
| 147 | 97 | $forcedRefs[$ref[0]] = $ref[1]; |
|
| 148 | 1 | } |
|
| 149 | } |
||
| 150 | $migrationContext = array( |
||
| 151 | 97 | 'useTransaction' => !$input->getOption('no-transactions'), |
|
| 152 | 2 | 'defaultLanguageCode' => $input->getOption('default-language'), |
|
| 153 | 2 | 'adminUserLogin' => $input->getOption('admin-login'), |
|
| 154 | 2 | 'forceExecution' => $force, |
|
| 155 | 2 | 'forcedReferences' => $forcedRefs |
|
| 156 | ); |
||
| 157 | } |
||
| 158 | 2 | ||
| 159 | // For cli scripts, this means: do not die if anyone yanks out our stdout. |
||
| 160 | if ($input->getOption('survive-disconnected-tty')) { |
||
| 161 | ignore_user_abort(true); |
||
| 162 | 97 | } |
|
| 163 | 97 | ||
| 164 | 97 | // Allow forcing handling of sigchild. Useful on eg. Debian and Ubuntu |
|
| 165 | 97 | if ($input->getOption('force-sigchild-enabled')) { |
|
| 166 | 97 | Process::forceSigchildEnabled(true); |
|
| 167 | } |
||
| 168 | |||
| 169 | 97 | $aborted = false; |
|
| 170 | $executed = 0; |
||
| 171 | $failed = 0; |
||
| 172 | $skipped = 0; |
||
| 173 | 97 | $total = count($toExecute); |
|
| 174 | 7 | ||
| 175 | 7 | foreach ($toExecute as $name => $migrationDefinition) { |
|
| 176 | 7 | ||
| 177 | // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway |
||
| 178 | /// @todo should we save their 'skipped' status in the db? |
||
| 179 | 90 | if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) { |
|
| 180 | 9 | $output->writeln("<comment>Skipping $name</comment>"); |
|
| 181 | $skipped++; |
||
| 182 | 81 | continue; |
|
| 183 | } |
||
| 184 | |||
| 185 | 90 | if ($this->verbosity >= OutputInterface::VERBOSITY_VERBOSE) { |
|
| 186 | $this->writeln("<info>Processing $name (from definition $migrationDefinition->path)</info>"); |
||
| 187 | } else { |
||
| 188 | 5 | $this->writeln("<info>Processing $name</info>"); |
|
| 189 | } |
||
| 190 | 5 | ||
| 191 | if ($input->getOption('separate-process')) { |
||
| 192 | |||
| 193 | try { |
||
| 194 | $this->executeMigrationInSeparateProcess($migrationDefinition, $migrationService, $builder, $builderArgs); |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
Comprehensibility
Best Practice
introduced
by
|
|||
| 195 | |||
| 196 | $executed++; |
||
| 197 | } catch (\Exception $e) { |
||
| 198 | $failed++; |
||
| 199 | |||
| 200 | $errorMessage = $e->getMessage(); |
||
| 201 | // we probably have already echoed the error message while the subprocess was executing, avoid repeating it |
||
| 202 | if ($errorMessage != $this->subProcessErrorString) { |
||
| 203 | /// @todo atm this is impossible case - executeMigrationInSeparateProcess does not know enough |
||
| 204 | /// to throw an AfterMigrationExecutionException |
||
| 205 | if ($e instanceof AfterMigrationExecutionException) { |
||
| 206 | $errorMessage = "Failure after migration end! Reason: " . $errorMessage; |
||
| 207 | } else { |
||
| 208 | 5 | $errorMessage = "Migration failed! Reason: " . $errorMessage; |
|
| 209 | } |
||
| 210 | |||
| 211 | $this->writeErrorln("\n<error>$errorMessage</error>"); |
||
| 212 | } |
||
| 213 | |||
| 214 | if (!$input->getOption('ignore-failures')) { |
||
| 215 | 85 | $aborted = true; |
|
| 216 | break; |
||
| 217 | 57 | } |
|
| 218 | 28 | } |
|
| 219 | 28 | ||
| 220 | } else { |
||
| 221 | 28 | ||
| 222 | try { |
||
| 223 | 28 | $this->executeMigrationInProcess($migrationDefinition, $migrationService, $migrationContext); |
|
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
| 224 | 28 | ||
| 225 | 28 | $executed++; |
|
| 226 | |||
| 227 | // in case the 1st mig changes values to the refs, we avoid injecting them in the 2nd mig and later |
||
| 228 | $migrationContext['forcedReferences'] = array(); |
||
| 229 | } catch (\Exception $e) { |
||
| 230 | $failed++; |
||
| 231 | |||
| 232 | 97 | $errorMessage = $e->getMessage(); |
|
| 233 | if ($e instanceof AfterMigrationExecutionException) { |
||
| 234 | 97 | $errorMessage = "Failure after migration end! Reason: " . $errorMessage; |
|
| 235 | 28 | } else { |
|
| 236 | 28 | $errorMessage = "Migration failed! Reason: " . $errorMessage; |
|
| 237 | } |
||
| 238 | $this->writeErrorln("\n<error>$errorMessage</error>"); |
||
| 239 | |||
| 240 | if (!$input->getOption('ignore-failures')) { |
||
| 241 | 69 | $aborted = true; |
|
| 242 | break; |
||
| 243 | } |
||
| 244 | } |
||
| 245 | |||
| 246 | } |
||
| 247 | } |
||
| 248 | 97 | ||
| 249 | $missed = $total - $executed - $failed - $skipped; |
||
| 250 | 97 | ||
| 251 | 97 | if ($aborted) { |
|
| 252 | if ($missed > 0) { |
||
| 253 | 5 | $this->writeErrorln("\n<error>Migration execution aborted</error>"); |
|
| 254 | } |
||
| 255 | 92 | } else { |
|
| 256 | // NB: as per the Sf doc at https://symfony.com/doc/2.7/console/calling_commands.html, the 'cache:clear' |
||
| 257 | // command should be run 'at the end', as they change some class definitions |
||
| 258 | 97 | if ($input->getOption('clear-cache')) { |
|
| 259 | $command = $this->getApplication()->find('cache:clear'); |
||
| 260 | $inputArray = new ArrayInput(array('command' => 'cache:clear')); |
||
| 261 | $command->run($inputArray, $output); |
||
| 262 | } |
||
| 263 | } |
||
| 264 | |||
| 265 | $this->writeln("\nExecuted $executed migrations, failed $failed, skipped $skipped" . ($missed ? ", missed $missed" : '')); |
||
| 266 | |||
| 267 | 85 | $time = microtime(true) - $start; |
|
| 268 | if ($input->getOption('separate-process')) { |
||
| 269 | 85 | // in case of using subprocesses, we can not measure max memory used |
|
| 270 | 85 | $this->writeln("<info>Time taken: ".sprintf('%.3f', $time)." secs</info>"); |
|
| 271 | 85 | } else { |
|
| 272 | 85 | $this->writeln("<info>Time taken: ".sprintf('%.3f', $time)." secs, memory: ".sprintf('%.2f', (memory_get_peak_usage(true) / 1000000)). ' MB</info>'); |
|
| 273 | 85 | } |
|
| 274 | |||
| 275 | return $failed; |
||
| 276 | 57 | } |
|
| 277 | |||
| 278 | /** |
||
| 279 | * @param MigrationDefinition $migrationDefinition |
||
| 280 | * @param MigrationService $migrationService |
||
| 281 | * @param array $migrationContext |
||
| 282 | */ |
||
| 283 | protected function executeMigrationInProcess($migrationDefinition, $migrationService, $migrationContext) |
||
| 284 | { |
||
| 285 | 5 | $migrationService->executeMigration( |
|
| 286 | $migrationDefinition, |
||
| 287 | $migrationContext |
||
| 288 | 5 | ); |
|
| 289 | 5 | } |
|
| 290 | |||
| 291 | 5 | /** |
|
| 292 | 5 | * @param MigrationDefinition $migrationDefinition |
|
| 293 | * @param MigrationService $migrationService |
||
| 294 | * @param ProcessBuilder $builder |
||
| 295 | 5 | * @param array $builderArgs |
|
| 296 | * @param bool $feedback |
||
| 297 | */ |
||
| 298 | 5 | protected function executeMigrationInSeparateProcess($migrationDefinition, $migrationService, $builder, $builderArgs, $feedback = true) |
|
| 299 | { |
||
| 300 | $process = $builder |
||
| 301 | ->setArguments(array_merge($builderArgs, array('--path=' . $migrationDefinition->path))) |
||
| 302 | ->getProcess(); |
||
| 303 | |||
| 304 | if ($feedback) { |
||
| 305 | 5 | $this->writeln('<info>Executing: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_NORMAL); |
|
| 306 | 5 | } |
|
| 307 | |||
| 308 | 4 | $this->subProcessErrorString = ''; |
|
| 309 | 1 | ||
| 310 | 1 | // allow long migrations processes by default |
|
| 311 | $process->setTimeout($this->subProcessTimeout); |
||
| 312 | |||
| 313 | 4 | // and give immediate feedback to the user... |
|
| 314 | // NB: if the subprocess writes to stderr then terminates with non-0 exit code, this will lead us to echoing the |
||
| 315 | 5 | // error text twice, once here and once at the end of execution of this command. |
|
| 316 | // In order to avoid that, since we can not know at this time what the subprocess exit code will be, we |
||
| 317 | // do print the error text now, and compare it to what we get at the end... |
||
| 318 | 5 | $process->run( |
|
| 319 | $feedback ? |
||
| 320 | function($type, $buffer) { |
||
| 321 | 5 | if ($type == 'err') { |
|
| 322 | $this->subProcessErrorString .= $buffer; |
||
| 323 | $this->writeErrorln($buffer, OutputInterface::VERBOSITY_QUIET, OutputInterface::OUTPUT_RAW); |
||
| 324 | } else { |
||
| 325 | // swallow output of child processes in quiet mode |
||
| 326 | $this->writeLn($buffer, self::$VERBOSITY_CHILD, OutputInterface::OUTPUT_RAW); |
||
| 327 | } |
||
| 328 | } |
||
| 329 | : |
||
| 330 | function($type, $buffer) { |
||
| 331 | } |
||
| 332 | ); |
||
| 333 | |||
| 334 | if (!$process->isSuccessful()) { |
||
| 335 | $errorOutput = $process->getErrorOutput(); |
||
| 336 | /// @todo should we always add the exit code to the error message, even when $errorOutput is not null ? |
||
| 337 | if ($errorOutput === '') { |
||
| 338 | $errorOutput = "(separate process used to execute migration failed with no stderr output. Its exit code was: " . $process->getExitCode(); |
||
| 339 | // We go out of our way to help the user finding the cause of the error. |
||
| 340 | /// @todo another cause we might check for in case of empty $errorOutput is when error_reporting does |
||
| 341 | /// not include fatal errors (E_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR?) |
||
| 342 | $errorLog = ini_get("error_log"); |
||
| 343 | if ($errorLog != '' && $errorLog != 'syslog') { |
||
| 344 | 5 | $errorOutput .= ". Error details might be in file $errorLog"; |
|
| 345 | } |
||
| 346 | 5 | if ($process->getExitCode() == -1) { |
|
| 347 | $errorOutput .= ". If you are using Debian or Ubuntu linux, please consider using the --force-sigchild-enabled option."; |
||
| 348 | } |
||
| 349 | 5 | $errorOutput .= ")"; |
|
| 350 | } |
||
| 351 | throw new MigrationBundleException($errorOutput); |
||
| 352 | } |
||
| 353 | |||
| 354 | // There are cases where the separate process dies halfway but does not return a non-zero code. |
||
| 355 | // That's why we double-check here if the migration is still tagged as 'started'... |
||
| 356 | /** @var Migration $migration */ |
||
| 357 | $migration = $migrationService->getMigration($migrationDefinition->name); |
||
| 358 | |||
| 359 | if (!$migration) { |
||
| 360 | // q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace... |
||
| 361 | 5 | throw new MigrationBundleException("After the separate process charged to execute the migration finished, the migration can not be found in the database any more."); |
|
| 362 | } else if ($migration->status == Migration::STATUS_STARTED) { |
||
| 363 | $errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution."; |
||
| 364 | $migrationService->endMigration(New Migration( |
||
| 365 | $migration->name, |
||
| 366 | $migration->md5, |
||
| 367 | $migration->path, |
||
| 368 | $migration->executionDate, |
||
| 369 | Migration::STATUS_FAILED, |
||
| 370 | ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg) |
||
| 371 | 98 | )); |
|
| 372 | throw new MigrationBundleException($errorMsg); |
||
| 373 | 98 | } |
|
| 374 | 98 | } |
|
| 375 | |||
| 376 | 98 | /** |
|
| 377 | 98 | * @param string[] $paths |
|
| 378 | 2 | * @param MigrationService $migrationService |
|
| 379 | * @param bool $force when true, look not only for TODO migrations, but also DONE, SKIPPED, FAILED ones (we still omit STARTED and SUSPENDED ones) |
||
| 380 | * @return MigrationDefinition[] |
||
| 381 | * |
||
| 382 | 98 | * @todo optimize. This does not scale well with many definitions or migrations |
|
| 383 | 98 | */ |
|
| 384 | 98 | protected function buildMigrationsList($paths, $migrationService, $force = false) |
|
| 385 | 97 | { |
|
| 386 | $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths); |
||
| 387 | $migrations = $migrationService->getMigrations(); |
||
| 388 | |||
| 389 | $allowedStatuses = array(Migration::STATUS_TODO); |
||
| 390 | if ($force) { |
||
| 391 | 98 | $allowedStatuses = array_merge($allowedStatuses, array(Migration::STATUS_DONE, Migration::STATUS_FAILED, Migration::STATUS_SKIPPED)); |
|
| 392 | } |
||
| 393 | |||
| 394 | // filter away all migrations except 'to do' ones |
||
| 395 | $toExecute = array(); |
||
| 396 | foreach ($migrationDefinitions as $name => $migrationDefinition) { |
||
| 397 | if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && in_array($migration->status, $allowedStatuses))) { |
||
| 398 | $toExecute[$name] = $migrationService->parseMigrationDefinition($migrationDefinition); |
||
| 399 | } |
||
| 400 | } |
||
| 401 | |||
| 402 | // if user wants to execute 'all' migrations: look for those which are registered in the database even if not |
||
| 403 | // found by the loader |
||
| 404 | if (empty($paths)) { |
||
| 405 | foreach ($migrations as $migration) { |
||
| 406 | if (in_array($migration->status, $allowedStatuses) && !isset($toExecute[$migration->name])) { |
||
| 407 | try { |
||
| 408 | $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path)); |
||
| 409 | if (count($migrationDefinitions)) { |
||
| 410 | 98 | // q: shall we raise a warning here if migrations found > 1? |
|
| 411 | $migrationDefinition = $migrationDefinitions->reset(); |
||
| 412 | 98 | $toExecute[$migration->name] = $migrationService->parseMigrationDefinition($migrationDefinition); |
|
| 413 | } else { |
||
| 414 | throw new MigrationBundleException("Migration definition not found at path '$migration->path'"); |
||
| 415 | } |
||
| 416 | } catch (\Exception $e) { |
||
| 417 | $this->writeErrorln("Error while loading definition for migration '{$migration->name}' registered in the database, skipping it: " . $e->getMessage()); |
||
| 418 | } |
||
| 419 | } |
||
| 420 | } |
||
| 421 | } |
||
| 422 | 97 | ||
| 423 | ksort($toExecute); |
||
| 424 | 97 | ||
| 425 | 97 | return $toExecute; |
|
| 426 | 97 | } |
|
| 427 | 97 | ||
| 428 | 97 | /** |
|
| 429 | 7 | * @param MigrationDefinition[] $toExecute |
|
| 430 | * @param InputInterface $input |
||
| 431 | 97 | * @param OutputInterface $output |
|
| 432 | 97 | * |
|
| 433 | 97 | * @todo use a more compact output when there are *many* migrations |
|
| 434 | 97 | */ |
|
| 435 | protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output) |
||
| 436 | { |
||
| 437 | $data = array(); |
||
| 438 | 97 | $i = 1; |
|
| 439 | 97 | foreach ($toExecute as $name => $migrationDefinition) { |
|
| 440 | $notes = ''; |
||
| 441 | 97 | if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) { |
|
| 442 | 97 | $notes = '<error>' . $migrationDefinition->parsingError . '</error>'; |
|
| 443 | 97 | } |
|
| 444 | $data[] = array( |
||
| 445 | $i++, |
||
| 446 | 97 | $name, |
|
| 447 | 97 | $notes |
|
| 448 | ); |
||
| 449 | 97 | } |
|
| 450 | |||
| 451 | 97 | if (!$input->getOption('child')) { |
|
| 452 | $table = new Table($output); |
||
| 453 | $table |
||
| 454 | ->setHeaders(array('#', 'Migration', 'Notes')) |
||
| 455 | ->setRows($data); |
||
| 456 | $table->render(); |
||
| 457 | } |
||
| 458 | |||
| 459 | $this->writeln(''); |
||
| 460 | } |
||
| 461 | |||
| 462 | protected function askForConfirmation(InputInterface $input, OutputInterface $output, $nonIteractiveOutput = "=============================================\n") |
||
| 463 | 97 | { |
|
| 464 | 97 | if ($input->isInteractive() && !$input->getOption('no-interaction')) { |
|
| 465 | $dialog = $this->getHelperSet()->get('question'); |
||
| 466 | if (!$dialog->ask( |
||
| 467 | $input, |
||
| 468 | 97 | $output, |
|
| 469 | new ConfirmationQuestion('<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>', false) |
||
| 470 | ) |
||
| 471 | ) { |
||
| 472 | $output->writeln('<error>Migration execution cancelled!</error>'); |
||
| 473 | return 0; |
||
| 474 | } |
||
| 475 | } else { |
||
| 476 | if ($nonIteractiveOutput != '') { |
||
| 477 | $this->writeln("$nonIteractiveOutput"); |
||
| 478 | 5 | } |
|
| 479 | } |
||
| 480 | 5 | ||
| 481 | return 1; |
||
| 482 | } |
||
| 483 | |||
| 484 | 5 | /** |
|
| 485 | 5 | * Returns the command-line arguments needed to execute a migration in a separate subprocess |
|
| 486 | 5 | * (except path, which should be added after this call) |
|
| 487 | 5 | * @param InputInterface $input |
|
| 488 | * @return array |
||
| 489 | * @todo check if it is a good idea to pass on the current verbosity |
||
| 490 | 5 | */ |
|
| 491 | 5 | protected function createChildProcessArgs(InputInterface $input) |
|
| 492 | { |
||
| 493 | 5 | $kernel = $this->getContainer()->get('kernel'); |
|
| 494 | |||
| 495 | // mandatory args and options |
||
| 496 | 5 | $builderArgs = array( |
|
| 497 | $this->getConsoleFile(), // sf console |
||
| 498 | 1 | self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding? |
|
| 499 | 1 | '--env=' . $kernel->getEnvironment(), // sf env |
|
| 500 | '--child' |
||
| 501 | 1 | ); |
|
| 502 | 1 | // sf/ez env options |
|
| 503 | if (!$kernel->isDebug()) { |
||
| 504 | $builderArgs[] = '--no-debug'; |
||
| 505 | } |
||
| 506 | if ($input->getOption('siteaccess')) { |
||
| 507 | $builderArgs[] = '--siteaccess='.$input->getOption('siteaccess'); |
||
| 508 | } |
||
| 509 | 5 | switch ($this->verbosity) { |
|
| 510 | case OutputInterface::VERBOSITY_VERBOSE: |
||
| 511 | $builderArgs[] = '-v'; |
||
| 512 | 5 | break; |
|
| 513 | case OutputInterface::VERBOSITY_VERY_VERBOSE: |
||
| 514 | $builderArgs[] = '-vv'; |
||
| 515 | 5 | break; |
|
| 516 | case OutputInterface::VERBOSITY_DEBUG: |
||
| 517 | $builderArgs[] = '-vvv'; |
||
| 518 | break; |
||
| 519 | 5 | } |
|
| 520 | // 'optional' options |
||
| 521 | // note: options 'clear-cache', 'ignore-failures', 'no-interaction', 'path', 'separate-process' and 'survive-disconnected-tty' we never propagate |
||
| 522 | 5 | if ($input->getOption('admin-login')) { |
|
| 523 | $builderArgs[] = '--admin-login=' . $input->getOption('admin-login'); |
||
| 524 | } |
||
| 525 | 5 | if ($input->getOption('default-language')) { |
|
| 526 | $builderArgs[] = '--default-language=' . $input->getOption('default-language'); |
||
| 527 | } |
||
| 528 | if ($input->getOption('force')) { |
||
| 529 | $builderArgs[] = '--force'; |
||
| 530 | 5 | } |
|
| 531 | // useful in case the subprocess has a migration step of type process/run |
||
| 532 | if ($input->getOption('force-sigchild-enabled')) { |
||
| 533 | $builderArgs[] = '--force-sigchild-enabled'; |
||
| 534 | } |
||
| 535 | if ($input->getOption('no-transactions')) { |
||
| 536 | $builderArgs[] = '--no-transactions'; |
||
| 537 | } |
||
| 538 | 5 | if ($input->getOption('set-reference')) { |
|
| 539 | foreach ($input->getOption('set-reference') as $refSpec) { |
||
| 540 | 5 | $builderArgs[] = '--set-reference=' . $refSpec; |
|
| 541 | 5 | } |
|
| 542 | 5 | } |
|
| 543 | return $builderArgs; |
||
| 544 | } |
||
| 545 | 5 | ||
| 546 | 5 | /** |
|
| 547 | * Returns the file-path of the symfony console in use, based on simple heuristics |
||
| 548 | * @return string |
||
| 549 | * @todo improve how we look for the console: we could fe. scan all of the files in the kernel dir, or look up the full process info based on its pid |
||
| 550 | */ |
||
| 551 | protected function getConsoleFile() |
||
| 552 | { |
||
| 553 | if (strpos($_SERVER['argv'][0], 'phpunit') !== false) { |
||
| 554 | $kernelDir = $this->getContainer()->get('kernel')->getRootDir(); |
||
| 555 | if (is_file("$kernelDir/console")) { |
||
| 556 | return "$kernelDir/console"; |
||
| 557 | } |
||
| 558 | if (is_file("$kernelDir/../bin/console")) { |
||
| 559 | return "$kernelDir/../bin/console"; |
||
| 560 | } |
||
| 561 | throw new MigrationBundleException("Can not determine the name of the symfony console file in use for running as separate process"); |
||
| 562 | } |
||
| 563 | |||
| 564 | return $_SERVER['argv'][0]; // sf console |
||
| 565 | } |
||
| 566 | } |
||
| 567 |