Issues (27)

src/Phinx/Migration/Manager.php (3 issues)

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
     * @var int
68
     */
69
    private $verbosityLevel = OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_NORMAL;
70
71
    /**
72
     * @param \Phinx\Config\ConfigInterface $config Configuration Object
73
     * @param \Symfony\Component\Console\Input\InputInterface $input Console Input
74
     * @param \Symfony\Component\Console\Output\OutputInterface $output Console Output
75
     */
76
    public function __construct(ConfigInterface $config, InputInterface $input, OutputInterface $output)
77
    {
78
        $this->setConfig($config);
79
        $this->setInput($input);
80
        $this->setOutput($output);
81
    }
82
83
    /**
84
     * Prints the specified environment's migration status.
85
     *
86
     * @param string $environment environment to print status of
87
     * @param string|null $format format to print status in (either text, json, or null)
88
     * @throws \RuntimeException
89 432
     * @return array array indicating if there are any missing or down migrations
90
     */
91 432
    public function printStatus($environment, $format = null)
92 432
    {
93 432
        $output = $this->getOutput();
94 432
        $hasDownMigration = false;
95
        $hasMissingMigration = false;
96
        $migrations = $this->getMigrations($environment);
97
        $migrationCount = 0;
98
        $missingCount = 0;
99
        $pendingMigrationCount = 0;
100
        $finalMigrations = [];
101
        $verbosity = $output->getVerbosity();
102
        if ($format === 'json') {
103 22
            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
104
        }
105 22
        if (count($migrations)) {
106 22
            // rewrite using Symfony Table Helper as we already have this library
107 22
            // included and it will fix formatting issues (e.g drawing the lines)
108 22
            $output->writeln('', $this->verbosityLevel);
109 22
110 22
            switch ($this->getConfig()->getVersionOrder()) {
111
                case Config::VERSION_ORDER_CREATION_TIME:
112
                    $migrationIdAndStartedHeader = '<info>[Migration ID]</info>  Started            ';
113 21
                    break;
114
                case Config::VERSION_ORDER_EXECUTION_TIME:
115 21
                    $migrationIdAndStartedHeader = 'Migration ID    <info>[Started          ]</info>';
116 21
                    break;
117 19
                default:
118 19
                    throw new RuntimeException('Invalid version_order configuration option');
119 2
            }
120 1
121 1
            $output->writeln(" Status  $migrationIdAndStartedHeader  Finished             Migration Name ", $this->verbosityLevel);
122 1
            $output->writeln('----------------------------------------------------------------------------------', $this->verbosityLevel);
123 1
124 21
            $env = $this->getEnvironment($environment);
125
            $versions = $env->getVersionLog();
126 20
127 20
            $maxNameLength = $versions ? max(array_map(function ($version) {
128
                return strlen($version['migration_name']);
129 20
            }, $versions)) : 0;
130 20
131
            $missingVersions = array_diff_key($versions, $migrations);
132
            $missingCount = count($missingVersions);
133 17
134 20
            $hasMissingMigration = !empty($missingVersions);
135
136 20
            // get the migrations sorted in the same way as the versions
137
            /** @var \Phinx\Migration\AbstractMigration[] $sortedMigrations */
138 20
            $sortedMigrations = [];
139
140
            foreach ($versions as $versionCreationTime => $version) {
141 20
                if (isset($migrations[$versionCreationTime])) {
142
                    array_push($sortedMigrations, $migrations[$versionCreationTime]);
143 20
                    unset($migrations[$versionCreationTime]);
144 17
                }
145 13
            }
146 13
147 13
            if (empty($sortedMigrations) && !empty($missingVersions)) {
148 20
                // this means we have no up migrations, so we write all the missing versions already so they show up
149
                // before any possible down migration
150 20
                foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
151
                    $this->printMissingVersion($missingVersion, $maxNameLength);
152
153 4
                    unset($missingVersions[$missingVersionCreationTime]);
154 4
                }
155
            }
156 4
157 4
            // any migration left in the migrations (ie. not unset when sorting the migrations by the version order) is
158 4
            // a migration that is down, so we add them to the end of the sorted migrations list
159
            if (!empty($migrations)) {
160
                $sortedMigrations = array_merge($sortedMigrations, $migrations);
161
            }
162 20
163 13
            $migrationCount = count($sortedMigrations);
164 13
            foreach ($sortedMigrations as $migration) {
165
                $version = array_key_exists($migration->getVersion(), $versions) ? $versions[$migration->getVersion()] : false;
166 20
                if ($version) {
167 20
                    // check if there are missing versions before this version
168 20
                    foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
169
                        if ($this->getConfig()->isVersionOrderCreationTime()) {
170 13
                            if ($missingVersion['version'] > $version['version']) {
171 6
                                break;
172 6
                            }
173 4
                        } else {
174
                            if ($missingVersion['start_time'] > $version['start_time']) {
175 3
                                break;
176
                            } elseif (
177
                                $missingVersion['start_time'] == $version['start_time'] &&
178
                                $missingVersion['version'] > $version['version']
179
                            ) {
180
                                break;
181
                            }
182
                        }
183
184 3
                        $this->printMissingVersion($missingVersion, $maxNameLength);
185
186 3
                        unset($missingVersions[$missingVersionCreationTime]);
187 13
                    }
188
189 13
                    $status = '     <info>up</info> ';
190 13
                } else {
191 13
                    $pendingMigrationCount++;
192 13
                    $hasDownMigration = true;
193
                    $status = '   <error>down</error> ';
194 20
                }
195
                $maxNameLength = max($maxNameLength, strlen($migration->getName()));
196 20
197 20
                $output->writeln(
198 20
                    sprintf(
199 20
                        '%s %14.0f  %19s  %19s  <comment>%s</comment>',
200 20
                        $status,
201 20
                        $migration->getVersion(),
202 20
                        ($version ? $version['start_time'] : ''),
203 20
                        ($version ? $version['end_time'] : ''),
204
                        $migration->getName()
205 20
                    ),
206 1
                    $this->verbosityLevel
207 1
                );
208
209 20
                if ($version && $version['breakpoint']) {
210 20
                    $output->writeln('         <error>BREAKPOINT SET</error>', $this->verbosityLevel);
211 20
                }
212
213
                $finalMigrations[] = ['migration_status' => trim(strip_tags($status)), 'migration_id' => sprintf('%14.0f', $migration->getVersion()), 'migration_name' => $migration->getName()];
214 20
                unset($versions[$migration->getVersion()]);
215 4
            }
