Completed
Push — master ( 2b894b...836484 )
by Gaetano
05:34
created

MassMigrateCommand::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 0
cts 19
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 10
nc 1
nop 0
crap 2
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
    protected function configure()
28
    {
29
        parent::configure();
30
31
        $this
32
            ->setName(self::COMMAND_NAME)
33
            ->setAliases(array())
34
            ->setDescription('Executes available migration definitions, using parallelism.')
35
            ->addOption('concurrency', 'r', InputOption::VALUE_REQUIRED, "The number of executors to run in parallel", 2)
36
            ->setHelp(<<<EOT
37
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
    }
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)
0 ignored issues
show
Coding Style introduced by
execute uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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
            $kernel = $this->getContainer()->get('kernel');
95
            $builder = new ProcessBuilder();
96
            $executableFinder = new PhpExecutableFinder();
97
            if (false !== ($php = $executableFinder->find())) {
98
                $builder->setPrefix($php);
99
            }
100
            // mandatory args and options
101
            $builderArgs = array(
102
                $_SERVER['argv'][0], // sf console
103
                self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
104
                '--env=' . $kernel-> getEnvironment(), // sf env
105
                '--child'
106
            );
107
            // 'optional' options
108
            // note: options 'clear-cache' we never propagate
109
            if (!$kernel->isDebug()) {
110
                $builderArgs[] = '--no-debug';
111
            }
112
            if ($input->getOption('default-language')) {
113
                $builderArgs[]='--default-language='.$input->getOption('default-language');
114
            }
115
            if ($input->getOption('no-transactions')) {
116
                $builderArgs[]='--no-transactions';
117
            }
118
            if ($input->getOption('siteaccess')) {
119
                $builderArgs[]='--siteaccess='.$input->getOption('siteaccess');
120
            }
121
            if ($input->getOption('ignore-failures')) {
122
                $builderArgs[]='--ignore-failures';
123
            }
124
            if ($input->getOption('separate-process')) {
125
                $builderArgs[]='--separate-process';
126
            }
127
            $processes = array();
128
            /** @var MigrationDefinition $migrationDefinition */
