Completed
Pull Request — master (#1560)
by Richard
01:52
created

Manager::setBreakpoint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 * Copyright (c) 2015 Rob Morgan
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated * documentation files (the "Software"), to
10
 * deal in the Software without restriction, including without limitation the
11
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
 * sell copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
 * IN THE SOFTWARE.
25
 *
26
 * @package    Phinx
27
 * @subpackage Phinx\Migration
28
 */
29
namespace Phinx\Migration;
30
31
use Phinx\Config\ConfigInterface;
32
use Phinx\Config\NamespaceAwareInterface;
33
use Phinx\Migration\Manager\Environment;
34
use Phinx\Seed\AbstractSeed;
35
use Phinx\Seed\SeedInterface;
36
use Phinx\Util\Util;
37
use Symfony\Component\Console\Input\InputInterface;
38
use Symfony\Component\Console\Output\OutputInterface;
39
40
class Manager
41
{
42
    const BREAKPOINT_TOGGLE = 1;
43
    const BREAKPOINT_SET = 2;
44
    const BREAKPOINT_UNSET = 3;
45
46
    /**
47
     * @var \Phinx\Config\ConfigInterface
48
     */
49
    protected $config;
50
51
    /**
52
     * @var \Symfony\Component\Console\Input\InputInterface
53
     */
54
    protected $input;
55
56
    /**
57
     * @var \Symfony\Component\Console\Output\OutputInterface
58
     */
59
    protected $output;
60
61
    /**
62
     * @var array
63
     */
64
    protected $environments;
65
66
    /**
67
     * @var array
68
     */
69
    protected $migrations;
70
71
    /**
72
     * @var array
73
     */
74
    protected $seeds;
75
76
    /**
77
     * @var integer
78
     */
79
    const EXIT_STATUS_DOWN = 3;
80
81
    /**
82
     * @var integer
83
     */
84
    const EXIT_STATUS_MISSING = 2;
85
86
    /**
87
     * Class Constructor.
88
     *
89 432
     * @param \Phinx\Config\ConfigInterface $config Configuration Object
90
     * @param \Symfony\Component\Console\Input\InputInterface $input Console Input
91 432
     * @param \Symfony\Component\Console\Output\OutputInterface $output Console Output
92 432
     */
93 432
    public function __construct(ConfigInterface $config, InputInterface $input, OutputInterface $output)
94 432
    {
95
        $this->setConfig($config);
96
        $this->setInput($input);
97
        $this->setOutput($output);
98
    }
99
100
    /**
101
     * Prints the specified environment's migration status.
102
     *
103 22
     * @param string $environment
104
     * @param null $format
105 22
     * @return int 0 if all migrations are up, or an error code
106 22
     */
107 22
    public function printStatus($environment, $format = null)
108 22
    {
109 22
        $output = $this->getOutput();
110 22
        $hasDownMigration = false;
111
        $hasMissingMigration = false;
112
        $migrations = $this->getMigrations($environment);
113 21
        $migrationCount = 0;
114
        $missingCount = 0;
115 21
        $pendingMigrationCount = 0;
116 21
        if (count($migrations)) {
117 19
            // TODO - rewrite using Symfony Table Helper as we already have this library
118 19
            // included and it will fix formatting issues (e.g drawing the lines)
119 2
            $output->writeln('');
120 1
121 1
            switch ($this->getConfig()->getVersionOrder()) {
122 1
                case \Phinx\Config\Config::VERSION_ORDER_CREATION_TIME:
123 1
                    $migrationIdAndStartedHeader = "<info>[Migration ID]</info>  Started            ";
124 21
                    break;
125
                case \Phinx\Config\Config::VERSION_ORDER_EXECUTION_TIME:
126 20
                    $migrationIdAndStartedHeader = "Migration ID    <info>[Started          ]</info>";
127 20
                    break;
128
                default:
129 20
                    throw new \RuntimeException('Invalid version_order configuration option');
130 20
            }
131
132
            $output->writeln(" Status  $migrationIdAndStartedHeader  Finished             Migration Name ");
133 17
            $output->writeln('----------------------------------------------------------------------------------');
134 20
135
            $env = $this->getEnvironment($environment);
136 20
            $versions = $env->getVersionLog();
137
138 20
            $maxNameLength = $versions ? max(array_map(function ($version) {
139
                return strlen($version['migration_name']);
140
            }, $versions)) : 0;
141 20
142
            $missingVersions = array_diff_key($versions, $migrations);
143 20
            $missingCount = count($missingVersions);
144 17
145 13
            $hasMissingMigration = !empty($missingVersions);
146 13
147 13
            // get the migrations sorted in the same way as the versions
148 20
            $sortedMigrations = [];
149
150 20
            foreach ($versions as $versionCreationTime => $version) {
151
                if (isset($migrations[$versionCreationTime])) {
152
                    array_push($sortedMigrations, $migrations[$versionCreationTime]);
153 4
                    unset($migrations[$versionCreationTime]);
154 4
                }
155
            }
156 4
157 4
            if (empty($sortedMigrations) && !empty($missingVersions)) {
158 4
                // this means we have no up migrations, so we write all the missing versions already so they show up
159
                // before any possible down migration
160
                foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
161
                    $this->printMissingVersion($missingVersion, $maxNameLength);
162 20
163 13
                    unset($missingVersions[$missingVersionCreationTime]);
164 13
                }
165
            }
166 20
167 20
            // any migration left in the migrations (ie. not unset when sorting the migrations by the version order) is
168 20
            // a migration that is down, so we add them to the end of the sorted migrations list
169
            if (!empty($migrations)) {
170 13
                $sortedMigrations = array_merge($sortedMigrations, $migrations);
171 6
            }
172 6
173 4
            $migrationCount = count($sortedMigrations);
174
            foreach ($sortedMigrations as $migration) {
175 3
                $version = array_key_exists($migration->getVersion(), $versions) ? $versions[$migration->getVersion()] : false;
176
                if ($version) {
177
                    // check if there are missing versions before this version
178
                    foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
179
                        if ($this->getConfig()->isVersionOrderCreationTime()) {
180
                            if ($missingVersion['version'] > $version['version']) {
181
                                break;
182
                            }
183
                        } else {
184 3
                            if ($missingVersion['start_time'] > $version['start_time']) {
185
                                break;
186 3
                            } elseif ($missingVersion['start_time'] == $version['start_time'] &&
187 13
                                $missingVersion['version'] > $version['version']) {
188
                                break;
189 13
                            }
190 13
                        }
191 13
192 13
                        $this->printMissingVersion($missingVersion, $maxNameLength);
193
194 20
                        unset($missingVersions[$missingVersionCreationTime]);
195
                    }
196 20
197 20
                    $status = '     <info>up</info> ';
198 20
                } else {
199 20
                    $pendingMigrationCount++;
200 20
                    $hasDownMigration = true;
201 20
                    $status = '   <error>down</error> ';
202 20
                }
203 20
                $maxNameLength = max($maxNameLength, strlen($migration->getName()));
204
205 20
                $output->writeln(sprintf(
206 1
                    '%s %14.0f  %19s  %19s  <comment>%s</comment>',
207 1
                    $status,
208
                    $migration->getVersion(),
209 20
                    $version['start_time'],
210 20
                    $version['end_time'],
211 20
                    $migration->getName()
212
                ));
213
214 20
                if ($version && $version['breakpoint']) {
215 4
                    $output->writeln('         <error>BREAKPOINT SET</error>');
216
                }
217 4
218 20
                $migrations[] = ['migration_status' => trim(strip_tags($status)), 'migration_id' => sprintf('%14.0f', $migration->getVersion()), 'migration_name' => $migration->getName()];
219 20
                unset($versions[$migration->getVersion()]);
220
            }
221 1
222 1
            // and finally add any possibly-remaining missing migrations
223
            foreach ($missingVersions as $missingVersionCreationTime => $missingVersion) {
224
                $this->printMissingVersion($missingVersion, $maxNameLength);
225
226 21
                unset($missingVersions[$missingVersionCreationTime]);
227 21
            }
228
        } else {
229
            // there are no migrations
230
            $output->writeln('');
231
            $output->writeln('There are no available migrations. Try creating one using the <info>create</info> command.');
232
        }
233
234
        // write an empty line
235
        $output->writeln('');
236
        if ($format !== null) {
237
            switch ($format) {
238
                case 'json':
239
                    $output->writeln(json_encode(
240
                        [
241
                            'pending_count' => $pendingMigrationCount,
242 21
                            'missing_count' => $missingCount,
243 10
                            'total_count' => $migrationCount + $missingCount,
244 11
                            'migrations' => $migrations
245 6
                        ]
246
                    ));
247 5
                    break;
248
                default:
249
                    $output->writeln('<info>Unsupported format: ' . $format . '</info>');
250
            }
251
        }
252
253
        if ($hasMissingMigration) {
254
            return self::EXIT_STATUS_MISSING;
255
        } elseif ($hasDownMigration) {
256
            return self::EXIT_STATUS_DOWN;
257 10
        } else {
258
            return 0;
259 10
        }
260 10
    }
