MassMigrateCommand   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 527
Duplicated Lines 0 %

Test Coverage

Coverage 3.93%

Importance

Changes 0
Metric Value
eloc 260
dl 0
loc 527
ccs 9
cts 229
cp 0.0393
rs 2
c 0
b 0
f 0
wmc 90

9 Methods

Rating   Name   Duplication   Size   Complexity  
C executeAsParent() 0 100 14
A execute() 0 32 5
A configure() 0 10 1
B onChildProcessOutput() 0 24 7
F executeAsChild() 0 134 24
A printMigrationsList() 0 7 2
C buildMigrationsList() 0 43 17
A groupMigrationsByPath() 0 15 3
F createChildProcessArgs() 0 65 17

How to fix   Complexity   

Complex Class

Complex classes like MassMigrateCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MassMigrateCommand, and based on these observations, apply Extract Interface, too.

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
It seems like $start can also be of type string; however, parameter $start of Kaliop\eZMigrationBundle...mand::executeAsParent() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
            return $this->executeAsParent($input, $output, $toExecute, /** @scrutinizer ignore-type */ $start);
Loading history...
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
Unused Code introduced by
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 ignore-unused  annotation

210
    protected function executeAsChild($input, /** @scrutinizer ignore-unused */ $output, $toExecute, $force, $migrationService)

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...
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
Bug introduced by
$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 ignore-type  annotation

230
                $builder->setPrefix(/** @scrutinizer ignore-type */ $prefix);
Loading history...
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
Bug introduced by
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 ignore-type  annotation

268
                $this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", /** @scrutinizer ignore-type */ self::$VERBOSITY_CHILD);
Loading history...
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
The variable $builder does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $builderArgs does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $migrationContext does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
Bug introduced by
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 ignore-type  annotation

374
                    $this->writeLn($msg, /** @scrutinizer ignore-type */ self::$VERBOSITY_CHILD, OutputInterface::OUTPUT_RAW);
Loading history...
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