Passed
Pull Request — master (#1939)
by
unknown
02:54
created

Manager::migrate()   C

Complexity

Conditions 14
Paths 50

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 32.0516

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 49
c 0
b 0
f 0
ccs 17
cts 31
cp 0.5484
rs 6.2666
cc 14
nc 50
nop 3
crap 32.0516

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Migration;
9
10
use DateTime;
11
use InvalidArgumentException;
12
use Phinx\Config\Config;
13
use Phinx\Config\ConfigInterface;
14
use Phinx\Config\NamespaceAwareInterface;
15
use Phinx\Console\Command\AbstractCommand;
16
use Phinx\Migration\Manager\Environment;
17
use Phinx\Seed\AbstractSeed;
18
use Phinx\Seed\SeedInterface;
19
use Phinx\Util\Util;
20
use Psr\Container\ContainerInterface;
21
use RuntimeException;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Output\OutputInterface;
24
25
class Manager
26
{
27
    public const BREAKPOINT_TOGGLE = 1;
28
    public const BREAKPOINT_SET = 2;
29
    public const BREAKPOINT_UNSET = 3;
30
31
    /**
32
     * @var \Phinx\Config\ConfigInterface
33
     */
34
    protected $config;
35
36
    /**
37
     * @var \Symfony\Component\Console\Input\InputInterface
38
     */
39
    protected $input;
40
41
    /**
42
     * @var \Symfony\Component\Console\Output\OutputInterface
43
     */
44
    protected $output;
45
46
    /**
47
     * @var \Phinx\Migration\Manager\Environment[]
48
     */
49
    protected $environments = [];
50
51
    /**
52
     * @var \Phinx\Migration\AbstractMigration[]|null
53
     */
54
    protected $migrations;
55
56
    /**
57
     * @var \Phinx\Seed\AbstractSeed[]|null
58
     */
59
    protected $seeds;
60
61
    /**
62
     * @var \Psr\Container\ContainerInterface
63
     */
64
    protected $container;
65
66
    /**
67
     * @param \Phinx\Config\ConfigInterface $config Configuration Object
68
     * @param \Symfony\Component\Console\Input\InputInterface $input Console Input
69
     * @param \Symfony\Component\Console\Output\OutputInterface $output Console Output
70
     */
71
    public function __construct(ConfigInterface $config, InputInterface $input, OutputInterface $output)
72
    {
73
        $this->setConfig($config);
74
        $this->setInput($input);
75
        $this->setOutput($output);
76
    }
77
78
    /**
79
     * Prints the specified environment's migration status.
80
     *
81
     * @param string $environment environment to print status of
82
     * @param string|null $format format to print status in (either text, json, or null)
83
     *
84
     * @throws \RuntimeException
85
     *
86
     * @return array array indicating if there are any missing or down migrations
87
     */
88
    public function printStatus($environment, $format = null)
89 432
    {
90
        $output = $this->getOutput();
91 432
        $hasDownMigration = false;
92 432
        $hasMissingMigration = false;
93 432
        $migrations = $this->getMigrations($environment);
94 432
        $migrationCount = 0;
95
        $missingCount = 0;
96
        $pendingMigrationCount = 0;
97
        $finalMigrations = [];
98
        $verbosity = $output->getVerbosity();
99
        if ($format === 'json') {
100
            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
101
        }
102
        if (count($migrations)) {
103 22
            // rewrite using Symfony Table Helper as we already have this library
104
            // included and it will fix formatting issues (e.g drawing the lines)
105 22
            $output->writeln('');
106 22
107 22
            switch ($this->getConfig()->getVersionOrder()) {
108 22
                case Config::VERSION_ORDER_CREATION_TIME:
109 22
                    $migrationIdAndStartedHeader = "<info>[Migration ID]</info>  Started            ";
110 22
                    break;
111
                case Config::VERSION_ORDER_EXECUTION_TIME:
112
                    $migrationIdAndStartedHeader = "Migration ID    <info>[Started          ]</info>";
113 21
                    break;
114
                default:
115 21
                    throw new RuntimeException('Invalid version_order configuration option');
116 21
            }
117 19
118 19
            $output->writeln(" Status  $migrationIdAndStartedHeader  Finished             Migration Name ");
119 2
            $output->writeln('----------------------------------------------------------------------------------');
120 1
121 1
            $env = $this->getEnvironment($environment);
122 1
            $versions = $env->getVersionLog();
123 1
124 21
            $maxNameLength = $versions ? max(array_map(function ($version) {
125
                return strlen($version['migration_name']);
126 20
            }, $versions)) : 0;
127 20
128
            $missingVersions = array_diff_key($versions, $migrations);
129 20
            $missingCount = count($missingVersions);
130 20
131
            $hasMissingMigration = !empty($missingVersions);
132
133 17
            // get the migrations sorted in the same way as the versions
134 20
            /** @var \Phinx\Migration\AbstractMigration[] $sortedMigrations */
135
            $sortedMigrations = [];
136 20
137
            foreach ($versions as $versionCreationTime => $version) {
138 20
                if (isset($migrations[$versionCreationTime])) {
139
                    array_push($sortedMigrations, $migrations[$versionCreationTime]);
140
                    unset($migrations[$versionCreationTime]);
141 20
                }
142
            }
143 20
144 17
            if (empty($sortedMigrations) && !empty($missingVersions)) {
145 13
                // this means we have no up migrations, so we write all the missing versions already so they show up
146 13
                // before any possible down migration
147 13
                foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
148 20
                    $this->printMissingVersion($missingVersion, $maxNameLength);
149
150 20
                    unset($missingVersions[$missingVersionCreationTime]);
151
                }
152
            }
153 4
154 4
            // any migration left in the migrations (ie. not unset when sorting the migrations by the version order) is
155
            // a migration that is down, so we add them to the end of the sorted migrations list
156 4
            if (!empty($migrations)) {
157 4
                $sortedMigrations = array_merge($sortedMigrations, $migrations);
158 4
            }
159
160
            $migrationCount = count($sortedMigrations);
161
            foreach ($sortedMigrations as $migration) {
162 20
                $version = array_key_exists($migration->getVersion(), $versions) ? $versions[$migration->getVersion()] : false;
163 13
                if ($version) {
164 13
                    // check if there are missing versions before this version
165
                    foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
166 20
                        if ($this->getConfig()->isVersionOrderCreationTime()) {
167 20
                            if ($missingVersion['version'] > $version['version']) {
168 20
                                break;
169
                            }
170 13
                        } else {
171 6
                            if ($missingVersion['start_time'] > $version['start_time']) {
172 6
                                break;
173 4
                            } elseif (
174
                                $missingVersion['start_time'] == $version['start_time'] &&
175 3
                                $missingVersion['version'] > $version['version']
176
                            ) {
177
                                break;
178
                            }
179
                        }
180
181
                        $this->printMissingVersion($missingVersion, $maxNameLength);
182
183
                        unset($missingVersions[$missingVersionCreationTime]);
184 3
                    }
185
186 3
                    $status = '     <info>up</info> ';
187 13
                } else {
188
                    $pendingMigrationCount++;
189 13
                    $hasDownMigration = true;
190 13
                    $status = '   <error>down</error> ';
191 13
                }
192 13
                $maxNameLength = max($maxNameLength, strlen($migration->getName()));
193
194 20
                $output->writeln(sprintf(
195
                    '%s %14.0f  %19s  %19s  <comment>%s</comment>',
196 20
                    $status,
197 20
                    $migration->getVersion(),
198 20
                    ($version ? $version['start_time'] : ''),
199 20
                    ($version ? $version['end_time'] : ''),
200 20
                    $migration->getName()
201 20
                ));
202 20
203 20
                if ($version && $version['breakpoint']) {
204
                    $output->writeln('         <error>BREAKPOINT SET</error>');
205 20
                }
206 1
207 1
                $finalMigrations[] = ['migration_status' => trim(strip_tags($status)), 'migration_id' => sprintf('%14.0f', $migration->getVersion()), 'migration_name' => $migration->getName()];
208
                unset($versions[$migration->getVersion()]);
209 20
            }
210 20
211 20
            // and finally add any possibly-remaining missing migrations
212
            foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
213
                $this->printMissingVersion($missingVersion, $maxNameLength);
214 20
215 4
                unset($missingVersions[$missingVersionCreationTime]);
216
            }
217 4
        } else {
218 20
            // there are no migrations
219 20
            $output->writeln('');
220
            $output->writeln('There are no available migrations. Try creating one using the <info>create</info> command.');
221 1
        }