261 10
262 10
    /**
263 10
     * Print Missing Version
264 10
     *
265 10
     * @param array $version        The missing version to print (in the format returned by Environment.getVersionLog).
266
     * @param int   $maxNameLength  The maximum migration name length.
267 10
     */
268 1
    private function printMissingVersion($version, $maxNameLength)
269 1
    {
270 10
        $this->getOutput()->writeln(sprintf(
271
            '     <error>up</error>  %14.0f  %19s  %19s  <comment>%s</comment>  <error>** MISSING **</error>',
272
            $version['version'],
273
            $version['start_time'],
274
            $version['end_time'],
275
            str_pad($version['migration_name'], $maxNameLength, ' ')
276
        ));
277
278
        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...
279
            $this->getOutput()->writeln('         <error>BREAKPOINT SET</error>');
280 4
        }
281
    }
282 4
283 4
    /**
284
     * Migrate to the version of the database on a given date.
285
     *
286 4
     * @param string    $environment Environment
287 4
     * @param \DateTime $dateTime    Date to migrate to
288
     * @param bool      $fake        flag that if true, we just record running the migration, but not actually do the
289 4
     *                               migration
290 3
     *
291 3
     * @return void
292 3
     */
293 3
    public function migrateToDateTime($environment, \DateTime $dateTime, $fake = false)
294 4
    {
295
        $versions = array_keys($this->getMigrations($environment));
296
        $dateString = $dateTime->format('YmdHis');
297
298
        $outstandingMigrations = array_filter($versions, function ($version) use ($dateString) {
299
            return $version <= $dateString;
300
        });
301
302
        if (count($outstandingMigrations) > 0) {
303 8
            $migration = max($outstandingMigrations);
304
            $this->getOutput()->writeln('Migrating to version ' . $migration);
305 8
            $this->migrate($environment, $migration, $fake);
306 8
        }
307 8
    }
308 8
309
    /**
310 8
     * Migrate an environment to the specified version.
311
     *
312
     * @param string $environment Environment
313
     * @param int    $version     version to migrate to
314 8
     * @param bool   $fake        flag that if true, we just record running the migration, but not actually do the migration
315 5
     * @return void
316 5
     */
317 3
    public function migrate($environment, $version = null, $fake = false)
318
    {
319
        $migrations = $this->getMigrations($environment);
320
        $env = $this->getEnvironment($environment);
321
        $versions = $env->getVersions();
322
        $current = $env->getCurrentVersion();
323
324
        if (empty($versions) && empty($migrations)) {
325
            return;
326
        }
327 8
328
        if ($version === null) {
329 8
            $version = max(array_merge($versions, array_keys($migrations)));
330 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
331
            if (0 != $version && !isset($migrations[$version])) {
332
                $this->output->writeln(sprintf(
333
                    '<comment>warning</comment> %s is not a valid version',
334
                    $version
335
                ));
336
337
                return;
338
            }
339
        }
340
341
        // are we migrating up or down?
342
        $direction = $version > $current ? MigrationInterface::UP : MigrationInterface::DOWN;
343 8
344 8
        if ($direction === MigrationInterface::DOWN) {
345 8
            // run downs first
346 2
            krsort($migrations);
347 View Code Duplication
            foreach ($migrations as $migration) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
348
                if ($migration->getVersion() <= $version) {
349 8
                    break;
350 5
                }
351 5
352 8
                if (in_array($migration->getVersion(), $versions)) {
353 8
                    $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake);
354
                }
355
            }
356
        }
357
358
        ksort($migrations);
359 View Code Duplication
        foreach ($migrations as $migration) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
360
            if ($migration->getVersion() > $version) {
361
                break;
362
            }
363 119
364
            if (!in_array($migration->getVersion(), $versions)) {
365 119
                $this->executeMigration($environment, $migration, MigrationInterface::UP, $fake);
366 119
            }
367
        }
368 119
    }
369 119
370 119
    /**
371
     * Execute a migration against the specified environment.
372
     *
373 119
     * @param string $name Environment Name
374 119
     * @param \Phinx\Migration\MigrationInterface $migration Migration
375 119
     * @param string $direction Direction
376
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration
377 119
     * @return void
378
     */
379 119
    public function executeMigration($name, MigrationInterface $migration, $direction = MigrationInterface::UP, $fake = false)
380 119
    {
381 119
        $this->getOutput()->writeln('');
382 119
        $this->getOutput()->writeln(
383 119
            ' ==' .
384
            ' <info>' . $migration->getVersion() . ' ' . $migration->getName() . ':</info>' .
385
            ' <comment>' . ($direction === MigrationInterface::UP ? 'migrating' : 'reverting') . '</comment>'
386
        );
387
        $migration->preFlightCheck($direction);
388
389
        // Execute the migration and log the time elapsed.
390
        $start = microtime(true);
391
        $this->getEnvironment($name)->executeMigration($migration, $direction, $fake);
392 6
        $end = microtime(true);
393
394 6
        $migration->postFlightCheck($direction);
395 6
        $this->getOutput()->writeln(
396
            ' ==' .
397 6
            ' <info>' . $migration->getVersion() . ' ' . $migration->getName() . ':</info>' .
398 6
            ' <comment>' . ($direction === MigrationInterface::UP ? 'migrated' : 'reverted') .
399 6
            ' ' . sprintf('%.4fs', $end - $start) . '</comment>'
400
        );
401
    }
402 6
403 6
    /**
404 6
     * Execute a seeder against the specified environment.
405
     *
406 6
     * @param string $name Environment Name
407
     * @param \Phinx\Seed\SeedInterface $seed Seed
408 6
     * @return void
409 6
     */
410 6
    public function executeSeed($name, SeedInterface $seed)
411 6
    {
412 6
        $this->getOutput()->writeln('');
413
        $this->getOutput()->writeln(
414
            ' ==' .
415
            ' <info>' . $seed->getName() . ':</info>' .
416
            ' <comment>seeding</comment>'
417
        );
418
419
        // Execute the seeder and log the time elapsed.
420
        $start = microtime(true);
421
        $this->getEnvironment($name)->executeSeed($seed);
422
        $end = microtime(true);
423 349
424
        $this->getOutput()->writeln(
425
            ' ==' .
426 349
            ' <info>' . $seed->getName() . ':</info>' .
427
            ' <comment>seeded' .
428
            ' ' . sprintf('%.4fs', $end - $start) . '</comment>'
429 349
        );
430
    }
431
432 349
    /**
433
     * Rollback an environment to the specified version.
434 349
     *
435
     * @param string $environment Environment
436
     * @param int|string $target
437 349
     * @param bool $force
438 48
     * @param bool $targetMustMatchVersion
439 48
     * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration
440 48
     * @return void
441
     */
442 349
    public function rollback($environment, $target = null, $force = false, $targetMustMatchVersion = true, $fake = false)
443 349
    {
444 349
        // note that the migrations are indexed by name (aka creation time) in ascending order
445
        $migrations = $this->getMigrations($environment);
446
447 47
        // note that the version log are also indexed by name with the proper ascending order according to the version order
448
        $executedVersions = $this->getEnvironment($environment)->getVersionLog();
449 349
450
        // get a list of migrations sorted in the opposite way of the executed versions
451 349
        $sortedMigrations = [];
452 23
453 349
        foreach ($executedVersions as $versionCreationTime => &$executedVersion) {
454
            // if we have a date (ie. the target must not match a version) and we are sorting by execution time, we
455 20
            // convert the version start time so we can compare directly with the target date
456 20
            if (!$this->getConfig()->isVersionOrderCreationTime() && !$targetMustMatchVersion) {
457 20
                $dateTime = \DateTime::createFromFormat('Y-m-d H:i:s', $executedVersion['start_time']);
458 20
                $executedVersion['start_time'] = $dateTime->format('YmdHis');
459
            }
460
461 20
            if (isset($migrations[$versionCreationTime])) {
462 20
                array_unshift($sortedMigrations, $migrations[$versionCreationTime]);
463 20
            } else {
464
                // this means the version is missing so we unset it so that we don't consider it when rolling back
465
                // migrations (or choosing the last up version as target)
466
                unset($executedVersions[$versionCreationTime]);
467 20
            }
468
        }
469
470 349
        if ($target === 'all' || $target === '0') {
471 349
            $target = 0;
472 53
        } elseif (!is_numeric($target) && !is_null($target)) { // try to find a target version based on name
473 53
            // search through the migrations using the name
474
            $migrationNames = array_map(function ($item) {
475
                return $item['migration_name'];
476
            }, $executedVersions);
477 296
            $found = array_search($target, $migrationNames);
478
479 94
            // check on was found
480 94
            if ($found !== false) {
481 94
                $target = (string)$found;
482
            } else {
483
                $this->getOutput()->writeln("<error>No migration found with name ($target)</error>");
484 296
485 46
                return;
486 46
            }
487
        }
488
489
        // Check we have at least 1 migration to revert
490 250
        $executedVersionCreationTimes = array_keys($executedVersions);
491
        if (empty($executedVersionCreationTimes) || $target == end($executedVersionCreationTimes)) {
492 250
            $this->getOutput()->writeln('<error>No migrations to rollback</error>');
493 250
494 70
            return;
495
        }
496
497 250
        // If no target was supplied, revert the last migration
498 250
        if ($target === null) {
499
            // Get the migration before the last run migration
500 250
            $prev = count($executedVersionCreationTimes) - 2;
501 96
            $target = $prev >= 0 ? $executedVersionCreationTimes[$prev] : 0;
502 96
        }
503 42
504
        // If the target must match a version, check the target version exists
505 68
        if ($targetMustMatchVersion && 0 !== $target && !isset($migrations[$target])) {
506
            $this->getOutput()->writeln("<error>Target version ($target) not found</error>");
507 222
508 121
            return;
509 121
        }
510
511 117
        // Rollback all versions until we find the wanted rollback target
512 117
        $rollbacked = false;
513 117
514 250
        foreach ($sortedMigrations as $migration) {
515
            if ($targetMustMatchVersion && $migration->getVersion() == $target) {
516 250
                break;
517 133
            }
518 133
519 250
            if (in_array($migration->getVersion(), $executedVersionCreationTimes)) {
520
                $executedVersion = $executedVersions[$migration->getVersion()];
521
522
                if (!$targetMustMatchVersion) {
523
                    if (($this->getConfig()->isVersionOrderCreationTime() && $executedVersion['version'] <= $target) ||
524
                        (!$this->getConfig()->isVersionOrderCreationTime() && $executedVersion['start_time'] <= $target)) {
525
                        break;
526
                    }
527
                }
528 9
529
                if (0 != $executedVersion['breakpoint'] && !$force) {
530 9
                    $this->getOutput()->writeln('<error>Breakpoint reached. Further rollbacks inhibited.</error>');
531
                    break;
532 9
                }
533
                $this->executeMigration($environment, $migration, MigrationInterface::DOWN, $fake);
534 3
                $rollbacked = true;
535 3
            }
536 3
        }
537 3
538 3
        if (!$rollbacked) {
539 3
            $this->getOutput()->writeln('<error>No migrations to rollback</error>');
540
        }
541 6
    }
