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; |
||||||
|
0 ignored issues
–
show
|
|||||||
| 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); |
|||||
|
0 ignored issues
–
show
$prefix of type array<integer,string> is incompatible with the type string expected by parameter $prefix of Symfony\Component\Proces...essBuilder::setPrefix().
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 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
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); |
||||||
|
0 ignored issues
–
show
self::VERBOSITY_CHILD of type double is incompatible with the type integer expected by parameter $verbosity of Kaliop\eZMigrationBundle...tractCommand::writeln().
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 327 | } |
||||||
| 328 | } |
||||||
| 329 | : |
||||||
| 330 | function($type, $buffer) { |
||||||
|
0 ignored issues
–
show
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
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...
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
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...
|
|||||||
| 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) { |
||||||
|
0 ignored issues
–
show
|
|||||||
| 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( |
||||||
|
0 ignored issues
–
show
The method
ask() does not exist on Symfony\Component\Console\Helper\HelperInterface. It seems like you code against a sub-type of Symfony\Component\Console\Helper\HelperInterface such as 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
Loading history...
|
|||||||
| 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 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths