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
![]() |
|||||||
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
![]() |
|||||||
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. ![]() 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. ![]() |
|||||||
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
![]() |
|||||||
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