216
217 4
            // and finally add any possibly-remaining missing migrations
218 20
            foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
219 20
                $this->printMissingVersion($missingVersion, $maxNameLength);
220
221 1
                unset($missingVersions[$missingVersionCreationTime]);
222 1
            }
223
        } else {
224
            // there are no migrations
225
            $output->writeln('', $this->verbosityLevel);
226 21
            $output->writeln('There are no available migrations. Try creating one using the <info>create</info> command.', $this->verbosityLevel);
227 21
        }
228
229
        // write an empty line
230
        $output->writeln('', $this->verbosityLevel);
231
232
        if ($format !== null) {
233
            switch ($format) {
234
                case AbstractCommand::FORMAT_JSON:
235
                    $output->setVerbosity($verbosity);
236
                    $output->writeln(json_encode(
237
                        [
238
                            'pending_count' => $pendingMigrationCount,
239
                            'missing_count' => $missingCount,
240
                            'total_count' => $migrationCount + $missingCount,
241
                            'migrations' => $finalMigrations,
242 21
                        ]
243 10
                    ));
244 11
                    break;
245 6
                default:
246
                    $output->writeln('<info>Unsupported format: ' . $format . '</info>');
247 5
            }
248
        }
249
250
        return [
251
            'hasMissingMigration' => $hasMissingMigration,
252
            'hasDownMigration' => $hasDownMigration,
253
        ];
254
    }
255
256
    /**
257 10
     * Print Missing Version
258
     *
259 10
     * @param array $version The missing version to print (in the format returned by Environment.getVersionLog).
260 10
     * @param int $maxNameLength The maximum migration name length.
261 10
     * @return void
262 10
     */
263 10
    protected function printMissingVersion($version, $maxNameLength)
264 10
    {
265 10
        $this->getOutput()->writeln(sprintf(
266
            '     <error>up</error>  %14.0f  %19s  %19s  <comment>%s</comment>  <error>** MISSING MIGRATION FILE **</error>',
267 10
            $version['version'],
268 1
            $version['start_time'],
269 1
            $version['end_time'],
270 10
            str_pad($version['migration_name'], $maxNameLength, ' ')
271
        ));
272
273
        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...
274
            $this->getOutput()->writeln('         <error>BREAKPOINT SET</error>');
275
        }
276
    }
277
278
    /**
279
     * Migrate to the version of the database on a given date.
280 4
     *
281
     * @param string $environment Environment
282 4
     * @param \DateTime $dateTime Date to migrate to
283 4
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the
284
     *                               migration
285
     * @return void
286 4
     */
287 4
    public function migrateToDateTime($environment, DateTime $dateTime, $fake = false)
288
    {
289 4
        $versions = array_keys($this->getMigrations($environment));
290 3
        $dateString = $dateTime->format('YmdHis');
291 3
292 3
        $outstandingMigrations = array_filter($versions, function ($version) use ($dateString) {
293 3
            return $version <= $dateString;
294 4
        });
295
296
        if (count($outstandingMigrations) > 0) {
297
            $migration = max($outstandingMigrations);
298
            $this->getOutput()->writeln('Migrating to version ' . $migration, $this->verbosityLevel);
299
            $this->migrate($environment, $migration, $fake);
300
        }
301
    }
302
303 8
    /**
304
     * Migrate an environment to the specified version.
305 8
     *
306 8
     * @param string $environment Environment
307 8
     * @param int|null $version version to migrate to
308 8
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration
309
     * @return void
310 8
     */
311
    public function migrate($environment, $version = null, $fake = false)
312
    {
313
        $migrations = $this->getMigrations($environment);
314 8
        $env = $this->getEnvironment($environment);
315 5
        $versions = $env->getVersions();
316 5
        $current = $env->getCurrentVersion();
317 3
318
        if (empty($versions) && empty($migrations)) {
319
            return;
320
        }
321
322
        if ($version === null) {
323
            $version = max(array_merge($versions, array_keys($migrations)));
324
        } else {
325
            if ($version != 0 && !isset($migrations[$version])) {
326
                $this->output->writeln(sprintf(
327 8
                    '<comment>warning</comment> %s is not a valid version',
328
                    $version
329 8
                ));
330
331
                return;
332
            }
333
        }
334
335
        // are we migrating up or down?
336
        $direction = $version > $current ? MigrationInterface::UP : MigrationInterface::DOWN;
337
338
        if ($direction === MigrationInterface::DOWN) {
339
            // run downs first
340
            krsort($migrations);
341
            foreach ($migrations as $migration) {
342
                if ($migration->getVersion() <= $version) {
343 8
                    break;
344 8
                }
345 8
346 2
                if (in_array($migration->getVersion(), $versions)) {
347
                    $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake);
348
                }
349 8
            }
350 5
        }
351 5
352 8
        ksort($migrations);
353 8
        foreach ($migrations as $migration) {
354
            if ($migration->getVersion() > $version) {
355
                break;
356
            }
357
358
            if (!in_array($migration->getVersion(), $versions)) {
359
                $this->executeMigration($environment, $migration, MigrationInterface::UP, $fake);
360
            }
361
        }
362
    }
363 119
364
    /**
365 119
     * Execute a migration against the specified environment.
366 119
     *
367
     * @param string $name Environment Name
368 119
     * @param \Phinx\Migration\MigrationInterface $migration Migration
369 119
     * @param string $direction Direction
370 119
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration
371
     * @return void
372
     */
373 119
    public function executeMigration($name, MigrationInterface $migration, $direction = MigrationInterface::UP, $fake = false)
374 119
    {
375 119
        $this->getOutput()->writeln('', $this->verbosityLevel);
376
        $this->getOutput()->writeln(
377 119
            ' ==' .
378
            ' <info>' . $migration->getVersion() . ' ' . $migration->getName() . ':</info>' .
379 119
            ' <comment>' . ($direction === MigrationInterface::UP ? 'migrating' : 'reverting') . '</comment>',
380 119
            $this->verbosityLevel
381 119
        );
382 119
383 119
        // Execute the migration and log the time elapsed.
384
        $start = microtime(true);
385
        $this->getEnvironment($name)->executeMigration($migration, $direction, $fake);
386
        $end = microtime(true);
387
388
        $this->getOutput()->writeln(
389
            ' ==' .
390
            ' <info>' . $migration->getVersion() . ' ' . $migration->getName() . ':</info>' .
391
            ' <comment>' . ($direction === MigrationInterface::UP ? 'migrated' : 'reverted') .
392 6
            ' ' . sprintf('%.4fs', $end - $start) . '</comment>',
393
            $this->verbosityLevel
394 6
        );
395 6
    }
396
397 6
    /**
398 6
     * Execute a seeder against the specified environment.
399 6
     *
400
     * @param string $name Environment Name
401
     * @param \Phinx\Seed\SeedInterface $seed Seed
402 6
     * @return void
403 6
     */
404 6
    public function executeSeed($name, SeedInterface $seed)
405
    {
406 6
        $this->getOutput()->writeln('', $this->verbosityLevel);
407
        $this->getOutput()->writeln(
408 6
            ' ==' .
409 6
            ' <info>' . $seed->getName() . ':</info>' .
410 6
            ' <comment>seeding</comment>',
411 6
            $this->verbosityLevel
412 6
        );
413
414
        // Execute the seeder and log the time elapsed.
415
        $start = microtime(true);
416
        $this->getEnvironment($name)->executeSeed($seed);
417
        $end = microtime(true);
418
419
        $this->getOutput()->writeln(
420
            ' ==' .
421
            ' <info>' . $seed->getName() . ':</info>' .
422
            ' <comment>seeded' .
423 349
            ' ' . sprintf('%.4fs', $end - $start) . '</comment>',
424
            $this->verbosityLevel
425
        );
426 349
    }
427
428
    /**
429 349
     * Rollback an environment to the specified version.
430
     *
431
     * @param string $environment Environment
432 349
     * @param int|string|null $target Target
433
     * @param bool $force Force
434 349
     * @param bool $targetMustMatchVersion Target must match version
435
     * @param bool $fake Flag that if true, we just record running the migration, but not actually do the migration
436
     * @return void
437 349
     */
438 48
    public function rollback($environment, $target = null, $force = false, $targetMustMatchVersion = true, $fake = false)
439 48
    {
440 48
        // note that the migrations are indexed by name (aka creation time) in ascending order
441
        $migrations = $this->getMigrations($environment);
442 349
443 349
        // note that the version log are also indexed by name with the proper ascending order according to the version order
444 349
        $executedVersions = $this->getEnvironment($environment)->getVersionLog();
445
446
        // get a list of migrations sorted in the opposite way of the executed versions
447 47
        $sortedMigrations = [];
448
449 349
        foreach ($executedVersions as $versionCreationTime => &$executedVersion) {
450
            // if we have a date (ie. the target must not match a version) and we are sorting by execution time, we
451 349
            // convert the version start time so we can compare directly with the target date
452 23
            if (!$this->getConfig()->isVersionOrderCreationTime() && !$targetMustMatchVersion) {
453 349
                $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $executedVersion['start_time']);
454
                $executedVersion['start_time'] = $dateTime->format('YmdHis');
455 20
            }
456 20
457 20
            if (isset($migrations[$versionCreationTime])) {
458 20
                array_unshift($sortedMigrations, $migrations[$versionCreationTime]);
459
            } else {
460
                // this means the version is missing so we unset it so that we don't consider it when rolling back
461 20
                // migrations (or choosing the last up version as target)
462 20
                unset($executedVersions[$versionCreationTime]);
463 20
            }
464
        }
465
466
        if ($target === 'all' || $target === '0') {
467 20
            $target = 0;
468
        } elseif (!is_numeric($target) && $target !== null) { // try to find a target version based on name
469
            // search through the migrations using the name
470 349
            $migrationNames = array_map(function ($item) {
471 349
                return $item['migration_name'];
472 53
            }, $executedVersions);
473 53
            $found = array_search($target, $migrationNames, true);
474
475
            // check on was found
476
            if ($found !== false) {
477 296
                $target = (string)$found;
478
            } else {
479 94
                $this->getOutput()->writeln("<error>No migration found with name ($target)</error>");
480 94
481 94
                return;
482
            }
483
        }
484 296
485 46
        // Check we have at least 1 migration to revert
486 46
        $executedVersionCreationTimes = array_keys($executedVersions);
487
        if (empty($executedVersionCreationTimes) || $target == end($executedVersionCreationTimes)) {
488
            $this->getOutput()->writeln('<error>No migrations to rollback</error>');
489
490 250
            return;
491
        }
492 250
493 250
        // If no target was supplied, revert the last migration
494 70
        if ($target === null) {
495
            // Get the migration before the last run migration
496
            $prev = count($executedVersionCreationTimes) - 2;
497 250
            $target = $prev >= 0 ? $executedVersionCreationTimes[$prev] : 0;
498 250
        }
499
500 250
        // If the target must match a version, check the target version exists
501 96
        if ($targetMustMatchVersion && $target !== 0 && !isset($migrations[$target])) {
502 96
            $this->getOutput()->writeln("<error>Target version ($target) not found</error>");
503 42
504
            return;
505 68
        }
506
507 222
        // Rollback all versions until we find the wanted rollback target
508 121
        $rollbacked = false;
509 121
510
        foreach ($sortedMigrations as $migration) {
511 117
            if ($targetMustMatchVersion && $migration->getVersion() == $target) {
512 117
                break;
513 117
            }
514 250
515
            if (in_array($migration->getVersion(), $executedVersionCreationTimes)) {
516 250
                $executedVersion = $executedVersions[$migration->getVersion()];
517 133
518 133
                if (!$targetMustMatchVersion) {
519 250
                    if (
520
                        ($this->getConfig()->isVersionOrderCreationTime() && $executedVersion['version'] <= $target) ||
521
                        (!$this->getConfig()->isVersionOrderCreationTime() && $executedVersion['start_time'] <= $target)
522
                    ) {
523
                        break;
524
                    }
525
                }
526
527
                if ($executedVersion['breakpoint'] != 0 && !$force) {
528 9
                    $this->getOutput()->writeln('<error>Breakpoint reached. Further rollbacks inhibited.</error>');
529
                    break;
530 9
                }
531
                $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake);
532 9
                $rollbacked = true;
533
            }
534 3
        }
535 3
536 3
        if (!$rollbacked) {
0 ignored issues
show
The condition $rollbacked is always false.
Loading history...
537 3
            $this->getOutput()->writeln('<error>No migrations to rollback</error>');
538 3
        }
539 3
    }
540
541 6
    /**
542 3
     * Run database seeders against an environment.
543 3
     *
544 3
     * @param string $environment Environment
545
     * @param string|null $seed Seeder
546
     * @throws \InvalidArgumentException
547 6
     * @return void
548
     */
549
    public function seed($environment, $seed = null)
550
    {
551
        $seeds = $this->getSeeds();
552
553
        if ($seed === null) {
554
            // run all seeders
555 381
            foreach ($seeds as $seeder) {
556
                if (array_key_exists($seeder->getName(), $seeds)) {
557 381
                    $this->executeSeed($environment, $seeder);
558 381
                }
559
            }
560
        } else {
561
            // run only one seeder
562
            if (array_key_exists($seed, $seeds)) {
563
                $this->executeSeed($environment, $seeds[$seed]);
564
            } else {
565
                throw new InvalidArgumentException(sprintf('The seed class "%s" does not exist', $seed));
566
            }
567
        }
568 382
    }
569
570 382
    /**
571 380
     * Sets the environments.
572
     *
573
     * @param \Phinx\Migration\Manager\Environment[] $environments Environments
574
     * @return $this
575 7
     */
576 1
    public function setEnvironments($environments = [])
577 1
    {
578
        $this->environments = $environments;
579 1
580
        return $this;
581
    }
582
583 6
    /**
584 6
     * Gets the manager class for the given environment.
585
     *
586 6
     * @param string $name Environment Name
587 6
     * @throws \InvalidArgumentException
588 6
     * @return \Phinx\Migration\Manager\Environment
589 6
     */
590
    public function getEnvironment($name)
591 6
    {
592
        if (isset($this->environments[$name])) {
593
            return $this->environments[$name];
594
        }
595
596
        // check the environment exists
597
        if (!$this->getConfig()->hasEnvironment($name)) {
598
            throw new InvalidArgumentException(sprintf(
599
                'The environment "%s" does not exist',
600 400
                $name
601
            ));
602 400
        }
603 400
604
        // create an environment instance and cache it
605
        $envOptions = $this->getConfig()->getEnvironment($name);
606
        $envOptions['version_order'] = $this->getConfig()->getVersionOrder();
607
        $envOptions['data_domain'] = $this->getConfig()->getDataDomain();
608
609
        $environment = new Environment($name, $envOptions);
610
        $this->environments[$name] = $environment;
611 393
        $environment->setInput($this->getInput());
612
        $environment->setOutput($this->getOutput());
613 393
614
        return $environment;
615
    }
616
617
    /**
618
     * Sets the user defined PSR-11 container
619
     *
620
     * @param \Psr\Container\ContainerInterface $container Container
621
     * @return void
622 400
     */
623
    public function setContainer(ContainerInterface $container)
624 400
    {
625 400
        $this->container = $container;
626
    }
627
628
    /**
629
     * Sets the console input.
630
     *
631
     * @param \Symfony\Component\Console\Input\InputInterface $input Input
632
     * @return $this
633 395
     */
634
    public function setInput(InputInterface $input)
635 395
    {
636
        $this->input = $input;
637
638
        return $this;
639
    }
640
641
    /**
642
     * Gets the console input.
643
     *
644 379
     * @return \Symfony\Component\Console\Input\InputInterface
645
     */
646 379
    public function getInput()
647 379
    {
648
        return $this->input;
649
    }
650
651
    /**
652
     * Sets the console output.
653
     *
654
     * @param \Symfony\Component\Console\Output\OutputInterface $output Output
655
     * @return $this
656
     */
657 388
    public function setOutput(OutputInterface $output)
658
    {
659 388
        $this->output = $output;
660 388
661
        return $this;
662
    }
663 388
664
    /**
665 388
     * Gets the console output.
666
     *
667 388
     * @return \Symfony\Component\Console\Output\OutputInterface
668 387
     */
669 387
    public function getOutput()
670
    {
671 387
        return $this->output;
672 3
    }
673
674
    /**
675 387
     * Sets the database migrations.
676 387
     *
677
     * @param \Phinx\Migration\AbstractMigration[] $migrations Migrations
678
     * @return $this
679 387
     */
680
    public function setMigrations(array $migrations)
681 387
    {
682 2
        $this->migrations = $migrations;
683 2
684 2
        return $this;
685 2
    }
686 2
687
    /**
688
     * Gets an array of the database migrations, indexed by migration name (aka creation time) and sorted in ascending
689 387
     * order
690
     *
691
     * @param string $environment Environment
692
     * @throws \InvalidArgumentException
693 387
     * @return \Phinx\Migration\AbstractMigration[]
694 387
     */
695 2
    public function getMigrations($environment)
696 2
    {
697 2
        if ($this->migrations === null) {
698
            $phpFiles = $this->getMigrationFiles();
699 2
700
            if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
701
                $this->getOutput()->writeln('Migration file');
702
                $this->getOutput()->writeln(
703 385
                    array_map(
704
                        function ($phpFile) {
705 385
                            return "    <info>{$phpFile}</info>";
706 2
                        },
707 2
                        $phpFiles
708 2
                    )
709
                );
710 2
            }
711
712
            // filter the files to only get the ones that match our naming scheme
713 383
            $fileNames = [];
714 383
            /** @var \Phinx\Migration\AbstractMigration[] $versions */
715 384
            $versions = [];
716
717 379
            foreach ($phpFiles as $filePath) {
718 379
                if (Util::isValidMigrationFileName(basename($filePath))) {
719 379
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
720
                        $this->getOutput()->writeln("Valid migration file <info>{$filePath}</info>.");
721 379
                    }
722
723
                    $version = Util::getVersionFromFileName(basename($filePath));
724
725
                    if (isset($versions[$version])) {
726
                        throw new InvalidArgumentException(sprintf('Duplicate migration - "%s" has the same version as "%s"', $filePath, $versions[$version]->getVersion()));
727
                    }
728
729 388
                    $config = $this->getConfig();
730
                    $namespace = $config instanceof NamespaceAwareInterface ? $config->getMigrationNamespaceByPath(dirname($filePath)) : null;
731 388
732 388
                    // convert the filename to a class name
733 388
                    $class = ($namespace === null ? '' : $namespace . '\\') . Util::mapFileNameToClassName(basename($filePath));
734
735 388
                    if (isset($fileNames[$class])) {
736 388
                        throw new InvalidArgumentException(sprintf(
737 388
                            'Migration "%s" has the same name as "%s"',
738 388
                            basename($filePath),
739 388
                            $fileNames[$class]
740 388
                        ));
741
                    }
742 388
743
                    $fileNames[$class] = basename($filePath);
744
745
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
746
                        $this->getOutput()->writeln("Loading class <info>$class</info> from <info>$filePath</info>.");
747
                    }
748
749
                    // load the migration file
750
                    $orig_display_errors_setting = ini_get('display_errors');
751 11
                    ini_set('display_errors', 'On');
752
                    /** @noinspection PhpIncludeInspection */
753 11
                    require_once $filePath;
754 11
                    ini_set('display_errors', $orig_display_errors_setting);
755
                    if (!class_exists($class)) {
756
                        throw new InvalidArgumentException(sprintf(
757
                            'Could not find class "%s" in file "%s"',
758
                            $class,
759
                            $filePath
760
                        ));
761
                    }
762
763 11
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
764
                        $this->getOutput()->writeln("Running <info>$class</info>.");
765 11
                    }
766 11
767
                    // instantiate it
768
                    $migration = new $class($environment, $version, $this->getInput(), $this->getOutput());
769 11
770
                    if (!($migration instanceof AbstractMigration)) {
771 11
                        throw new InvalidArgumentException(sprintf(
772
                            'The class "%s" in file "%s" must extend \Phinx\Migration\AbstractMigration',
773 11
                            $class,
774 11
                            $filePath
775 11
                        ));
776 11
                    }
777
778
                    $versions[$version] = $migration;
779 11
                } else {
780 11
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
781
                        $this->getOutput()->writeln("Invalid migration file <error>{$filePath}</error>.");
782
                    }
783
                }
784 11
            }
785 11
786
            ksort($versions);
787
            $this->setMigrations($versions);
788
        }
789
790
        return $this->migrations;
791
    }
792
793
    /**
794 11
     * Returns a list of migration files found in the provided migration paths.
795
     *
796 11
     * @return string[]
797
     */
798
    protected function getMigrationFiles()
799
    {
800
        return Util::getFiles($this->getConfig()->getMigrationPaths());
801
    }
802
803
    /**
804 11
     * Sets the database seeders.
805 11
     *
806 11
     * @param \Phinx\Seed\AbstractSeed[] $seeds Seeders
807
     * @return $this
808 11
     */
809 11
    public function setSeeds(array $seeds)
810 11
    {
811
        $this->seeds = $seeds;
812 11
813
        return $this;
814
    }