129
            foreach($paths as $path => $count) {
130
                $this->writeln("<info>Queueing processing of: $path ($count migrations)</info>", OutputInterface::VERBOSITY_VERBOSE);
131
132
                $process = $builder
133
                    ->setArguments(array_merge($builderArgs, array('--path=' . $path)))
134
                    ->getProcess();
135
136
                $this->writeln('<info>Command: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
137
138
                // allow long migrations processes by default
139
                $process->setTimeout(86400);
140
                $processes[] = $process;
141
            }
142
143
            $this->writeln("Starting queued processes...");
144
145
            $this->migrationsDone = array(0, 0, 0);
146
147
            $processManager = new ProcessManager();
148
            $processManager->runParallel($processes, $concurrency, 500, array($this, 'onSubProcessOutput'));
149
150
            $failed = 0;
151
            foreach ($processes as $i => $process) {
152
                if (!$process->isSuccessful()) {
153
                    $output->writeln("\n<error>Subprocess $i failed! Reason: " . $process->getErrorOutput() . "</error>\n");
154
                    $failed++;
155
                }
156
            }
157
158 View Code Duplication
            if ($input->getOption('clear-cache')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
159
                $command = $this->getApplication()->find('cache:clear');
160
                $inputArray = new ArrayInput(array('command' => 'cache:clear'));
161
                $command->run($inputArray, $output);
162
            }
163
164
            $time = microtime(true) - $start;
165
166
            $this->writeln('<info>'.$this->migrationsDone[0].' migrations executed, '.$this->migrationsDone[1].' failed, '.$this->migrationsDone[2].' skipped</info>');
167
            $this->writeln("<info>Import finished</info>\n");
168
169
            // since we use subprocesses, we can not measure max memory used
170
            $this->writeln("Time taken: ".sprintf('%.2f', $time)." secs");
171
172
            return $failed;
173
174
        } else {
175
176
            // @todo disable signal slots that are harmful during migrations, if any
177
178
            if ($input->getOption('separate-process')) {
179
                $builder = new ProcessBuilder();
180
                $executableFinder = new PhpExecutableFinder();
181
                if (false !== $php = $executableFinder->find()) {
182
                    $builder->setPrefix($php);
183
                }
184
                // mandatory args and options
185
                $builderArgs = array(
186
                    $_SERVER['argv'][0], // sf console
187
                    MigrateCommand::COMMAND_NAME, // name of sf command
188
                    '--env=' . $this->getContainer()->get('kernel')->getEnvironment(), // sf env
189
                    '--child'
190
                );
191
                // 'optional' options
192
                // note: options 'clear-cache', 'ignore-failures' and 'no-transactions' we never propagate
193
                if ($input->getOption('default-language')) {
194
                    $builderArgs[] = '--default-language=' . $input->getOption('default-language');
195
                }
196
                if ($input->getOption('no-transactions')) {
197
                    $builderArgs[] = '--no-transactions';
198
                }
199
            }
200
201
            $failed = 0;
202
            $executed = 0;
203
            $skipped = 0;
204
            $total = count($toExecute);
205
206
            foreach ($toExecute as  $name => $migrationDefinition) {
207
                // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
208
                if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
209
                    $this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", self::VERBOSITY_CHILD);
210
                    $skipped++;
211
                    continue;
212
                }
213
214
                if ($input->getOption('separate-process')) {
215
216
                    $process = $builder
0 ignored issues
show
Bug introduced by
The variable $builder does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
217
                        ->setArguments(array_merge($builderArgs, array('--path=' . $migrationDefinition->path)))
0 ignored issues
show
Bug introduced by
The variable $builderArgs does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
218
                        ->getProcess();
219
220
                    // allow long migrations processes by default
221
                    $process->setTimeout(86400);
222
                    // and give no feedback to the user
223
                    $process->run(
224
                        function($type, $buffer) {
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $buffer is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
225
                            //echo $buffer;
226
                        }
227
                    );
228
229
                    try {
230
231
                        if (!$process->isSuccessful()) {
232
                            throw new \Exception($process->getErrorOutput());
233
                        }
234
235
                        // There are cases where the separate process dies halfway but does not return a non-zero code.
236
                        // That's why we should double-check here if the migration is still tagged as 'started'...
237
                        /** @var Migration $migration */
238
                        $migration = $migrationService->getMigration($migrationDefinition->name);
239
240 View Code Duplication
                        if (!$migration) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
241
                            // q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace...
242
                            throw new \Exception("After the separate process charged to execute the migration finished, the migration can not be found in the database any more.");
243
                        } else if ($migration->status == Migration::STATUS_STARTED) {
244
                            $errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution.";
245
                            $migrationService->endMigration(New Migration(
246
                                $migration->name,
247
                                $migration->md5,
248
                                $migration->path,
249
                                $migration->executionDate,
250
                                Migration::STATUS_FAILED,
251
                                ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg)
252
                            ));
253
                            throw new \Exception($errorMsg);
254
                        }
255
256
                        $executed++;
257
258
                    } catch (\Exception $e) {
259
                        if ($input->getOption('ignore-failures')) {
260
                            $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
261
                            $failed++;
262
                            continue;
263
                        }
264
                        $output->writeln("\n<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>");
265
266
                        $missed = $total - $executed - $failed - $skipped;
267
                        $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed");
268
269
                        return 1;
270
                    }
271
272
                } else {
273
274
                    try {
275
276
                        $migrationService->executeMigration(
277
                            $migrationDefinition,
278
                            !$input->getOption('no-transactions'),
279
                            $input->getOption('default-language')
280
                        );
281
282
                        $executed++;
283
                    } catch(\Exception $e) {
284
                        $failed++;
285
                        if ($input->getOption('ignore-failures')) {
286
                            $this->writeln("<error>Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD);
287
                            continue;
288
                        }
289
290
                        $this->writeln("<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD);
291
292
                        $missed = $total - $executed - $failed - $skipped;
293
                        $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed");
294
295
                        return 1;
296
                    }
297
298
                }
299
            }
300
301
            $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped", self::VERBOSITY_CHILD);
302
303
            // We do not return an error code > 0 if migrations fail, but only on proper fatals.
304
            // The parent will analyze the output of the child process to gather the number of executed/failed migrations anyway
305
            //return $failed;
306
        }
307
    }
308
309
    public function onSubProcessOutput($type, $buffer, $process=null)