222 1
223
        // write an empty line
224
        $output->writeln('');
225
226 21
        if ($format !== null) {
227 21
            switch ($format) {
228
                case AbstractCommand::FORMAT_JSON:
229
                    $output->setVerbosity($verbosity);
230
                    $output->writeln(json_encode(
231
                        [
232
                            'pending_count' => $pendingMigrationCount,
233
                            'missing_count' => $missingCount,
234
                            'total_count' => $migrationCount + $missingCount,
235
                            'migrations' => $finalMigrations,
236
                        ]
237
                    ));
238
                    break;
239
                default:
240
                    $output->writeln('<info>Unsupported format: ' . $format . '</info>');
241
            }
242 21
        }
243 10
244 11
        return [
245 6
            'hasMissingMigration' => $hasMissingMigration,
246
            'hasDownMigration' => $hasDownMigration,
247 5
        ];
248
    }
249
250
    /**
251
     * Print Missing Version
252
     *
253
     * @param array $version The missing version to print (in the format returned by Environment.getVersionLog).
254
     * @param int $maxNameLength The maximum migration name length.
255
     *
256
     * @return void
257 10
     */
258
    protected function printMissingVersion($version, $maxNameLength)
259 10
    {
260 10
        $this->getOutput()->writeln(sprintf(
261 10
            '     <error>up</error>  %14.0f  %19s  %19s  <comment>%s</comment>  <error>** MISSING MIGRATION FILE **</error>',
262 10
            $version['version'],
263 10
            $version['start_time'],
264 10
            $version['end_time'],
265 10
            str_pad($version['migration_name'], $maxNameLength, ' ')
266
        ));
267 10
268 1
        if ($version && $version['breakpoint']) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $version of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
269 1
            $this->getOutput()->writeln('         <error>BREAKPOINT SET</error>');
270 10
        }