542 3
543 3
    /**
544 3
     * Run database seeders against an environment.
545
     *
546
     * @param string $environment Environment
547 6
     * @param string $seed Seeder
548
     * @return void
549
     */
550
    public function seed($environment, $seed = null)
551
    {
552
        $seeds = $this->getSeeds();
553
554
        if ($seed === null) {
555 381
            // run all seeders
556
            foreach ($seeds as $seeder) {
557 381
                if (array_key_exists($seeder->getName(), $seeds)) {
558 381
                    $this->executeSeed($environment, $seeder);
559
                }
560
            }
561
        } else {
562
            // run only one seeder
563
            if (array_key_exists($seed, $seeds)) {
564
                $this->executeSeed($environment, $seeds[$seed]);
565
            } else {
566
                throw new \InvalidArgumentException(sprintf('The seed class "%s" does not exist', $seed));
567
            }
568 382
        }
569
    }
570 382
571 380
    /**
572
     * Sets the environments.
573
     *
574
     * @param array $environments Environments
575 7
     * @return \Phinx\Migration\Manager
576 1
     */
577 1
    public function setEnvironments($environments = [])
578
    {
579 1
        $this->environments = $environments;
580
581
        return $this;
582
    }
583 6
584 6
    /**
585
     * Gets the manager class for the given environment.
586 6
     *
587 6
     * @param string $name Environment Name
588 6
     * @throws \InvalidArgumentException
589 6
     * @return \Phinx\Migration\Manager\Environment
590
     */
591 6
    public function getEnvironment($name)