815
816
    /**
817
     * Get seed dependencies instances from seed dependency array
818
     *
819
     * @param \Phinx\Seed\AbstractSeed $seed Seed
820 11
     * @return \Phinx\Seed\AbstractSeed[]
821
     */
822 11
    protected function getSeedDependenciesInstances(AbstractSeed $seed)
823 11
    {
824 11
        $dependenciesInstances = [];
825
        $dependencies = $seed->getDependencies();
826 11
        if (!empty($dependencies)) {
827 11
            foreach ($dependencies as $dependency) {
828 11
                foreach ($this->seeds as $seed) {
829 11
                    if (get_class($seed) === $dependency) {
830 11
                        $dependenciesInstances[get_class($seed)] = $seed;
831 11
                    }
832
                }
833 11
            }
834
        }
835
836
        return $dependenciesInstances;
837
    }
838
839
    /**
840
     * Order seeds by dependencies
841
     *
842 400
     * @param \Phinx\Seed\AbstractSeed[] $seeds Seeds
843
     * @return \Phinx\Seed\AbstractSeed[]
844 400
     */
845 400
    protected function orderSeedsByDependencies(array $seeds)
846
    {
847
        $orderedSeeds = [];
848
        foreach ($seeds as $seed) {
849
            $key = get_class($seed);
850
            $dependencies = $this->getSeedDependenciesInstances($seed);
851
            if (!empty($dependencies)) {
852
                $orderedSeeds[$key] = $seed;
853 399
                $orderedSeeds = array_merge($this->orderSeedsByDependencies($dependencies), $orderedSeeds);
854
            } else {
855 399
                $orderedSeeds[$key] = $seed;
856
            }
857
        }
858
859
        return $orderedSeeds;
860
    }
861
862
    /**
863
     * Gets an array of database seeders.
864
     *
865 2
     * @throws \InvalidArgumentException
866
     * @return \Phinx\Seed\AbstractSeed[]
867 2
     */
868 2
    public function getSeeds()
869 2
    {
870 2
        if ($this->seeds === null) {
871
            $phpFiles = $this->getSeedFiles();
872 2
873
            // filter the files to only get the ones that match our naming scheme
874
            $fileNames = [];
875
            /** @var \Phinx\Seed\AbstractSeed[] $seeds */
876 2
            $seeds = [];
877 1
878 1
            foreach ($phpFiles as $filePath) {
879 1
                if (Util::isValidSeedFileName(basename($filePath))) {
880
                    $config = $this->getConfig();
881 2
                    $namespace = $config instanceof NamespaceAwareInterface ? $config->getSeedNamespaceByPath(dirname($filePath)) : null;
882 1
883 1
                    // convert the filename to a class name
884
                    $class = ($namespace === null ? '' : $namespace . '\\') . pathinfo($filePath, PATHINFO_FILENAME);
0 ignored issues
show
Are you sure pathinfo($filePath, Phin...tion\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

884
                    $class = ($namespace === null ? '' : $namespace . '\\') . /** @scrutinizer ignore-type */ pathinfo($filePath, PATHINFO_FILENAME);
Loading history...
885 1
                    $fileNames[$class] = basename($filePath);
886 1
887
                    // load the seed file
888
                    /** @noinspection PhpIncludeInspection */
889 1
                    require_once $filePath;
890
                    if (!class_exists($class)) {
891 1
                        throw new InvalidArgumentException(sprintf(
892
                            'Could not find class "%s" in file "%s"',
893 1
                            $class,
894 1
                            $filePath
895 1
                        ));
896 1
                    }
897 1
898 1
                    // instantiate it
899
                    /** @var \Phinx\Seed\AbstractSeed $seed */
900
                    if ($this->container !== null) {
901
                        $seed = $this->container->get($class);
902
                    } else {
903
                        $seed = new $class();
904
                    }
905
                    $input = $this->getInput();
906 1
                    if ($input !== null) {
907
                        $seed->setInput($input);
908 1
                    }
909 1
                    $output = $this->getOutput();
910 1
                    if ($output !== null) {
911 1
                        $seed->setOutput($output);
912 1
                    }
913
914
                    if (!($seed instanceof AbstractSeed)) {
915
                        throw new InvalidArgumentException(sprintf(
916
                            'The class "%s" in file "%s" must extend \Phinx\Seed\AbstractSeed',
917
                            $class,
918
                            $filePath
919
                        ));
920
                    }
921
922
                    $seeds[$class] = $seed;
923
                }
924
            }
925
926
            ksort($seeds);
927
            $this->setSeeds($seeds);
928
        }
929
930
        $this->seeds = $this->orderSeedsByDependencies($this->seeds);
931
932
        return $this->seeds;
933
    }
934
935
    /**
936
     * Returns a list of seed files found in the provided seed paths.
937
     *
938
     * @return string[]
939
     */
940
    protected function getSeedFiles()
941
    {
942
        return Util::getFiles($this->getConfig()->getSeedPaths());
943
    }
944
945
    /**
946
     * Sets the config.
947
     *
948
     * @param \Phinx\Config\ConfigInterface $config Configuration Object
949
     * @return $this
950
     */
951
    public function setConfig(ConfigInterface $config)
952
    {
953
        $this->config = $config;
954
955
        return $this;
956
    }
957
958
    /**
959
     * Gets the config.
960
     *
961
     * @return \Phinx\Config\ConfigInterface
962
     */
963
    public function getConfig()
964
    {
965
        return $this->config;
966
    }
967
968
    /**
969
     * Toggles the breakpoint for a specific version.
970
     *
971
     * @param string $environment Environment name
972
     * @param int|null $version Version
973
     * @return void
974
     */
975
    public function toggleBreakpoint($environment, $version)
976
    {
977
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_TOGGLE);
978
    }