271
    }
272
273
    /**
274
     * Migrate to the version of the database on a given date.
275
     *
276
     * @param string $environment Environment
277
     * @param \DateTime $dateTime Date to migrate to
278
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the
279
     *                               migration
280 4
     *
281
     * @return void
282 4
     */
283 4
    public function migrateToDateTime($environment, DateTime $dateTime, $fake = false)
284
    {
285
        $versions = array_keys($this->getMigrations($environment));
286 4
        $dateString = $dateTime->format('YmdHis');
287 4
288
        $outstandingMigrations = array_filter($versions, function ($version) use ($dateString) {
289 4
            return $version <= $dateString;
290 3
        });
291 3
292 3
        if (count($outstandingMigrations) > 0) {
293 3
            $migration = max($outstandingMigrations);
294 4
            $this->getOutput()->writeln('Migrating to version ' . $migration);
295
            $this->migrate($environment, $migration, $fake);
296
        }
297
    }
298
299
    /**
300
     * Migrate an environment to the specified version.
301
     *
302
     * @param string $environment Environment
303 8
     * @param int|null $version version to migrate to
304
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration
305 8
     *
306 8
     * @return void
307 8
     */
308 8
    public function migrate($environment, $version = null, $fake = false)
309
    {
310 8
        $migrations = $this->getMigrations($environment);
311
        $env = $this->getEnvironment($environment);
312
        $versions = $env->getVersions();
313
        $current = $env->getCurrentVersion();
314 8
315 5
        if (empty($versions) && empty($migrations)) {
316 5
            return;
317 3
        }
318
319
        if ($version === null) {
320
            $version = max(array_merge($versions, array_keys($migrations)));
321
        } else {
322
            if ($version != 0 && !isset($migrations[$version])) {
323
                $this->output->writeln(sprintf(
324
                    '<comment>warning</comment> %s is not a valid version',
325
                    $version
326
                ));
327 8
328
                return;
329 8
            }
330
        }
331
332
        // are we migrating up or down?
333
        $direction = $version > $current ? MigrationInterface::UP : MigrationInterface::DOWN;
334
335
        if ($direction === MigrationInterface::DOWN) {
336
            // run downs first
337
            krsort($migrations);
338
            foreach ($migrations as $migration) {
339
                if ($migration->getVersion() <= $version) {
340
                    break;
341
                }
342
343 8
                if (in_array($migration->getVersion(), $versions)) {
344 8
                    $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake);
345 8
                }
346 2
            }
347
        }
348
349 8
        ksort($migrations);
350 5
        foreach ($migrations as $migration) {
351 5
            if ($migration->getVersion() > $version) {
352 8
                break;
353 8
            }
354
355
            if (!in_array($migration->getVersion(), $versions)) {
356
                $this->executeMigration($environment, $migration, MigrationInterface::UP, $fake);
357
            }
358
        }
359
    }
360
361
    /**
362
     * Execute a migration against the specified environment.
363 119
     *
364
     * @param string $name Environment Name
365 119
     * @param \Phinx\Migration\MigrationInterface $migration Migration
366 119
     * @param string $direction Direction
367
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration
368 119
     *
369 119
     * @return void
370 119
     */
371
    public function executeMigration($name, MigrationInterface $migration, $direction = MigrationInterface::UP, $fake = false)
372
    {
373 119
        $this->getOutput()->writeln('');
374 119
375 119
        // Skip the migration if it should not be executed
376
        if (!$migration->shouldExecute()) {
377 119
            $this->printMigrationStatus($migration, 'skipped');
378
379 119
            return;
380 119
        }
381 119
382 119
        $this->printMigrationStatus($migration, ($direction === MigrationInterface::UP ? 'migrating' : 'reverting'));
383 119
384
        // Execute the migration and log the time elapsed.
385
        $start = microtime(true);
386
        $this->getEnvironment($name)->executeMigration($migration, $direction, $fake);
387
        $end = microtime(true);
388
389
        $this->printMigrationStatus(
390
            $migration,
391
            ($direction === MigrationInterface::UP ? 'migrated' : 'reverted'),
392 6
            sprintf('%.4fs', $end - $start)
393
        );
394 6
    }
395 6
396
    /**
397 6
     * Execute a seeder against the specified environment.
398 6
     *
399 6
     * @param string $name Environment Name
400
     * @param \Phinx\Seed\SeedInterface $seed Seed
401
     *
402 6
     * @return void
403 6
     */
404 6
    public function executeSeed($name, SeedInterface $seed)
