1 | <?php |
||||
2 | |||||
3 | namespace Kaliop\eZMigrationBundle\Command; |
||||
4 | |||||
5 | use Kaliop\eZMigrationBundle\API\Exception\AfterMigrationExecutionException; |
||||
6 | use Kaliop\eZMigrationBundle\API\Value\Migration; |
||||
7 | use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; |
||||
8 | use Kaliop\eZMigrationBundle\Core\Helper\ProcessManager; |
||||
9 | use Kaliop\eZMigrationBundle\Core\Process\Process; |
||||
10 | use Kaliop\eZMigrationBundle\Core\Process\ProcessBuilder; |
||||
11 | use Symfony\Component\Console\Input\ArrayInput; |
||||
12 | use Symfony\Component\Console\Input\InputOption; |
||||
13 | use Symfony\Component\Console\Input\InputInterface; |
||||
14 | use Symfony\Component\Console\Output\Output; |
||||
15 | use Symfony\Component\Console\Output\OutputInterface; |
||||
16 | use Symfony\Component\Process\PhpExecutableFinder; |
||||
17 | |||||
18 | class MassMigrateCommand extends MigrateCommand |
||||
19 | { |
||||
20 | const COMMAND_NAME = 'kaliop:migration:mass_migrate'; |
||||
21 | |||||
22 | // Note: in this array, we lump together in STATUS_DONE everything which is not failed or suspended |
||||
23 | protected $migrationsDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0); |
||||
24 | protected $migrationsAlreadyDone = array(); |
||||
25 | |||||
26 | /** |
||||
27 | * @todo (!important) can we rename the option --separate-process ? |
||||
28 | */ |
||||
29 | protected function configure() |
||||
30 | 148 | { |
|||
31 | parent::configure(); |
||||
32 | 148 | ||||
33 | $this |
||||
34 | ->setName(self::COMMAND_NAME) |
||||
35 | 148 | ->setAliases(array()) |
|||
36 | 148 | ->setDescription('Executes available migration definitions, using parallelism.') |
|||
37 | 148 | ->addOption('concurrency', 'r', InputOption::VALUE_REQUIRED, "The number of executors to run in parallel", 2) |
|||
38 | 148 | ->setHelp(<<<EOT |
|||
39 | 148 | This command is designed to scan recursively a directory for migration files and execute them all in parallel. |
|||
40 | 148 | One child process will be spawned for each subdirectory found. |
|||
41 | The maximum number of processes to run in parallel is specified via the 'concurrency' option. |
||||
42 | <info>NB: this command does not guarantee that any given migration will be executed before another. Take care about dependencies.</info> |
||||
43 | <info>NB: the rule that each migration filename has to be unique still applies, even if migrations are spread across different directories.</info> |
||||
44 | Unlike for the 'normal' migration command, it is not recommended to use the <info>--separate-process</info> option, as it will make execution slower if you have many migrations |
||||
45 | EOT |
||||
46 | ) |
||||
47 | ; |
||||
48 | } |
||||
49 | 148 | ||||
50 | /** |
||||
51 | * Execute the command. |
||||
52 | * |
||||
53 | * @param InputInterface $input |
||||
54 | * @param OutputInterface $output |
||||
55 | * @return null|int null or 0 if everything went fine, or an error code |
||||
56 | */ |
||||
57 | protected function execute(InputInterface $input, OutputInterface $output) |
||||
58 | { |
||||
59 | $start = microtime(true); |
||||
60 | |||||
61 | $this->setOutput($output); |
||||
62 | $this->setVerbosity($output->getVerbosity()); |
||||
63 | |||||
64 | $isChild = $input->getOption('child'); |
||||
65 | |||||
66 | if ($isChild && $output->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) { |
||||
67 | $this->setVerbosity(self::$VERBOSITY_CHILD); |
||||
68 | } |
||||
69 | |||||
70 | $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output); |
||||
71 | |||||
72 | // q: is it worth declaring a new, dedicated migration service ? |
||||
73 | $migrationService = $this->getMigrationService(); |
||||
74 | $migrationService->setLoader($this->getContainer()->get('ez_migration_bundle.loader.filesystem_recursive')); |
||||
75 | |||||
76 | $force = $input->getOption('force'); |
||||
77 | |||||
78 | $toExecute = $this->buildMigrationsList($this->normalizePaths($input->getOption('path')), $migrationService, $force, $isChild); |
||||
79 | |||||
80 | if (!count($toExecute)) { |
||||
81 | $this->writeln('<info>No migrations to execute</info>'); |
||||
82 | return 0; |
||||
83 | } |
||||
84 | |||||
85 | if ($isChild) { |
||||
86 | return $this->executeAsChild($input, $output, $toExecute, $force, $migrationService); |
||||
87 | } else { |
||||
88 | return $this->executeAsParent($input, $output, $toExecute, $start); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
89 | } |
||||
90 | } |
||||
91 | |||||
92 | /** |
||||
93 | * @param InputInterface $input |
||||
94 | * @param OutputInterface $output |
||||
95 | * @param MigrationDefinition[] $toExecute |
||||
96 | * @param float $start |
||||
97 | * @return int |
||||
98 | */ |
||||
99 | protected function executeAsParent($input, $output, $toExecute, $start) |
||||
100 | { |
||||
101 | $paths = $this->groupMigrationsByPath($toExecute); |
||||
102 | $this->printMigrationsList($toExecute, $input, $output, $paths); |
||||
103 | |||||
104 | // ask user for confirmation to make changes |
||||
105 | if (!$this->askForConfirmation($input, $output, null)) { |
||||
106 | return 0; |
||||
107 | } |
||||
108 | |||||
109 | // For cli scripts, this means: do not die if anyone yanks out our stdout. |
||||
110 | // We presume that users who want to halt migrations do send us a KILL signal, and that a lost tty is |
||||
111 | // generally a mistake, and that carrying on with executing migrations is the best outcome |
||||
112 | if ($input->getOption('survive-disconnected-tty')) { |
||||
113 | ignore_user_abort(true); |
||||
114 | } |
||||
115 | |||||
116 | $concurrency = $input->getOption('concurrency'); |
||||
117 | $this->writeln("Executing migrations using " . count($paths) . " processes with a concurrency of $concurrency"); |
||||
118 | |||||
119 | // Allow forcing handling of sigchild. Useful on eg. Debian and Ubuntu |
||||
120 | if ($input->getOption('force-sigchild-enabled')) { |
||||
121 | Process::forceSigchildEnabled(true); |
||||
122 | } |
||||
123 | |||||
124 | $builder = new ProcessBuilder(); |
||||
125 | $executableFinder = new PhpExecutableFinder(); |
||||
126 | if (false !== ($php = $executableFinder->find())) { |
||||
127 | $builder->setPrefix($php); |
||||
128 | } |
||||
129 | |||||
130 | // mandatory args and options |
||||
131 | $builderArgs = $this->createChildProcessArgs($input); |
||||
132 | |||||
133 | $processes = array(); |
||||
134 | /** @var MigrationDefinition $migrationDefinition */ |
||||
135 | foreach ($paths as $path => $count) { |
||||
136 | $this->writeln("<info>Queueing processing of: $path ($count migrations)</info>", OutputInterface::VERBOSITY_VERBOSE); |
||||
137 | |||||
138 | $process = $builder |
||||
139 | ->setArguments(array_merge($builderArgs, array('--path=' . $path))) |
||||
140 | ->getProcess(); |
||||
141 | |||||
142 | $this->writeln('<info>Command: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE); |
||||
143 | |||||
144 | // allow long migrations processes by default |
||||
145 | $process->setTimeout($this->subProcessTimeout); |
||||
146 | $processes[] = $process; |
||||
147 | } |
||||
148 | |||||
149 | $this->writeln("<info>Starting queued processes...</info>"); |
||||
150 | |||||
151 | $total = count($toExecute); |
||||
152 | $this->migrationsDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0); |
||||
153 | |||||
154 | $processManager = new ProcessManager(); |
||||
155 | $processManager->runParallel($processes, $concurrency, 500, array($this, 'onChildProcessOutput')); |
||||
156 | |||||
157 | $subprocessesFailed = 0; |
||||
158 | foreach ($processes as $i => $process) { |
||||
159 | if (!$process->isSuccessful()) { |
||||
160 | $errorOutput = $process->getErrorOutput(); |
||||
161 | if ($errorOutput === '') { |
||||
162 | $errorOutput = "(process used to execute migrations failed with no stderr output. Its exit code was: " . $process->getExitCode(); |
||||
163 | // We go out of our way to help the user finding the cause of the error. |
||||
164 | /// @todo another cause we might check for in case of an empty $errorOutput is when error_reporting |
||||
165 | /// does not include fatal errors (E_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR?) |
||||
166 | $errorLog = ini_get("error_log"); |
||||
167 | if ($errorLog != '' && $errorLog != 'syslog') { |
||||
168 | $errorOutput .= ". Error details might be in file $errorLog"; |
||||
169 | } |
||||
170 | if ($process->getExitCode() == -1) { |
||||
171 | $errorOutput .= ". If you are using Debian or Ubuntu linux, please consider using the --force-sigchild-enabled option."; |
||||
172 | } |
||||
173 | $errorOutput .= ")"; |
||||
174 | } |
||||
175 | /// @todo should we always add the exit code, even when $errorOutput is not null ? |
||||
176 | $this->writeErrorln("\n<error>Subprocess $i failed! Reason: " . $errorOutput . "</error>\n"); |
||||
177 | $subprocessesFailed++; |
||||
178 | } |
||||
179 | } |
||||
180 | |||||
181 | if ($input->getOption('clear-cache')) { |
||||
182 | /// @see the comment in the parent class about the problems tied to clearing Sf cache in-process |
||||
183 | $command = $this->getApplication()->find('cache:clear'); |
||||
184 | $inputArray = new ArrayInput(array('command' => 'cache:clear')); |
||||
185 | $command->run($inputArray, $output); |
||||
186 | } |
||||
187 | |||||
188 | $missed = $total - $this->migrationsDone[Migration::STATUS_DONE] - $this->migrationsDone[Migration::STATUS_FAILED] - $this->migrationsDone[Migration::STATUS_SKIPPED]; |
||||
189 | $this->writeln("\nExecuted ".$this->migrationsDone[Migration::STATUS_DONE].' migrations'. |
||||
190 | ', failed '.$this->migrationsDone[Migration::STATUS_FAILED]. |
||||
191 | ', skipped '.$this->migrationsDone[Migration::STATUS_SKIPPED]. |
||||
192 | ($missed ? ", missed $missed" : '')); |
||||
193 | |||||
194 | $time = microtime(true) - $start; |
||||
195 | // since we use subprocesses, we can not measure max memory used |
||||
196 | $this->writeln("<info>Time taken: ".sprintf('%.3f', $time)." secs</info>"); |
||||
197 | |||||
198 | return $subprocessesFailed + $this->migrationsDone[Migration::STATUS_FAILED] + $missed; |
||||
199 | } |
||||
200 | |||||
201 | /** |
||||
202 | * @param InputInterface $input |
||||
203 | * @param OutputInterface $output |
||||
204 | * @param MigrationDefinition[] $toExecute |
||||
205 | * @param bool $force |
||||
206 | * @param $migrationService |
||||
207 | * @return int |
||||
208 | * @todo does it make sense to honour the `survive-disconnected-tty` flag when executing as child? |
||||
209 | */ |
||||
210 | protected function executeAsChild($input, $output, $toExecute, $force, $migrationService) |
||||
0 ignored issues
–
show
The parameter
$output 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. ![]() |
|||||
211 | { |
||||
212 | // @todo disable signal slots that are harmful during migrations, if any |
||||
213 | |||||
214 | if ($input->getOption('separate-process')) { |
||||
215 | $builder = new ProcessBuilder(); |
||||
216 | $executableFinder = new PhpExecutableFinder(); |
||||
217 | if (false !== $php = $executableFinder->find()) { |
||||
218 | $prefix = array($php); |
||||
219 | |||||
220 | if ($input->getOption('child-process-php-ini-config')) { |
||||
221 | foreach ($input->getOption('child-process-php-ini-config') as $iniSpec) { |
||||
222 | $ini = explode(':', $iniSpec, 2); |
||||
223 | if (count($ini) < 2 || $ini[0] === '') { |
||||
224 | throw new \InvalidArgumentException("Invalid php ini specification: '$iniSpec'"); |
||||
225 | } |
||||
226 | $prefix[] = '-d ' . $ini[0] . '=' . $ini[1]; |
||||
227 | } |
||||
228 | } |
||||
229 | |||||
230 | $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
![]() |
|||||
231 | } |
||||
232 | |||||
233 | $builderArgs = parent::createChildProcessArgs($input); |
||||
234 | } else { |
||||
235 | $forcedRefs = array(); |
||||
236 | if ($input->getOption('set-reference')) { |
||||
237 | foreach ($input->getOption('set-reference') as $refSpec) { |
||||
238 | $ref = explode(':', $refSpec, 2); |
||||
239 | if (count($ref) < 2 || $ref[0] === '') { |
||||
240 | throw new \InvalidArgumentException("Invalid reference specification: '$refSpec'"); |
||||
241 | } |
||||
242 | $forcedRefs[$ref[0]] = $ref[1]; |
||||
243 | } |
||||
244 | } |
||||
245 | $migrationContext = array( |
||||
246 | 'useTransactions' => !$input->getOption('no-transactions'), |
||||
247 | 'defaultLanguageCode' => $input->getOption('default-language'), |
||||
248 | 'adminUserLogin' => $input->getOption('admin-login'), |
||||
249 | 'forceExecution' => $force, |
||||
250 | 'forcedReferences' => $forcedRefs |
||||
251 | ); |
||||
252 | } |
||||
253 | |||||
254 | // Allow forcing handling of sigchild. Useful on eg. Debian and Ubuntu |
||||
255 | if ($input->getOption('force-sigchild-enabled')) { |
||||
256 | Process::forceSigchildEnabled(true); |
||||
257 | } |
||||
258 | |||||
259 | $aborted = false; |
||||
260 | $executed = 0; |
||||
261 | $failed = 0; |
||||
262 | $skipped = 0; |
||||
263 | $total = count($toExecute); |
||||
264 | |||||
265 | foreach ($toExecute as $name => $migrationDefinition) { |
||||
266 | // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway |
||||
267 | if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) { |
||||
268 | $this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", self::$VERBOSITY_CHILD); |
||||
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
![]() |
|||||
269 | $skipped++; |
||||
270 | continue; |
||||
271 | } |
||||
272 | |||||
273 | $this->writeln("<info>Processing $name</info>", self::$VERBOSITY_CHILD); |
||||
274 | |||||
275 | if ($input->getOption('separate-process')) { |
||||
276 | |||||
277 | try { |
||||
278 | $this->executeMigrationInSeparateProcess($migrationDefinition, $migrationService, $builder, $builderArgs); |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||||
279 | |||||
280 | $executed++; |
||||
281 | } catch (\Exception $e) { |
||||
282 | $failed++; |
||||
283 | |||||
284 | $errorMessage = $e->getMessage(); |
||||
285 | if ($errorMessage != $this->subProcessErrorString) { |
||||
286 | /// @todo move these bits of strings to class constants |
||||
287 | $errorMessage = preg_replace('/^\n*(\[[0-9]*\])?(Migration failed|Failure after migration end)! Reason: +/', '', $errorMessage); |
||||
288 | /// @todo atm this is impossible case - executeMigrationInSeparateProcess does not know enough |
||||
289 | /// to throw an AfterMigrationExecutionException |
||||
290 | if ($e instanceof AfterMigrationExecutionException) { |
||||
291 | $errorMessage = "Failure after migration end! Path: " . $migrationDefinition->path . ", Reason: " . $errorMessage; |
||||
292 | } else { |
||||
293 | $errorMessage = "Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $errorMessage; |
||||
294 | } |
||||
295 | |||||
296 | $this->writeErrorln("\n<error>$errorMessage</error>"); |
||||
297 | } |
||||
298 | |||||
299 | if (!$input->getOption('ignore-failures')) { |
||||
300 | $aborted = true; |
||||
301 | break; |
||||
302 | } |
||||
303 | } |
||||
304 | |||||
305 | } else { |
||||
306 | |||||
307 | try { |
||||
308 | $this->executeMigrationInProcess($migrationDefinition, $migrationService, $migrationContext); |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
309 | |||||
310 | $executed++; |
||||
311 | // in case the 1st mig changes values to the refs, we avoid injecting them in the 2nd mig and later |
||||
312 | $migrationContext['forcedReferences'] = array(); |
||||
313 | } catch (\Exception $e) { |
||||
314 | $failed++; |
||||
315 | |||||
316 | $errorMessage = $e->getMessage(); |
||||
317 | if ($e instanceof AfterMigrationExecutionException) { |
||||
318 | $errorMessage = "Failure after migration end! Path: " . $migrationDefinition->path . ", Reason: " . $errorMessage; |
||||
319 | } else { |
||||
320 | $errorMessage = "Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $errorMessage; |
||||
321 | } |
||||
322 | $this->writeErrorln("\n<error>$errorMessage</error>"); |
||||
323 | |||||
324 | if (!$input->getOption('ignore-failures')) { |
||||
325 | $aborted = true; |
||||
326 | break; |
||||
327 | } |
||||
328 | } |
||||
329 | |||||
330 | } |
||||
331 | } |
||||
332 | |||||
333 | $missed = $total - $executed - $failed - $skipped; |
||||
334 | |||||
335 | if ($aborted && $missed > 0) { |
||||
336 | $this->writeErrorln("\n<error>Migration execution aborted</error>"); |
||||
337 | } |
||||
338 | |||||
339 | $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, missed: $missed", self::$VERBOSITY_CHILD); |
||||
340 | |||||
341 | // We do not return an error code > 0 if migrations fail but , but only on proper fatals. |
||||
342 | // The parent will analyze the output of the child process to gather the number of executed/failed migrations anyway |
||||
343 | return 0; |
||||
344 | } |
||||
345 | |||||
346 | /** |
||||
347 | * @param string $type |
||||
348 | * @param string $buffer |
||||
349 | * @param null|\Symfony\Component\Process\Process $process |
||||
350 | */ |
||||
351 | public function onChildProcessOutput($type, $buffer, $process=null) |
||||
352 | { |
||||
353 | $lines = explode("\n", trim($buffer)); |
||||
354 | |||||
355 | foreach ($lines as $line) { |
||||
356 | if (preg_match('/Migrations executed: ([0-9]+), failed: ([0-9]+), skipped: ([0-9]+)/', $line, $matches)) { |
||||
357 | $this->migrationsDone[Migration::STATUS_DONE] += $matches[1]; |
||||
358 | $this->migrationsDone[Migration::STATUS_FAILED] += $matches[2]; |
||||
359 | $this->migrationsDone[Migration::STATUS_SKIPPED] += $matches[3]; |
||||
360 | |||||
361 | // swallow the recap lines unless we are in verbose mode |
||||
362 | if ($this->verbosity <= Output::VERBOSITY_NORMAL) { |
||||
363 | return; |
||||
364 | } |
||||
365 | } |
||||
366 | |||||
367 | // we tag the output with the id of the child process |
||||
368 | if (trim($line) !== '') { |
||||
369 | $msg = '[' . ($process ? $process->getPid() : ''). '] ' . trim($line); |
||||
370 | if ($type == 'err') { |
||||
371 | $this->writeErrorln($msg, OutputInterface::VERBOSITY_QUIET, OutputInterface::OUTPUT_RAW); |
||||
372 | } else { |
||||
373 | // swallow output of child processes in quiet mode |
||||
374 | $this->writeLn($msg, 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
![]() |
|||||
375 | } |
||||
376 | } |
||||
377 | } |
||||
378 | } |
||||
379 | |||||
380 | /** |
||||
381 | * @param string[] $paths |
||||
382 | * @param $migrationService |
||||
383 | * @param bool $force |
||||
384 | * @param bool $isChild when not in child mode, do not waste time parsing migrations |
||||
385 | * @return MigrationDefinition[] parsed or unparsed, depending on |
||||
386 | * |
||||
387 | * @todo this does not scale well with many definitions or migrations |
||||
388 | */ |
||||
389 | protected function buildMigrationsList($paths, $migrationService, $force = false, $isChild = false) |
||||
390 | { |
||||
391 | $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths); |
||||
392 | $migrations = $migrationService->getMigrations(); |
||||
393 | |||||
394 | $this->migrationsAlreadyDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0, Migration::STATUS_STARTED => 0); |
||||
395 | |||||
396 | $allowedStatuses = array(Migration::STATUS_TODO); |
||||
397 | if ($force) { |
||||
398 | $allowedStatuses = array_merge($allowedStatuses, array(Migration::STATUS_DONE, Migration::STATUS_FAILED, Migration::STATUS_SKIPPED)); |
||||
399 | } |
||||
400 | |||||
401 | // filter away all migrations except 'to do' ones |
||||
402 | $toExecute = array(); |
||||
403 | foreach ($migrationDefinitions as $name => $migrationDefinition) { |
||||
404 | if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && in_array($migration->status, $allowedStatuses))) { |
||||
405 | $toExecute[$name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition; |
||||
406 | } |
||||
407 | // save the list of non-executable migrations as well (even when using 'force') |
||||
408 | if (!$isChild && isset($migrations[$name]) && (($migration = $migrations[$name]) && $migration->status != Migration::STATUS_TODO)) { |
||||
409 | $this->migrationsAlreadyDone[$migration->status]++; |
||||
410 | } |
||||
411 | } |
||||
412 | |||||
413 | // if user wants to execute 'all' migrations: look for some which are registered in the database even if not |
||||
414 | // found by the loader |
||||
415 | if (empty($paths)) { |
||||
416 | foreach ($migrations as $migration) { |
||||
417 | if (in_array($migration->status, $allowedStatuses) && !isset($toExecute[$migration->name])) { |
||||
418 | $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path)); |
||||
419 | if (count($migrationDefinitions)) { |
||||
420 | $migrationDefinition = $migrationDefinitions->reset(); |
||||
421 | $toExecute[$migration->name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition; |
||||
422 | } else { |
||||
423 | // q: shall we raise a warning here ? |
||||
424 | } |
||||
425 | } |
||||
426 | } |
||||
427 | } |
||||
428 | |||||
429 | ksort($toExecute); |
||||
430 | |||||
431 | return $toExecute; |
||||
432 | } |
||||
433 | |||||
434 | /** |
||||
435 | * We use a more compact output when there are *many* migrations |
||||
436 | * @param MigrationDefinition[] $toExecute |
||||
437 | * @param array $paths |
||||
438 | * @param InputInterface $input |
||||
439 | * @param OutputInterface $output |
||||
440 | */ |
||||
441 | protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output, $paths = array()) |
||||
442 | { |
||||
443 | $output->writeln('Found ' . count($toExecute) . ' migrations in ' . count($paths) . ' directories'); |
||||
444 | $output->writeln('In the same directories, migrations previously executed: ' . $this->migrationsAlreadyDone[Migration::STATUS_DONE] . |
||||
445 | ', failed: ' . $this->migrationsAlreadyDone[Migration::STATUS_FAILED] . ', skipped: '. $this->migrationsAlreadyDone[Migration::STATUS_SKIPPED]); |
||||
446 | if ($this->migrationsAlreadyDone[Migration::STATUS_STARTED]) { |
||||
447 | $output->writeln('<info>In the same directories, migrations currently executing: ' . $this->migrationsAlreadyDone[Migration::STATUS_STARTED] . '</info>'); |
||||
448 | } |
||||
449 | } |
||||
450 | |||||
451 | /** |
||||
452 | * @param MigrationDefinition[] $toExecute |
||||
453 | * @return array key: folder name, value: number of migrations found |
||||
454 | */ |
||||
455 | protected function groupMigrationsByPath($toExecute) |
||||
456 | { |
||||
457 | $paths = array(); |
||||
458 | foreach ($toExecute as $name => $migrationDefinition) { |
||||
459 | $path = dirname($migrationDefinition->path); |
||||
460 | if (!isset($paths[$path])) { |
||||
461 | $paths[$path] = 1; |
||||
462 | } else { |
||||
463 | $paths[$path]++; |
||||
464 | } |
||||
465 | } |
||||
466 | |||||
467 | ksort($paths); |
||||
468 | |||||
469 | return $paths; |
||||
470 | } |
||||
471 | |||||
472 | /** |
||||
473 | * Returns the command-line arguments needed to execute a separate subprocess that will run a set of migrations |
||||
474 | * (except path, which should be added after this call) |
||||
475 | * @param InputInterface $input |
||||
476 | * @return array |
||||
477 | * @todo check if it is a good idea to pass on the current verbosity |
||||
478 | * @todo shall we pass to child processes the `survive-disconnected-tty` flag? |
||||
479 | */ |
||||
480 | protected function createChildProcessArgs(InputInterface $input) |
||||
481 | { |
||||
482 | $kernel = $this->getContainer()->get('kernel'); |
||||
483 | |||||
484 | // mandatory args and options |
||||
485 | $builderArgs = array( |
||||
486 | $this->getConsoleFile(), // sf console |
||||
487 | self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding? |
||||
488 | '--env=' . $kernel->getEnvironment(), // sf env |
||||
489 | '--child' |
||||
490 | ); |
||||
491 | // sf/ez env options |
||||
492 | if (!$kernel->isDebug()) { |
||||
493 | $builderArgs[] = '--no-debug'; |
||||
494 | } |
||||
495 | if ($input->getOption('siteaccess')) { |
||||
496 | $builderArgs[] = '--siteaccess=' . $input->getOption('siteaccess'); |
||||
497 | } |
||||
498 | switch ($this->verbosity) { |
||||
499 | // no propagation of 'quiet' mode, as we always need to have at least the child output with executed migs |
||||
500 | case OutputInterface::VERBOSITY_VERBOSE: |
||||
501 | $builderArgs[] = '-v'; |
||||
502 | break; |
||||
503 | case OutputInterface::VERBOSITY_VERY_VERBOSE: |
||||
504 | $builderArgs[] = '-vv'; |
||||
505 | break; |
||||
506 | case OutputInterface::VERBOSITY_DEBUG: |
||||
507 | $builderArgs[] = '-vvv'; |
||||
508 | break; |
||||
509 | } |
||||
510 | // 'optional' options |
||||
511 | // note: options 'clear-cache', 'no-interaction', 'path' and 'survive-disconnected-tty' we never propagate |
||||
512 | if ($input->getOption('admin-login')) { |
||||
513 | $builderArgs[] = '--admin-login=' . $input->getOption('admin-login'); |
||||
514 | } |
||||
515 | if ($input->getOption('default-language')) { |
||||
516 | $builderArgs[] = '--default-language=' . $input->getOption('default-language'); |
||||
517 | } |
||||
518 | if ($input->getOption('force')) { |
||||
519 | $builderArgs[] = '--force'; |
||||
520 | } |
||||
521 | // useful in case the subprocess has a migration step of type process/run |
||||
522 | if ($input->getOption('force-sigchild-enabled')) { |
||||
523 | $builderArgs[] = '--force-sigchild-enabled'; |
||||
524 | } |
||||
525 | if ($input->getOption('ignore-failures')) { |
||||
526 | $builderArgs[] = '--ignore-failures'; |
||||
527 | } |
||||
528 | if ($input->getOption('no-transactions')) { |
||||
529 | $builderArgs[] = '--no-transactions'; |
||||
530 | } |
||||
531 | if ($input->getOption('separate-process')) { |
||||
532 | $builderArgs[] = '--separate-process'; |
||||
533 | } |
||||
534 | if ($input->getOption('set-reference')) { |
||||
535 | foreach ($input->getOption('set-reference') as $refSpec) { |
||||
536 | $builderArgs[] = '--set-reference=' . $refSpec; |
||||
537 | } |
||||
538 | } |
||||
539 | if ($input->getOption('child-process-php-ini-config')) { |
||||
540 | foreach ($input->getOption('child-process-php-ini-config') as $iniSpec) { |
||||
541 | $builderArgs[] = '--child-process-php-ini-config=' . $iniSpec; |
||||
542 | } |
||||
543 | } |
||||
544 | return $builderArgs; |
||||
545 | } |
||||
546 | } |
||||
547 |