592
    {
593
        if (isset($this->environments[$name])) {
594
            return $this->environments[$name];
595
        }
596
597
        // check the environment exists
598
        if (!$this->getConfig()->hasEnvironment($name)) {
599
            throw new \InvalidArgumentException(sprintf(
600 400
                'The environment "%s" does not exist',
601
                $name
602 400
            ));
603 400
        }
604
605
        // create an environment instance and cache it
606
        $envOptions = $this->getConfig()->getEnvironment($name);
607
        $envOptions['version_order'] = $this->getConfig()->getVersionOrder();
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 console input.
619
     *
620
     * @param \Symfony\Component\Console\Input\InputInterface $input Input
621
     * @return \Phinx\Migration\Manager
622 400
     */
623
    public function setInput(InputInterface $input)
624 400
    {
625 400
        $this->input = $input;
626
627
        return $this;
628
    }
629
630
    /**
631
     * Gets the console input.
632
     *
633 395
     * @return \Symfony\Component\Console\Input\InputInterface
634
     */
635 395
    public function getInput()
636
    {
637
        return $this->input;
638
    }
639
640
    /**
641
     * Sets the console output.
642
     *
643
     * @param \Symfony\Component\Console\Output\OutputInterface $output Output
644 379
     * @return \Phinx\Migration\Manager
645
     */
646 379
    public function setOutput(OutputInterface $output)
647 379
    {
648
        $this->output = $output;
649
650
        return $this;
651
    }
652
653
    /**
654
     * Gets the console output.
655
     *
656
     * @return \Symfony\Component\Console\Output\OutputInterface
657 388
     */
658
    public function getOutput()
659 388
    {
660 388
        return $this->output;
661
    }
662
663 388
    /**
664
     * Sets the database migrations.
665 388
     *
666
     * @param array $migrations Migrations
667 388
     * @return \Phinx\Migration\Manager
668 387
     */
669 387
    public function setMigrations(array $migrations)
670
    {
671 387
        $this->migrations = $migrations;
672 3
673
        return $this;
674
    }
675 387
676 387
    /**
677
     * Gets an array of the database migrations, indexed by migration name (aka creation time) and sorted in ascending
678
     * order
679 387
     *
680
     * @param string $environment Environment
681 387
     * @throws \InvalidArgumentException
682 2
     * @return \Phinx\Migration\AbstractMigration[]
683 2
     */
684 2
    public function getMigrations($environment)
685 2
    {
686 2
        if ($this->migrations === null) {
687
            $phpFiles = $this->getMigrationFiles();
688
689 387
            if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
690
                $this->getOutput()->writeln('Migration file');
691
                $this->getOutput()->writeln(
692
                    array_map(
0 ignored issues
show
Documentation introduced by
array_map(function ($php...</info>"; }, $phpFiles) is of type array, but the function expects a string|object<Symfony\Co...onsole\Output\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
693 387
                        function ($phpFile) {
694 387
                            return "    <info>{$phpFile}</info>";
695 2
                        },
696 2
                        $phpFiles
697 2
                    )
698
                );
699 2
            }
700
701
            // filter the files to only get the ones that match our naming scheme
702
            $fileNames = [];
703 385
            /** @var \Phinx\Migration\AbstractMigration[] $versions */
704
            $versions = [];
705 385
706 2
            foreach ($phpFiles as $filePath) {
707 2
                if (Util::isValidMigrationFileName(basename($filePath))) {
708 2
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
709
                        $this->getOutput()->writeln("Valid migration file <info>{$filePath}</info>.");
710 2
                    }
711
712
                    $version = Util::getVersionFromFileName(basename($filePath));
713 383
714 383
                    if (isset($versions[$version])) {
715 384
                        throw new \InvalidArgumentException(sprintf('Duplicate migration - "%s" has the same version as "%s"', $filePath, $versions[$version]->getVersion()));
716
                    }
717 379
718 379
                    $config = $this->getConfig();
719 379
                    $namespace = $config instanceof NamespaceAwareInterface ? $config->getMigrationNamespaceByPath(dirname($filePath)) : null;
720
721 379
                    // convert the filename to a class name
722
                    $class = ($namespace === null ? '' : $namespace . '\\') . Util::mapFileNameToClassName(basename($filePath));
723
724
                    if (isset($fileNames[$class])) {
725
                        throw new \InvalidArgumentException(sprintf(
726
                            'Migration "%s" has the same name as "%s"',
727
                            basename($filePath),
728
                            $fileNames[$class]
729 388
                        ));
730
                    }
731 388
732 388
                    $fileNames[$class] = basename($filePath);
733 388
734
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
735 388
                        $this->getOutput()->writeln("Loading class <info>$class</info> from <info>$filePath</info>.");
736 388
                    }
737 388
738 388
                    // load the migration file
739 388
                    $orig_display_errors_setting = ini_get('display_errors');
740 388
                    ini_set('display_errors', 'On');
741
                    /** @noinspection PhpIncludeInspection */
742 388
                    require_once $filePath;
743
                    ini_set('display_errors', $orig_display_errors_setting);
744
                    if (!class_exists($class)) {
745
                        throw new \InvalidArgumentException(sprintf(
746
                            'Could not find class "%s" in file "%s"',
747
                            $class,
748
                            $filePath
749
                        ));
750
                    }
751 11
752
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
753 11
                        $this->getOutput()->writeln("Running <info>$class</info>.");
754 11
                    }
755
756
                    // instantiate it
757
                    $migration = new $class($environment, $version, $this->getInput(), $this->getOutput());
758
759
                    if (!($migration instanceof AbstractMigration)) {
760
                        throw new \InvalidArgumentException(sprintf(
761
                            'The class "%s" in file "%s" must extend \Phinx\Migration\AbstractMigration',
762
                            $class,
763 11
                            $filePath
764
                        ));
765 11
                    }
766 11
767
                    $versions[$version] = $migration;
768
                } else {
769 11
                    if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG) {
770
                        $this->getOutput()->writeln("Invalid migration file <error>{$filePath}</error>.");
771 11
                    }
772
                }
773 11
            }
774 11
775 11
            ksort($versions);
776 11
            $this->setMigrations($versions);
777
        }
778
779 11
        return $this->migrations;
780 11
    }
781
782
    /**
783
     * Returns a list of migration files found in the provided migration paths.
784 11
     *
785 11
     * @return string[]
786
     */
787 View Code Duplication
    protected function getMigrationFiles()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
788
    {
789
        $config = $this->getConfig();
790
        $paths = $config->getMigrationPaths();
791
        $files = [];
792
793
        foreach ($paths as $path) {
794 11
            $files = array_merge(
795
                $files,
796 11
                Util::glob($path . DIRECTORY_SEPARATOR . '*.php')
797
            );
798
        }
799
        // glob() can return the same file multiple times
800
        // This will cause the migration to fail with a
801
        // false assumption of duplicate migrations
802
        // http://php.net/manual/en/function.glob.php#110340
803
        $files = array_unique($files);
804 11
805 11
        return $files;
806 11
    }