405
    {
406 6
        $this->getOutput()->writeln('');
407
408 6
        // Skip the seed if it should not be executed
409 6
        if (!$seed->shouldExecute()) {
410 6
            $this->printSeedStatus($seed, 'skipped');
411 6
412 6
            return;
413
        }
414
415
        $this->printSeedStatus($seed, 'seeding');
416
417
        // Execute the seeder and log the time elapsed.
418
        $start = microtime(true);
419
        $this->getEnvironment($name)->executeSeed($seed);
420
        $end = microtime(true);
421
422
        $this->printSeedStatus(
423 349
            $seed,
424
            'seeded',
425
            sprintf('%.4fs', $end - $start)
426 349
        );
427
    }
428
429 349
    /**
430
     * Print Migration Status
431
     *
432 349
     * @param \Phinx\Migration\MigrationInterface $migration Migration
433
     * @param string $status Status of the migration
434 349
     * @param string|null $duration Duration the migration took the be executed
435
     *
436
     * @return void
437 349
     */
438 48
    protected function printMigrationStatus(MigrationInterface $migration, $status, $duration = null)
439 48
    {
440 48
        $this->printStatusOutput(
441
            $migration->getVersion() . ' ' . $migration->getName(),
442 349
            $status,
443 349
            $duration
444 349
        );
445
    }
446
447 47
    /**
448
     * Print Seed Status
449 349
     *
450
     * @param \Phinx\Seed\SeedInterface $seed Seed
451 349
     * @param string $status Status of the seed
452 23
     * @param string|null $duration Duration the seed took the be executed
453 349
     *
454
     * @return void
455 20
     */
456 20
    protected function printSeedStatus(SeedInterface $seed, $status, $duration = null)
457 20
    {
458 20
        $this->printStatusOutput(
459
            $seed->getName(),
460
            $status,
461 20
            $duration
462 20
        );
463 20
    }
464
465
    /**
466
     * Print Status in Output
467 20
     *
468
     * @param string $name Name of the migration or seed
469
     * @param string $status Status of the migration or seed
470 349
     * @param string|null $duration Duration the migration or seed took the be executed
471 349
     *
472 53
     * @return void
473 53
     */
474
    protected function printStatusOutput($name, $status, $duration = null)
475
    {
476
        $this->getOutput()->writeln(
477 296
            ' ==' .
478
            ' <info>' . $name . ':</info>' .
479 94
            ' <comment>' . $status . ' ' . $duration . '</comment>'
480 94
        );
481 94
    }
482
483
    /**
484 296
     * Rollback an environment to the specified version.
485 46
     *
486 46
     * @param string $environment Environment
487
     * @param int|string|null $target Target
488
     * @param bool $force Force
489
     * @param bool $targetMustMatchVersion Target must match version
490 250
     * @param bool $fake Flag that if true, we just record running the migration, but not actually do the migration
491
     *
492 250
     * @return void
493 250
     */
494 70
    public function rollback($environment, $target = null, $force = false, $targetMustMatchVersion = true, $fake = false)
495
    {
496
        // note that the migrations are indexed by name (aka creation time) in ascending order
497 250
        $migrations = $this->getMigrations($environment);
498 250
499
        // note that the version log are also indexed by name with the proper ascending order according to the version order
500 250
        $executedVersions = $this->getEnvironment($environment)->getVersionLog();
501 96
502 96
        // get a list of migrations sorted in the opposite way of the executed versions
503 42
        $sortedMigrations = [];
504
505 68
        foreach ($executedVersions as $versionCreationTime => &$executedVersion) {
506
            // if we have a date (ie. the target must not match a version) and we are sorting by execution time, we
507 222
            // convert the version start time so we can compare directly with the target date
508 121
            if (!$this->getConfig()->isVersionOrderCreationTime() && !$targetMustMatchVersion) {
509 121
                $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $executedVersion['start_time']);
510
                $executedVersion['start_time'] = $dateTime->format('YmdHis');
511 117
            }
512 117
513 117
            if (isset($migrations[$versionCreationTime])) {
514 250
                array_unshift($sortedMigrations, $migrations[$versionCreationTime]);
515
            } else {
516 250
                // this means the version is missing so we unset it so that we don't consider it when rolling back
517 133
                // migrations (or choosing the last up version as target)
518 133
                unset($executedVersions[$versionCreationTime]);
519 250
            }
520
        }
521
522
        if ($target === 'all' || $target === '0') {
523
            $target = 0;
524
        } elseif (!is_numeric($target) && $target !== null) { // try to find a target version based on name
525
            // search through the migrations using the name
526
            $migrationNames = array_map(function ($item) {
527
                return $item['migration_name'];
528 9
            }, $executedVersions);
529
            $found = array_search($target, $migrationNames, true);
530 9
531
            // check on was found
532 9
            if ($found !== false) {
533
                $target = (string)$found;
534 3
            } else {
535 3
                $this->getOutput()->writeln("<error>No migration found with name ($target)</error>");
536 3
537 3
                return;
538 3
            }
539 3
        }
540
541 6
        // Check we have at least 1 migration to revert
542 3
        $executedVersionCreationTimes = array_keys($executedVersions);