310
    {
311
        $lines = explode("\n", trim($buffer));
312
313
        foreach ($lines as $line) {
314
            if (preg_match('/Migrations executed: ([0-9]+), failed: ([0-9]+), skipped: ([0-9]+)/', $line, $matches)) {
315
                $this->migrationsDone[0] += $matches[1];
316
                $this->migrationsDone[1] += $matches[2];
317
                $this->migrationsDone[2] += $matches[3];
318
319
                // swallow these lines unless we are in verbose mode
320
                if ($this->verbosity <= Output::VERBOSITY_NORMAL) {
321
                    return;
322
                }
323
            }
324
325
            // we tag the output from the different processes
326
            if (trim($line) !== '') {
327
                echo '[' . ($process ? $process->getPid() : ''). '] ' . trim($line) . "\n";
328
            }
329
        }
330
    }
331
332
    /**
333
     * @param string $paths
334
     * @param $migrationService
335
     * @param bool $isChild when not in child mode, do not waste time parsing migrations
336
     * @return MigrationDefinition[] parsed or unparsed, depending on
337
     *
338
     * @todo this does not scale well with many definitions or migrations
339
     */
340
    protected function buildMigrationsList($paths, $migrationService, $isChild = false)
341
    {
342
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
343
        $migrations = $migrationService->getMigrations();
344
345
        $this->migrationsAlreadyDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0, Migration::STATUS_STARTED => 0);
346
347
        // filter away all migrations except 'to do' ones
348
        $toExecute = array();
349
        foreach($migrationDefinitions as $name => $migrationDefinition) {
350 View Code Duplication
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && $migration->status == Migration::STATUS_TODO)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
351
                $toExecute[$name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
352
            }
353
            // save the list of non-executable migrations as well
354
            if (!$isChild && isset($migrations[$name]) && (($migration = $migrations[$name]) && $migration->status != Migration::STATUS_TODO)) {
355
                $this->migrationsAlreadyDone[$migration->status]++;
356
            }
357
        }
358
359
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
360
        // found by the loader
361 View Code Duplication
        if (empty($paths)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
362
            foreach ($migrations as $migration) {
363
                if ($migration->status == Migration::STATUS_TODO && !isset($toExecute[$migration->name])) {
364
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
365
                    if (count($migrationDefinitions)) {
366
                        $migrationDefinition = reset($migrationDefinitions);
367
                        $toExecute[$migration->name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
368
                    } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
369
                        // q: shall we raise a warning here ?
370
                    }
371
                }
372
            }
373
        }
374
375
        ksort($toExecute);
376
377
        return $toExecute;
378
    }
379
380
    /**
381
     * We use a more compact output when there are *many* migrations
382
     * @param MigrationDefinition[] $toExecute
383
     * @param array $paths
384
     * @param InputInterface $input
385
     * @param OutputInterface $output
386
     */
387
    protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output, $paths = array())
388
    {
389
        $output->writeln('Found ' . count($toExecute) . ' migrations in ' . count($paths) . ' directories');
390
        $output->writeln('In the same directories, migrations previously executed: ' . $this->migrationsAlreadyDone[Migration::STATUS_DONE] .
391
            ', failed: ' . $this->migrationsAlreadyDone[Migration::STATUS_FAILED] . ', skipped: '. $this->migrationsAlreadyDone[Migration::STATUS_SKIPPED]);
392
    }
393
394 View Code Duplication
    protected function askForConfirmation(InputInterface $input, OutputInterface $output)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
395
    {
396
        if ($input->isInteractive() && !$input->getOption('no-interaction')) {
397
            $dialog = $this->getHelperSet()->get('question');
398
            if (!$dialog->ask(
399
                $input,
400
                $output,
401
                new ConfirmationQuestion('<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>', false)
402
            )
403
            ) {
404
                $output->writeln('<error>Migration execution cancelled!</error>');
405
                return 0;
406
            }
407
        } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
408
            // this line is not that nice in the automated scenarios used by the parallel migration
409
            //$this->writeln("=============================================");
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
410
        }
411
412
        return 1;
413
    }
414
415
    /**
416
     * @param MigrationDefinition[] $toExecute
417
     * @return array key: folder name, value: number of migrations found
418
     */
419
    protected function groupMigrationsByPath($toExecute)
420
    {
421
        $paths = array();
422
        foreach($toExecute as $name => $migrationDefinition) {
423
            $path = dirname($migrationDefinition->path);
424
            if (!isset($paths[$path])) {
425
                $paths[$path] = 1;
426
            } else {
427
                $paths[$path]++;
428
            }
429
        }
430
431
        ksort($paths);
432
433
        return $paths;
434
    }
435
436
}
437