807
808 11
    /**
809 11
     * Sets the database seeders.
810 11
     *
811
     * @param array $seeds Seeders
812 11
     * @return \Phinx\Migration\Manager
813
     */
814
    public function setSeeds(array $seeds)
815
    {
816
        $this->seeds = $seeds;
817
818
        return $this;
819
    }
820 11
821
    /**
822 11
     * Get seed dependencies instances from seed dependency array
823 11
     *
824 11
     * @param AbstractSeed $seed Seed
825
     *
826 11
     * @return AbstractSeed[]
827 11
     */
828 11
    private function getSeedDependenciesInstances(AbstractSeed $seed)
829 11
    {
830 11
        $dependenciesInstances = [];
831 11
        $dependencies = $seed->getDependencies();
832
        if (!empty($dependencies)) {
833 11
            foreach ($dependencies as $dependency) {
834
                foreach ($this->seeds as $seed) {
835
                    if (get_class($seed) === $dependency) {
836
                        $dependenciesInstances[get_class($seed)] = $seed;
837
                    }
838
                }
839
            }
840
        }
841
842 400
        return $dependenciesInstances;
843
    }
844 400
845 400
    /**
846
     * Order seeds by dependencies
847
     *
848
     * @param AbstractSeed[] $seeds Seeds
849
     *
850
     * @return AbstractSeed[]
851
     */
852
    private function orderSeedsByDependencies(array $seeds)
853 399
    {
854
        $orderedSeeds = [];
855 399
        foreach ($seeds as $seed) {
856
            $key = get_class($seed);
857
            $dependencies = $this->getSeedDependenciesInstances($seed);
858
            if (!empty($dependencies)) {
859
                $orderedSeeds[$key] = $seed;
860
                $orderedSeeds = array_merge($this->orderSeedsByDependencies($dependencies), $orderedSeeds);
861
            } else {
862
                $orderedSeeds[$key] = $seed;
863
            }
864
        }
865 2
866
        return $orderedSeeds;
867 2
    }
868 2
869 2
    /**
870 2
     * Gets an array of database seeders.
871
     *
872 2
     * @throws \InvalidArgumentException
873
     * @return \Phinx\Seed\AbstractSeed[]
874
     */
875
    public function getSeeds()
876 2
    {
877 1
        if ($this->seeds === null) {
878 1
            $phpFiles = $this->getSeedFiles();
879 1
880
            // filter the files to only get the ones that match our naming scheme
881 2
            $fileNames = [];
882 1
            /** @var \Phinx\Seed\AbstractSeed[] $seeds */
883 1
            $seeds = [];
884
885 1
            foreach ($phpFiles as $filePath) {
886 1
                if (Util::isValidSeedFileName(basename($filePath))) {
887
                    $config = $this->getConfig();
888
                    $namespace = $config instanceof NamespaceAwareInterface ? $config->getSeedNamespaceByPath(dirname($filePath)) : null;
889 1
890
                    // convert the filename to a class name
891 1
                    $class = ($namespace === null ? '' : $namespace . '\\') . pathinfo($filePath, PATHINFO_FILENAME);
892
                    $fileNames[$class] = basename($filePath);
893 1
894 1
                    // load the seed file
895 1
                    /** @noinspection PhpIncludeInspection */
896 1
                    require_once $filePath;
897 1
                    if (!class_exists($class)) {
898 1
                        throw new \InvalidArgumentException(sprintf(
899
                            'Could not find class "%s" in file "%s"',
900
                            $class,
901
                            $filePath
902
                        ));
903
                    }
904
905
                    // instantiate it
906 1
                    $seed = new $class($this->getInput(), $this->getOutput());
907
908 1
                    if (!($seed instanceof AbstractSeed)) {
909 1
                        throw new \InvalidArgumentException(sprintf(
910 1
                            'The class "%s" in file "%s" must extend \Phinx\Seed\AbstractSeed',
911 1
                            $class,
912 1
                            $filePath
913
                        ));
914
                    }
915
916
                    $seeds[$class] = $seed;
917
                }
918
            }
919
920
            ksort($seeds);
921
            $this->setSeeds($seeds);
922
        }
923
924
        $this->seeds = $this->orderSeedsByDependencies($this->seeds);
925
926
        return $this->seeds;
927
    }
928
929
    /**
930
     * Returns a list of seed files found in the provided seed paths.
931
     *
932
     * @return string[]
933
     */
934 View Code Duplication
    protected function getSeedFiles()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