543 3
        if (empty($executedVersionCreationTimes) || $target == end($executedVersionCreationTimes)) {
544 3
            $this->getOutput()->writeln('<error>No migrations to rollback</error>');
545
546
            return;
547 6
        }
548
549
        // If no target was supplied, revert the last migration
550
        if ($target === null) {
551
            // Get the migration before the last run migration
552
            $prev = count($executedVersionCreationTimes) - 2;
553
            $target = $prev >= 0 ? $executedVersionCreationTimes[$prev] : 0;
554
        }
555 381
556
        // If the target must match a version, check the target version exists
557 381
        if ($targetMustMatchVersion && $target !== 0 && !isset($migrations[$target])) {
558 381
            $this->getOutput()->writeln("<error>Target version ($target) not found</error>");
559
560
            return;
561
        }
562
563
        // Rollback all versions until we find the wanted rollback target
564
        $rollbacked = false;
565
566
        foreach ($sortedMigrations as $migration) {
567
            if ($targetMustMatchVersion && $migration->getVersion() == $target) {
568 382
                break;
569
            }
570 382
571 380
            if (in_array($migration->getVersion(), $executedVersionCreationTimes)) {
572
                $executedVersion = $executedVersions[$migration->getVersion()];
573
574
                if (!$targetMustMatchVersion) {
575 7
                    if (
576 1
                        ($this->getConfig()->isVersionOrderCreationTime() && $executedVersion['version'] <= $target) ||
577 1
                        (!$this->getConfig()->isVersionOrderCreationTime() && $executedVersion['start_time'] <= $target)
578
                    ) {
579 1
                        break;
580
                    }
581
                }
582
583 6
                if ($executedVersion['breakpoint'] != 0 && !$force) {
584 6
                    $this->getOutput()->writeln('<error>Breakpoint reached. Further rollbacks inhibited.</error>');
585
                    break;
586 6
                }
587 6
                $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake);
588 6
                $rollbacked = true;
589 6
            }
590
        }
591 6
592
        if (!$rollbacked) {
0 ignored issues
show
introduced by
The condition $rollbacked is always false.
Loading history...
593
            $this->getOutput()->writeln('<error>No migrations to rollback</error>');
594
        }
595
    }
596
597
    /**
598
     * Run database seeders against an environment.
599
     *
600 400
     * @param string $environment Environment
601
     * @param string|null $seed Seeder
602 400
     *
603 400
     * @throws \InvalidArgumentException
604
     *
605
     * @return void
606
     */
607
    public function seed($environment, $seed = null)
608
    {
609
        $seeds = $this->getSeeds();
610
611 393
        if ($seed === null) {
612
            // run all seeders
613 393
            foreach ($seeds as $seeder) {
614
                if (array_key_exists($seeder->getName(), $seeds)) {
615
                    $this->executeSeed($environment, $seeder);
616
                }
617
            }
618
        } else {
619
            // run only one seeder
620
            if (array_key_exists($seed, $seeds)) {
621
                $this->executeSeed($environment, $seeds[$seed]);
622 400
            } else {
623
                throw new InvalidArgumentException(sprintf('The seed class "%s" does not exist', $seed));
624 400
            }
625 400
        }
626
    }
627
628
    /**
629
     * Sets the environments.
630
     *
631
     * @param \Phinx\Migration\Manager\Environment[] $environments Environments
632
     *
633 395
     * @return $this
634
     */
635 395
    public function setEnvironments($environments = [])
636
    {
637
        $this->environments = $environments;
638
639
        return $this;
640
    }
641
642
    /**
643
     * Gets the manager class for the given environment.
644 379
     *
645
     * @param string $name Environment Name
646 379
     *
647 379
     * @throws \InvalidArgumentException
648
     *
649
     * @return \Phinx\Migration\Manager\Environment
650
     */
651
    public function getEnvironment($name)
652
    {
653
        if (isset($this->environments[$name])) {
654
            return $this->environments[$name];
655
        }
656
657 388
        // check the environment exists
658
        if (!$this->getConfig()->hasEnvironment($name)) {
659 388
            throw new InvalidArgumentException(sprintf(
660 388
                'The environment "%s" does not exist',
661
                $name
662
            ));
663 388
        }
664
665 388
        // create an environment instance and cache it
666
        $envOptions = $this->getConfig()->getEnvironment($name);
667 388
        $envOptions['version_order'] = $this->getConfig()->getVersionOrder();
668 387
        $envOptions['data_domain'] = $this->getConfig()->getDataDomain();
669 387
670
        $environment = new Environment($name, $envOptions);
671 387
        $this->environments[$name] = $environment;
672 3
        $environment->setInput($this->getInput());
673
        $environment->setOutput($this->getOutput());
674
675 387
        return $environment;
676 387
    }
677
678
    /**
679 387
     * Sets the user defined PSR-11 container
680
     *
681 387
     * @param \Psr\Container\ContainerInterface $container Container
682 2
     *
683 2
     * @return void
684 2
     */
685 2
    public function setContainer(ContainerInterface $container)
686 2
    {
687
        $this->container = $container;
688
    }