979
980
    /**
981
     * Updates the breakpoint for a specific version.
982
     *
983
     * @param string $environment The required environment
984
     * @param int|null $version The version of the target migration
985
     * @param int $mark The state of the breakpoint as defined by self::BREAKPOINT_xxxx constants.
986
     * @return void
987
     */
988
    protected function markBreakpoint($environment, $version, $mark)
989
    {
990
        $migrations = $this->getMigrations($environment);
991
        $this->getMigrations($environment);
992
        $env = $this->getEnvironment($environment);
993
        $versions = $env->getVersionLog();
994
995
        if (empty($versions) || empty($migrations)) {
996
            return;
997
        }
998
999
        if ($version === null) {
1000
            $lastVersion = end($versions);
1001
            $version = $lastVersion['version'];
1002
        }
1003
1004
        if ($version != 0 && (!isset($versions[$version]) || !isset($migrations[$version]))) {
1005
            $this->output->writeln(sprintf(
1006
                '<comment>warning</comment> %s is not a valid version',
1007
                $version
1008
            ));
1009
1010
            return;
1011
        }
1012
1013
        switch ($mark) {
1014
            case self::BREAKPOINT_TOGGLE:
1015
                $env->getAdapter()->toggleBreakpoint($migrations[$version]);
1016
                break;
1017
            case self::BREAKPOINT_SET:
1018
                if ($versions[$version]['breakpoint'] == 0) {
1019
                    $env->getAdapter()->setBreakpoint($migrations[$version]);
1020
                }
1021
                break;
1022
            case self::BREAKPOINT_UNSET:
1023
                if ($versions[$version]['breakpoint'] == 1) {
1024
                    $env->getAdapter()->unsetBreakpoint($migrations[$version]);
1025
                }
1026
                break;
1027
        }
1028
1029
        $versions = $env->getVersionLog();
1030
1031
        $this->getOutput()->writeln(
1032
            ' Breakpoint ' . ($versions[$version]['breakpoint'] ? 'set' : 'cleared') .
1033
            ' for <info>' . $version . '</info>' .
1034
            ' <comment>' . $migrations[$version]->getName() . '</comment>'
1035
        );
1036
    }
1037
1038
    /**
1039
     * Remove all breakpoints
1040
     *
1041
     * @param string $environment The required environment
1042
     * @return void
1043
     */
1044
    public function removeBreakpoints($environment)
1045
    {
1046
        $this->getOutput()->writeln(sprintf(
1047
            ' %d breakpoints cleared.',
1048
            $this->getEnvironment($environment)->getAdapter()->resetAllBreakpoints()
1049
        ));
1050
    }
1051
1052
    /**
1053
     * Set the breakpoint for a specific version.
1054
     *
1055
     * @param string $environment The required environment
1056
     * @param int|null $version The version of the target migration
1057
     * @return void
1058
     */
1059
    public function setBreakpoint($environment, $version)
1060
    {
1061
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_SET);
1062
    }
1063
1064
    /**
1065
     * Unset the breakpoint for a specific version.
1066
     *
1067
     * @param string $environment The required environment
1068
     * @param int|null $version The version of the target migration
1069
     * @return void
1070
     */
1071
    public function unsetBreakpoint($environment, $version)
1072
    {
1073
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_UNSET);
1074
    }
1075
1076
    /**
1077
     * @param int $verbosityLevel Verbosity level for info messages
1078
     * @return $this
1079
     */
1080
    public function setVerbosityLevel(int $verbosityLevel)
1081
    {
1082
        $this->verbosityLevel = $verbosityLevel;
1083
1084
        return $this;
1085
    }
1086
}
1087