935
    {
936
        $config = $this->getConfig();
937
        $paths = $config->getSeedPaths();
938
        $files = [];
939
940
        foreach ($paths as $path) {
941
            $files = array_merge(
942
                $files,
943
                Util::glob($path . DIRECTORY_SEPARATOR . '*.php')
944
            );
945
        }
946
        // glob() can return the same file multiple times
947
        // This will cause the migration to fail with a
948
        // false assumption of duplicate migrations
949
        // http://php.net/manual/en/function.glob.php#110340
950
        $files = array_unique($files);
951
952
        return $files;
953
    }
954
955
    /**
956
     * Sets the config.
957
     *
958
     * @param  \Phinx\Config\ConfigInterface $config Configuration Object
959
     * @return \Phinx\Migration\Manager
960
     */
961
    public function setConfig(ConfigInterface $config)
962
    {
963
        $this->config = $config;
964
965
        return $this;
966
    }
967
968
    /**
969
     * Gets the config.
970
     *
971
     * @return \Phinx\Config\ConfigInterface
972
     */
973
    public function getConfig()
974
    {
975
        return $this->config;
976
    }
977
978
    /**
979
     * Toggles the breakpoint for a specific version.
980
     *
981
     * @param string $environment
982
     * @param int|null $version
983
     * @return void
984
     */
985
    public function toggleBreakpoint($environment, $version)
986
    {
987
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_TOGGLE);
988
    }
989
990
    /**
991
     * Toggles the breakpoint for a specific version.
992
     *
993
     * @param string $environment The required environment
994
     * @param int|null $version The version of the target migration
995
     * @param int $mark The state of the breakpoint as defined by self::BREAKPOINT_xxxx constants.
996
     *
997
     * @return void
998
     */
999
    protected function markBreakpoint($environment, $version, $mark)
1000
    {
1001
        $migrations = $this->getMigrations($environment);
1002
        $this->getMigrations($environment);
1003
        $env = $this->getEnvironment($environment);
1004
        $versions = $env->getVersionLog();
1005
1006
        if (empty($versions) || empty($migrations)) {
1007
            return;
1008
        }
1009
1010
        if ($version === null) {
1011
            $lastVersion = end($versions);
1012
            $version = $lastVersion['version'];
1013
        }
1014
1015 View Code Duplication
        if (0 != $version && !isset($migrations[$version])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1016
            $this->output->writeln(sprintf(
1017
                '<comment>warning</comment> %s is not a valid version',
1018
                $version
1019
            ));
1020
1021
            return;
1022
        }
1023
1024
        switch ($mark) {
1025
            case self::BREAKPOINT_TOGGLE:
1026
                $env->getAdapter()->toggleBreakpoint($migrations[$version]);
1027
                break;
1028 View Code Duplication
            case self::BREAKPOINT_SET:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1029
                if ($versions[$version]['breakpoint'] == 0) {
1030
                    $env->getAdapter()->setBreakpoint($migrations[$version]);
1031
                }
1032
                break;
1033 View Code Duplication
            case self::BREAKPOINT_UNSET:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1034
                if ($versions[$version]['breakpoint'] == 1) {
1035
                    $env->getAdapter()->unsetBreakpoint($migrations[$version]);
1036
                }
1037
                break;
1038
        }
1039
1040
        $versions = $env->getVersionLog();
1041
1042
        $this->getOutput()->writeln(
1043
            ' Breakpoint ' . ($versions[$version]['breakpoint'] ? 'set' : 'cleared') .
1044
            ' for <info>' . $version . '</info>' .
1045
            ' <comment>' . $migrations[$version]->getName() . '</comment>'
1046
        );
1047
    }
1048
1049
    /**
1050
     * Remove all breakpoints
1051
     *
1052
     * @param string $environment The required environment
1053
     * @return void
1054
     */
1055
    public function removeBreakpoints($environment)
1056
    {
1057
        $this->getOutput()->writeln(sprintf(
1058
            ' %d breakpoints cleared.',
1059
            $this->getEnvironment($environment)->getAdapter()->resetAllBreakpoints()
1060
        ));
1061
    }
1062
1063
    /**
1064
     * Set the breakpoint for a specific version.
1065
     *
1066
     * @param string $environment The required environment
1067
     * @param int|null $version The version of the target migration
1068
     *
1069
     * @return void
1070
     */
1071
    public function setBreakpoint($environment, $version)
1072
    {
1073
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_SET);
1074
    }
1075
1076
    /**
1077
     * Unset the breakpoint for a specific version.
1078
     *
1079
     * @param string $environment The required environment
1080
     * @param int|null $version The version of the target migration
1081
     *
1082
     * @return void
1083
     */
1084
    public function unsetBreakpoint($environment, $version)
1085
    {
1086
        $this->markBreakpoint($environment, $version, self::BREAKPOINT_UNSET);
1087
    }
1088
}
1089