689 387
690
    /**
691
     * Sets the console input.
692
     *
693 387
     * @param \Symfony\Component\Console\Input\InputInterface $input Input
694 387
     *
695 2
     * @return $this
696 2
     */
697 2
    public function setInput(InputInterface $input)
698
    {
699 2
        $this->input = $input;
700
701
        return $this;
702
    }
703 385
704
    /**
705 385
     * Gets the console input.
706 2
     *
707 2
     * @return \Symfony\Component\Console\Input\InputInterface
708 2
     */
709
    public function getInput()
710 2
    {
711
        return $this->input;
712
    }
713 383
714 383
    /**
715 384
     * Sets the console output.
716
     *
717 379
     * @param \Symfony\Component\Console\Output\OutputInterface $output Output
718 379
     *
719 379
     * @return $this
720
     */
721 379
    public function setOutput(OutputInterface $output)
722
    {
723
        $this->output = $output;
724
725
        return $this;
726
    }
727
728
    /**
729 388
     * Gets the console output.
730
     *
731 388
     * @return \Symfony\Component\Console\Output\OutputInterface
732 388
     */
733 388
    public function getOutput()
734
    {
735 388
        return $this->output;
736 388
    }
737 388
738 388
    /**
739 388
     * Sets the database migrations.
740 388
     *
741
     * @param \Phinx\Migration\AbstractMigration[] $migrations Migrations
742 388
     *
743
     * @return $this
744
     */
745
    public function setMigrations(array $migrations)
746
    {
747
        $this->migrations = $migrations;
748
749
        return $this;
750
    }
751 11
752
    /**
753 11
     * Gets an array of the database migrations, indexed by migration name (aka creation time) and sorted in ascending
754 11
     * order
755
     *
756
     * @param string $environment Environment
757
     *
758
     * @throws \InvalidArgumentException
759
     *
760
     * @return \Phinx\Migration\AbstractMigration[]
761
     */
762
    public function getMigrations($environment)
763 11
    {
764
        if ($this->migrations === null) {
765 11
            $phpFiles = $this->getMigrationFiles();
766 11
767
            if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
768
                $this->getOutput()->writeln('Migration file');
769 11
                $this->getOutput()->writeln(
770
                    array_map(
771 11
                        function ($phpFile) {
772
                            return "    <info>{$phpFile}</info>";
773 11
                        },
774 11
                        $phpFiles
775 11
                    )
776 11
                );
777
            }
778
779 11
            // filter the files to only get the ones that match our naming scheme
780 11
            $fileNames = [];
781
            /** @var \Phinx\Migration\AbstractMigration[] $versions */
782
            $versions = [];
783
784 11
            foreach ($phpFiles as $filePath) {
785 11
                if (Util::isValidMigrationFileName(basename($filePath))) {
786
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
787
                        $this->getOutput()->writeln("Valid migration file <info>{$filePath}</info>.");
788
                    }
789
790
                    $version = Util::getVersionFromFileName(basename($filePath));
791
792
                    if (isset($versions[$version])) {
793
                        throw new InvalidArgumentException(sprintf('Duplicate migration - "%s" has the same version as "%s"', $filePath, $versions[$version]->getVersion()));
794 11
                    }
795
796 11
                    $config = $this->getConfig();
797
                    $namespace = $config instanceof NamespaceAwareInterface ? $config->getMigrationNamespaceByPath(dirname($filePath)) : null;
798
799
                    // convert the filename to a class name
800
                    $class = ($namespace === null ? '' : $namespace . '\\') . Util::mapFileNameToClassName(basename($filePath));
801
802
                    if (isset($fileNames[$class])) {
803
                        throw new InvalidArgumentException(sprintf(
804 11
                            'Migration "%s" has the same name as "%s"',
805 11
                            basename($filePath),
806 11
                            $fileNames[$class]
807
                        ));
808 11
                    }
809 11
810 11
                    $fileNames[$class] = basename($filePath);
811
812 11
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
813
                        $this->getOutput()->writeln("Loading class <info>$class</info> from <info>$filePath</info>.");
814
                    }
815
816
                    // load the migration file
817
                    $orig_display_errors_setting = ini_get('display_errors');
818
                    ini_set('display_errors', 'On');
819
                    /** @noinspection PhpIncludeInspection */
820 11
                    require_once $filePath;
821
                    ini_set('display_errors', $orig_display_errors_setting);
822 11
                    if (!class_exists($class)) {
823 11
                        throw new InvalidArgumentException(sprintf(
824 11
                            'Could not find class "%s" in file "%s"',
825
                            $class,
826 11
                            $filePath
827 11
                        ));
828 11
                    }
829 11
830 11
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
831 11
                        $this->getOutput()->writeln("Running <info>$class</info>.");
832
                    }
833 11
834
                    // instantiate it
835
                    $migration = new $class($environment, $version, $this->getInput(), $this->getOutput());
836
837
                    if (!($migration instanceof AbstractMigration)) {
838
                        throw new InvalidArgumentException(sprintf(
839
                            'The class "%s" in file "%s" must extend \Phinx\Migration\AbstractMigration',
840
                            $class,
841
                            $filePath
842 400
                        ));
843
                    }
