1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Kaliop\eZMigrationBundle\Command; |
4
|
|
|
|
5
|
|
|
use Symfony\Component\Console\Input\ArrayInput; |
6
|
|
|
use Symfony\Component\Console\Input\InputOption; |
7
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
8
|
|
|
use Symfony\Component\Console\Output\Output; |
9
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
10
|
|
|
use Kaliop\eZMigrationBundle\API\Value\Migration; |
11
|
|
|
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; |
12
|
|
|
use Symfony\Component\Process\ProcessBuilder; |
13
|
|
|
use Symfony\Component\Process\PhpExecutableFinder; |
14
|
|
|
use Kaliop\eZMigrationBundle\Core\Helper\ProcessManager; |
15
|
|
|
use Symfony\Component\Console\Question\ConfirmationQuestion; |
16
|
|
|
|
17
|
|
|
class MassMigrateCommand extends MigrateCommand |
18
|
|
|
{ |
19
|
|
|
const COMMAND_NAME = 'kaliop:migration:mass_migrate'; |
20
|
|
|
|
21
|
|
|
protected $migrationsDone = array(0, 0, 0); |
22
|
|
|
protected $migrationsAlreadyDone = array(); |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @todo (!important) can we rename the option --separate-process ? |
26
|
|
|
*/ |
27
|
76 |
|
protected function configure() |
28
|
|
|
{ |
29
|
76 |
|
parent::configure(); |
30
|
|
|
|
31
|
|
|
$this |
32
|
76 |
|
->setName(self::COMMAND_NAME) |
33
|
76 |
|
->setAliases(array()) |
34
|
76 |
|
->setDescription('Executes available migration definitions, using parallelism.') |
35
|
76 |
|
->addOption('concurrency', 'r', InputOption::VALUE_REQUIRED, "The number of executors to run in parallel", 2) |
36
|
76 |
|
->setHelp(<<<EOT |
37
|
76 |
|
This command is designed to scan recursively a directory for migration files and execute them all in parallel. |
38
|
|
|
One child process will be spawned for each subdirectory found. |
39
|
|
|
The maximum number of processes to run in parallel is specified via the 'concurrency' option. |
40
|
|
|
<info>NB: this command does not guarantee that any given migration will be executed before another. Take care about dependencies.</info> |
41
|
|
|
<info>NB: the rule that each migration filename has to be unique still applies, even if migrations are spread across different directories.</info> |
42
|
|
|
Unlike for the 'normal' migration command, it is not recommended to use the <info>--separate-process</info> option, as it will make execution much slower |
43
|
|
|
EOT |
44
|
|
|
) |
45
|
|
|
; |
46
|
76 |
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Execute the command. |
50
|
|
|
* |
51
|
|
|
* @param InputInterface $input |
52
|
|
|
* @param OutputInterface $output |
53
|
|
|
* @return null|int null or 0 if everything went fine, or an error code |
54
|
|
|
*/ |
55
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
56
|
|
|
{ |
57
|
|
|
$start = microtime(true); |
58
|
|
|
|
59
|
|
|
$this->setOutput($output); |
60
|
|
|
$this->setVerbosity($output->getVerbosity()); |
61
|
|
|
|
62
|
|
|
$isChild = $input->getOption('child'); |
63
|
|
|
|
64
|
|
|
if ($isChild) { |
65
|
|
|
$this->setVerbosity(self::VERBOSITY_CHILD); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
$this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output); |
69
|
|
|
|
70
|
|
|
// q: is it worth declaring a new, dedicated migration service ? |
71
|
|
|
$migrationService = $this->getMigrationService(); |
72
|
|
|
$migrationService->setLoader($this->getContainer()->get('ez_migration_bundle.loader.filesystem_recursive')); |
73
|
|
|
|
74
|
|
|
$toExecute = $this->buildMigrationsList($input->getOption('path'), $migrationService, $isChild); |
75
|
|
|
|
76
|
|
|
if (!count($toExecute)) { |
77
|
|
|
$this->writeln('<info>No migrations to execute</info>'); |
78
|
|
|
return 0; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
if (!$isChild) { |
82
|
|
|
|
83
|
|
|
$paths = $this->groupMigrationsByPath($toExecute); |
84
|
|
|
$this->printMigrationsList($toExecute, $input, $output, $paths); |
85
|
|
|
|
86
|
|
|
// ask user for confirmation to make changes |
87
|
|
|
if (!$this->askForConfirmation($input, $output)) { |
88
|
|
|
return 0; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
$concurrency = $input->getOption('concurrency'); |
92
|
|
|
$this->writeln("Executing migrations using " . count($paths) . " processes with a concurrency of $concurrency"); |
93
|
|
|
|
94
|
|
|
$builder = new ProcessBuilder(); |
95
|
|
|
$executableFinder = new PhpExecutableFinder(); |
96
|
|
|
if (false !== ($php = $executableFinder->find())) { |
97
|
|
|
$builder->setPrefix($php); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// mandatory args and options |
101
|
|
|
$builderArgs = $this->createChildProcessArgs($input); |
102
|
|
|
|
103
|
|
|
$processes = array(); |
104
|
|
|
/** @var MigrationDefinition $migrationDefinition */ |
105
|
|
|
foreach($paths as $path => $count) { |
106
|
|
|
$this->writeln("<info>Queueing processing of: $path ($count migrations)</info>", OutputInterface::VERBOSITY_VERBOSE); |
107
|
|
|
|
108
|
|
|
$process = $builder |
109
|
|
|
->setArguments(array_merge($builderArgs, array('--path=' . $path))) |
110
|
|
|
->getProcess(); |
111
|
|
|
|
112
|
|
|
$this->writeln('<info>Command: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE); |
113
|
|
|
|
114
|
|
|
// allow long migrations processes by default |
115
|
|
|
$process->setTimeout(86400); |
116
|
|
|
$processes[] = $process; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
$this->writeln("Starting queued processes..."); |
120
|
|
|
|
121
|
|
|
$this->migrationsDone = array(0, 0, 0); |
122
|
|
|
|
123
|
|
|
$processManager = new ProcessManager(); |
124
|
|
|
$processManager->runParallel($processes, $concurrency, 500, array($this, 'onSubProcessOutput')); |
125
|
|
|
|
126
|
|
|
$failed = 0; |
127
|
|
|
foreach ($processes as $i => $process) { |
128
|
|
|
if (!$process->isSuccessful()) { |
129
|
|
|
$output->writeln("\n<error>Subprocess $i failed! Reason: " . $process->getErrorOutput() . "</error>\n"); |
130
|
|
|
$failed++; |
131
|
|
|
} |
132
|
|
|
} |
133
|
|
|
|
134
|
|
View Code Duplication |
if ($input->getOption('clear-cache')) { |
|
|
|
|
135
|
|
|
$command = $this->getApplication()->find('cache:clear'); |
136
|
|
|
$inputArray = new ArrayInput(array('command' => 'cache:clear')); |
137
|
|
|
$command->run($inputArray, $output); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$time = microtime(true) - $start; |
141
|
|
|
|
142
|
|
|
$this->writeln('<info>'.$this->migrationsDone[0].' migrations executed, '.$this->migrationsDone[1].' failed, '.$this->migrationsDone[2].' skipped</info>'); |
143
|
|
|
$this->writeln("<info>Import finished</info>\n"); |
144
|
|
|
|
145
|
|
|
// since we use subprocesses, we can not measure max memory used |
146
|
|
|
$this->writeln("Time taken: ".sprintf('%.2f', $time)." secs"); |
147
|
|
|
|
148
|
|
|
return $failed; |
149
|
|
|
|
150
|
|
|
} else { |
151
|
|
|
|
152
|
|
|
// @todo disable signal slots that are harmful during migrations, if any |
153
|
|
|
|
154
|
|
|
if ($input->getOption('separate-process')) { |
155
|
|
|
$builder = new ProcessBuilder(); |
156
|
|
|
$executableFinder = new PhpExecutableFinder(); |
157
|
|
|
if (false !== $php = $executableFinder->find()) { |
158
|
|
|
$builder->setPrefix($php); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
$builderArgs = parent::createChildProcessArgs($input); |
|
|
|
|
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
$failed = 0; |
165
|
|
|
$executed = 0; |
166
|
|
|
$skipped = 0; |
167
|
|
|
$total = count($toExecute); |
168
|
|
|
|
169
|
|
|
foreach ($toExecute as $name => $migrationDefinition) { |
170
|
|
|
// let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway |
171
|
|
|
if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) { |
172
|
|
|
$this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", self::VERBOSITY_CHILD); |
173
|
|
|
$skipped++; |
174
|
|
|
continue; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
if ($input->getOption('separate-process')) { |
178
|
|
|
|
179
|
|
|
$process = $builder |
|
|
|
|
180
|
|
|
->setArguments(array_merge($builderArgs, array('--path=' . $migrationDefinition->path))) |
|
|
|
|
181
|
|
|
->getProcess(); |
182
|
|
|
|
183
|
|
|
// allow long migrations processes by default |
184
|
|
|
$process->setTimeout(86400); |
185
|
|
|
// and give no feedback to the user |
186
|
|
|
$process->run( |
187
|
|
|
function($type, $buffer) { |
|
|
|
|
188
|
|
|
//echo $buffer; |
189
|
|
|
} |
190
|
|
|
); |
191
|
|
|
|
192
|
|
|
try { |
193
|
|
|
|
194
|
|
|
if (!$process->isSuccessful()) { |
195
|
|
|
throw new \Exception($process->getErrorOutput()); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
// There are cases where the separate process dies halfway but does not return a non-zero code. |
199
|
|
|
// That's why we should double-check here if the migration is still tagged as 'started'... |
200
|
|
|
/** @var Migration $migration */ |
201
|
|
|
$migration = $migrationService->getMigration($migrationDefinition->name); |
202
|
|
|
|
203
|
|
View Code Duplication |
if (!$migration) { |
|
|
|
|
204
|
|
|
// q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace... |
205
|
|
|
throw new \Exception("After the separate process charged to execute the migration finished, the migration can not be found in the database any more."); |
206
|
|
|
} else if ($migration->status == Migration::STATUS_STARTED) { |
207
|
|
|
$errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution."; |
208
|
|
|
$migrationService->endMigration(New Migration( |
209
|
|
|
$migration->name, |
210
|
|
|
$migration->md5, |
211
|
|
|
$migration->path, |
212
|
|
|
$migration->executionDate, |
213
|
|
|
Migration::STATUS_FAILED, |
214
|
|
|
($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg) |
215
|
|
|
)); |
216
|
|
|
throw new \Exception($errorMsg); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
$executed++; |
220
|
|
|
|
221
|
|
|
} catch (\Exception $e) { |
222
|
|
|
if ($input->getOption('ignore-failures')) { |
223
|
|
|
$output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n"); |
224
|
|
|
$failed++; |
225
|
|
|
continue; |
226
|
|
|
} |
227
|
|
|
$output->writeln("\n<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>"); |
228
|
|
|
|
229
|
|
|
$missed = $total - $executed - $failed - $skipped; |
230
|
|
|
$this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed"); |
231
|
|
|
|
232
|
|
|
return 1; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
} else { |
236
|
|
|
|
237
|
|
|
try { |
238
|
|
|
|
239
|
|
|
$migrationService->executeMigration( |
240
|
|
|
$migrationDefinition, |
241
|
|
|
!$input->getOption('no-transactions'), |
242
|
|
|
$input->getOption('default-language') |
243
|
|
|
); |
244
|
|
|
|
245
|
|
|
$executed++; |
246
|
|
|
} catch(\Exception $e) { |
247
|
|
|
$failed++; |
248
|
|
|
if ($input->getOption('ignore-failures')) { |
249
|
|
|
$this->writeln("<error>Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD); |
250
|
|
|
continue; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
$this->writeln("<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD); |
254
|
|
|
|
255
|
|
|
$missed = $total - $executed - $failed - $skipped; |
256
|
|
|
$this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed"); |
257
|
|
|
|
258
|
|
|
return 1; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
$this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped", self::VERBOSITY_CHILD); |
265
|
|
|
|
266
|
|
|
// We do not return an error code > 0 if migrations fail, but only on proper fatals. |
267
|
|
|
// The parent will analyze the output of the child process to gather the number of executed/failed migrations anyway |
268
|
|
|
//return $failed; |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
public function onSubProcessOutput($type, $buffer, $process=null) |
273
|
|
|
{ |
274
|
|
|
$lines = explode("\n", trim($buffer)); |
275
|
|
|
|
276
|
|
|
foreach ($lines as $line) { |
277
|
|
|
if (preg_match('/Migrations executed: ([0-9]+), failed: ([0-9]+), skipped: ([0-9]+)/', $line, $matches)) { |
278
|
|
|
$this->migrationsDone[0] += $matches[1]; |
279
|
|
|
$this->migrationsDone[1] += $matches[2]; |
280
|
|
|
$this->migrationsDone[2] += $matches[3]; |
281
|
|
|
|
282
|
|
|
// swallow these lines unless we are in verbose mode |
283
|
|
|
if ($this->verbosity <= Output::VERBOSITY_NORMAL) { |
284
|
|
|
return; |
285
|
|
|
} |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
// we tag the output from the different processes |
289
|
|
|
if (trim($line) !== '') { |
290
|
|
|
echo '[' . ($process ? $process->getPid() : ''). '] ' . trim($line) . "\n"; |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* @param string $paths |
297
|
|
|
* @param $migrationService |
298
|
|
|
* @param bool $isChild when not in child mode, do not waste time parsing migrations |
299
|
|
|
* @return MigrationDefinition[] parsed or unparsed, depending on |
300
|
|
|
* |
301
|
|
|
* @todo this does not scale well with many definitions or migrations |
302
|
|
|
*/ |
303
|
|
|
protected function buildMigrationsList($paths, $migrationService, $isChild = false) |
304
|
|
|
{ |
305
|
|
|
$migrationDefinitions = $migrationService->getMigrationsDefinitions($paths); |
306
|
|
|
$migrations = $migrationService->getMigrations(); |
307
|
|
|
|
308
|
|
|
$this->migrationsAlreadyDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0, Migration::STATUS_STARTED => 0); |
309
|
|
|
|
310
|
|
|
// filter away all migrations except 'to do' ones |
311
|
|
|
$toExecute = array(); |
312
|
|
|
foreach($migrationDefinitions as $name => $migrationDefinition) { |
313
|
|
View Code Duplication |
if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && $migration->status == Migration::STATUS_TODO)) { |
|
|
|
|
314
|
|
|
$toExecute[$name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition; |
315
|
|
|
} |
316
|
|
|
// save the list of non-executable migrations as well |
317
|
|
|
if (!$isChild && isset($migrations[$name]) && (($migration = $migrations[$name]) && $migration->status != Migration::STATUS_TODO)) { |
318
|
|
|
$this->migrationsAlreadyDone[$migration->status]++; |
319
|
|
|
} |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
// if user wants to execute 'all' migrations: look for some which are registered in the database even if not |
323
|
|
|
// found by the loader |
324
|
|
View Code Duplication |
if (empty($paths)) { |
|
|
|
|
325
|
|
|
foreach ($migrations as $migration) { |
326
|
|
|
if ($migration->status == Migration::STATUS_TODO && !isset($toExecute[$migration->name])) { |
327
|
|
|
$migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path)); |
328
|
|
|
if (count($migrationDefinitions)) { |
329
|
|
|
$migrationDefinition = reset($migrationDefinitions); |
330
|
|
|
$toExecute[$migration->name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition; |
331
|
|
|
} else { |
332
|
|
|
// q: shall we raise a warning here ? |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
} |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
ksort($toExecute); |
339
|
|
|
|
340
|
|
|
return $toExecute; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* We use a more compact output when there are *many* migrations |
345
|
|
|
* @param MigrationDefinition[] $toExecute |
346
|
|
|
* @param array $paths |
347
|
|
|
* @param InputInterface $input |
348
|
|
|
* @param OutputInterface $output |
349
|
|
|
*/ |
350
|
|
|
protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output, $paths = array()) |
351
|
|
|
{ |
352
|
|
|
$output->writeln('Found ' . count($toExecute) . ' migrations in ' . count($paths) . ' directories'); |
353
|
|
|
$output->writeln('In the same directories, migrations previously executed: ' . $this->migrationsAlreadyDone[Migration::STATUS_DONE] . |
354
|
|
|
', failed: ' . $this->migrationsAlreadyDone[Migration::STATUS_FAILED] . ', skipped: '. $this->migrationsAlreadyDone[Migration::STATUS_SKIPPED]); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
View Code Duplication |
protected function askForConfirmation(InputInterface $input, OutputInterface $output) |
|
|
|
|
358
|
|
|
{ |
359
|
|
|
if ($input->isInteractive() && !$input->getOption('no-interaction')) { |
360
|
|
|
$dialog = $this->getHelperSet()->get('question'); |
361
|
|
|
if (!$dialog->ask( |
362
|
|
|
$input, |
363
|
|
|
$output, |
364
|
|
|
new ConfirmationQuestion('<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>', false) |
365
|
|
|
) |
366
|
|
|
) { |
367
|
|
|
$output->writeln('<error>Migration execution cancelled!</error>'); |
368
|
|
|
return 0; |
369
|
|
|
} |
370
|
|
|
} else { |
371
|
|
|
// this line is not that nice in the automated scenarios used by the parallel migration |
372
|
|
|
//$this->writeln("============================================="); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
return 1; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* @param MigrationDefinition[] $toExecute |
380
|
|
|
* @return array key: folder name, value: number of migrations found |
381
|
|
|
*/ |
382
|
|
|
protected function groupMigrationsByPath($toExecute) |
383
|
|
|
{ |
384
|
|
|
$paths = array(); |
385
|
|
|
foreach($toExecute as $name => $migrationDefinition) { |
386
|
|
|
$path = dirname($migrationDefinition->path); |
387
|
|
|
if (!isset($paths[$path])) { |
388
|
|
|
$paths[$path] = 1; |
389
|
|
|
} else { |
390
|
|
|
$paths[$path]++; |
391
|
|
|
} |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
ksort($paths); |
395
|
|
|
|
396
|
|
|
return $paths; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Returns the command-line arguments needed to execute a migration in a separate subprocess (omitting 'path') |
401
|
|
|
* @param InputInterface $input |
402
|
|
|
* @return array |
403
|
|
|
*/ |
404
|
|
|
protected function createChildProcessArgs(InputInterface $input) |
405
|
|
|
{ |
406
|
|
|
$kernel = $this->getContainer()->get('kernel'); |
407
|
|
|
|
408
|
|
|
// mandatory args and options |
409
|
|
|
$builderArgs = array( |
410
|
|
|
$_SERVER['argv'][0], // sf console |
411
|
|
|
self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding? |
412
|
|
|
'--env=' . $kernel-> getEnvironment(), // sf env |
413
|
|
|
'--child' |
414
|
|
|
); |
415
|
|
|
// sf/ez env options |
416
|
|
|
if (!$kernel->isDebug()) { |
417
|
|
|
$builderArgs[] = '--no-debug'; |
418
|
|
|
} |
419
|
|
|
if ($input->getOption('siteaccess')) { |
420
|
|
|
$builderArgs[] = '--siteaccess=' . $input->getOption('siteaccess'); |
421
|
|
|
} |
422
|
|
|
// 'optional' options |
423
|
|
|
// note: options 'clear-cache', 'no-interaction', 'path' we never propagate |
424
|
|
|
if ($input->getOption('admin-login')) { |
425
|
|
|
$builderArgs[] = '--admin-login=' . $input->getOption('admin-login'); |
426
|
|
|
} |
427
|
|
|
if ($input->getOption('default-language')) { |
428
|
|
|
$builderArgs[] = '--default-language=' . $input->getOption('default-language'); |
429
|
|
|
} |
430
|
|
|
if ($input->getOption('force')) { |
431
|
|
|
$builderArgs[] = '--force'; |
432
|
|
|
} |
433
|
|
|
if ($input->getOption('ignore-failures')) { |
434
|
|
|
$builderArgs[] = '--ignore-failures'; |
435
|
|
|
} |
436
|
|
|
if ($input->getOption('no-transactions')) { |
437
|
|
|
$builderArgs[] = '--no-transactions'; |
438
|
|
|
} |
439
|
|
|
if ($input->getOption('separate-process')) { |
440
|
|
|
$builderArgs[] = '--separate-process'; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
} |
444
|
|
|
} |
445
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.