844 400
845 400
                    $versions[$version] = $migration;
846
                } else {
847
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
848
                        $this->getOutput()->writeln("Invalid migration file <error>{$filePath}</error>.");
849
                    }
850
                }
851
            }
852
853 399
            ksort($versions);
854
            $this->setMigrations($versions);
855 399
        }
856
857
        return $this->migrations;
858
    }
859
860
    /**
861
     * Returns a list of migration files found in the provided migration paths.
862
     *
863
     * @return string[]
864
     */
865 2
    protected function getMigrationFiles()
866
    {
867 2
        return Util::getFiles($this->getConfig()->getMigrationPaths());
868 2
    }
869 2
870 2
    /**
871
     * Sets the database seeders.
872 2
     *
873
     * @param \Phinx\Seed\AbstractSeed[] $seeds Seeders
874
     *
875
     * @return $this
876 2
     */
877 1
    public function setSeeds(array $seeds)
878 1
    {
879 1
        $this->seeds = $seeds;
880
881 2
        return $this;
882 1
    }
883 1
884
    /**
885 1
     * Get seed dependencies instances from seed dependency array
886 1
     *
887
     * @param \Phinx\Seed\AbstractSeed $seed Seed
888
     *
889 1
     * @return \Phinx\Seed\AbstractSeed[]
890
     */
891 1
    protected function getSeedDependenciesInstances(AbstractSeed $seed)
892
    {
893 1
        $dependenciesInstances = [];
894 1
        $dependencies = $seed->getDependencies();
895 1
        if (!empty($dependencies)) {
896 1
            foreach ($dependencies as $dependency) {
897 1
                foreach ($this->seeds as $seed) {
898 1
                    if (get_class($seed) === $dependency) {
899
                        $dependenciesInstances[get_class($seed)] = $seed;
900
                    }
901
                }
902
            }
903
        }
904
905
        return $dependenciesInstances;
906 1
    }
907
908 1
    /**
909 1
     * Order seeds by dependencies
910 1
     *
911 1
     * @param \Phinx\Seed\AbstractSeed[] $seeds Seeds
912 1
     *
913
     * @return \Phinx\Seed\AbstractSeed[]
914
     */
915
    protected function orderSeedsByDependencies(array $seeds)
916
    {
917
        $orderedSeeds = [];
918
        foreach ($seeds as $seed) {
919
            $key = get_class($seed);
920
            $dependencies = $this->getSeedDependenciesInstances($seed);
921
            if (!empty($dependencies)) {
922
                $orderedSeeds[$key] = $seed;
923
                $orderedSeeds = array_merge($this->orderSeedsByDependencies($dependencies), $orderedSeeds);
924
            } else {
925
                $orderedSeeds[$key] = $seed;
926
            }
927
        }
928
929
        return $orderedSeeds;
930
    }
931
932
    /**
933
     * Gets an array of database seeders.
934
     *
935
     * @throws \InvalidArgumentException
936
     *
937
     * @return \Phinx\Seed\AbstractSeed[]
938
     */
939
    public function getSeeds()
940
    {
941
        if ($this->seeds === null) {
942
            $phpFiles = $this->getSeedFiles();
943
944
            // filter the files to only get the ones that match our naming scheme
945
            $fileNames = [];
946
            /** @var \Phinx\Seed\AbstractSeed[] $seeds */
947
            $seeds = [];
948
949
            foreach ($phpFiles as $filePath) {
950
                if (Util::isValidSeedFileName(basename($filePath))) {
951
                    $config = $this->getConfig();
952
                    $namespace = $config instanceof NamespaceAwareInterface ? $config->getSeedNamespaceByPath(dirname($filePath)) : null;
953
954
                    // convert the filename to a class name
955
                    $class = ($namespace === null ? '' : $namespace . '\\') . pathinfo($filePath, PATHINFO_FILENAME);
956
                    $fileNames[$class] = basename($filePath);
957
958
                    // load the seed file
959
                    /** @noinspection PhpIncludeInspection */
960
                    require_once $filePath;
961
                    if (!class_exists($class)) {
962
                        throw new InvalidArgumentException(sprintf(
963
                            'Could not find class "%s" in file "%s"',
964
                            $class,
965
                            $filePath
966
                        ));
967
                    }
968
969
                    // instantiate it
970
                    /** @var \Phinx\Seed\AbstractSeed $seed */
971
                    if ($this->container !== null) {
972
                        $seed = $this->container->get($class);
973
                    } else {
974
                        $seed = new $class();
975
                    }
976
                    $input = $this->getInput();
977
                    if ($input !== null) {
978
                        $seed->setInput($input);
979
                    }
980
                    $output = $this->getOutput();
981
                    if ($output !== null) {
982
                        $seed->setOutput($output);
983
                    }
984
985
                    if (!($seed instanceof AbstractSeed)) {
986
                        throw new InvalidArgumentException(sprintf(
987
                            'The class "%s" in file "%s" must extend \Phinx\Seed\AbstractSeed',
988
                            $class,
989
                            $filePath
990
                        ));
991
                    }
992
993
                    $seeds[$class] = $seed;
994
                }
995
            }
996
997
            ksort($seeds);
998
            $this->setSeeds($seeds);
999
        }
1000
1001
        $this->seeds = $this->orderSeedsByDependencies($this->seeds);
1002
1003
        return $this->seeds;
1004
    }
1005
1006
    /**
1007
     * Returns a list of seed files found in the provided seed paths.
1008
     *
1009
     * @return string[]
1010
     */
1011
    protected function getSeedFiles()
1012
    {
1013
        return Util::getFiles($this->getConfig()->getSeedPaths());
1014
    }
1015
1016
    /**
1017
     * Sets the config.
1018
     *
1019
     * @param \Phinx\Config\ConfigInterface $config Configuration Object
1020
     *
1021
     * @return $this
1022
     */
1023
    public function setConfig(ConfigInterface $config)
1024
    {
1025
        $this->config = $config;
1026
1027
        return $this;
1028
    }
1029
1030
    /**
1031
     * Gets the config.
1032
     *
1033
     * @return \Phinx\Config\ConfigInterface
1034
     */
1035
    public function getConfig()
1036
    {
1037
        return $this->config;
1038
    }
1039
1040
    /**
1041
     * Toggles the breakpoint for a specific version.
1042
     *
1043
     * @param string $environment Environment name
1044
     * @param int|null $version Version
1045
     *
1046
     * @return void
1047
     */
1048
    public function toggleBreakpoint($environment, $version)
1049
    {
1050
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_TOGGLE);
1051
    }
1052
1053
    /**
1054
     * Updates the breakpoint for a specific version.
1055
     *
1056
     * @param string $environment The required environment
1057
     * @param int|null $version The version of the target migration
1058
     * @param int $mark The state of the breakpoint as defined by self::BREAKPOINT_xxxx constants.
1059
     *
1060
     * @return void
1061
     */
1062
    protected function markBreakpoint($environment, $version, $mark)
1063
    {
1064
        $migrations = $this->getMigrations($environment);
1065
        $this->getMigrations($environment);
1066
        $env = $this->getEnvironment($environment);
1067
        $versions = $env->getVersionLog();
1068
1069
        if (empty($versions) || empty($migrations)) {
1070
            return;
1071
        }
1072
1073
        if ($version === null) {
1074
            $lastVersion = end($versions);
1075
            $version = $lastVersion['version'];
1076
        }
1077
1078
        if ($version != 0 && (!isset($versions[$version]) || !isset($migrations[$version]))) {
1079
            $this->output->writeln(sprintf(
1080
                '<comment>warning</comment> %s is not a valid version',
1081
                $version
1082
            ));
1083
1084
            return;
1085
        }
1086
1087
        switch ($mark) {
1088
            case self::BREAKPOINT_TOGGLE:
1089
                $env->getAdapter()->toggleBreakpoint($migrations[$version]);
1090
                break;
1091
            case self::BREAKPOINT_SET:
1092
                if ($versions[$version]['breakpoint'] == 0) {
1093
                    $env->getAdapter()->setBreakpoint($migrations[$version]);
1094
                }
1095
                break;
1096
            case self::BREAKPOINT_UNSET:
1097
                if ($versions[$version]['breakpoint'] == 1) {
1098
                    $env->getAdapter()->unsetBreakpoint($migrations[$version]);
1099
                }
1100
                break;
1101
        }
1102
1103
        $versions = $env->getVersionLog();
1104
1105
        $this->getOutput()->writeln(
1106
            ' Breakpoint ' . ($versions[$version]['breakpoint'] ? 'set' : 'cleared') .
1107
            ' for <info>' . $version . '</info>' .
1108
            ' <comment>' . $migrations[$version]->getName() . '</comment>'
1109
        );
1110
    }
1111
1112
    /**
1113
     * Remove all breakpoints
1114
     *
1115
     * @param string $environment The required environment
1116
     *
1117
     * @return void
1118
     */
1119
    public function removeBreakpoints($environment)
1120
    {
1121
        $this->getOutput()->writeln(sprintf(
1122
            ' %d breakpoints cleared.',
1123
            $this->getEnvironment($environment)->getAdapter()->resetAllBreakpoints()
1124
        ));
1125
    }
1126
1127
    /**
1128
     * Set the breakpoint for a specific version.
1129
     *
1130
     * @param string $environment The required environment
1131
     * @param int|null $version The version of the target migration
1132
     *
1133
     * @return void
1134
     */
1135
    public function setBreakpoint($environment, $version)
1136
    {
1137
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_SET);
1138
    }
1139
1140
    /**
1141
     * Unset the breakpoint for a specific version.
1142
     *
1143
     * @param string $environment The required environment
1144
     * @param int|null $version The version of the target migration
1145
     *
1146
     * @return void
1147
     */
1148
    public function unsetBreakpoint($environment, $version)
1149
    {
1150
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_UNSET);
1151
    }
1152
}
1153