Passed
Pull Request — master (#878)
by Asmir
02:42
created

MigrateCommand   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 219
Duplicated Lines 0 %

Test Coverage

Coverage 97.3%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 143
c 1
b 0
f 0
dl 0
loc 219
ccs 108
cts 111
cp 0.973
rs 10
wmc 23

4 Methods

Rating   Name   Duplication   Size   Complexity  
B configure() 0 84 1
C execute() 0 70 12
A getVersionNameFromAlias() 0 23 5
A checkExecutedUnavailableMigrations() 0 31 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Migrations\Tools\Console\Command;
6
7
use Doctrine\Migrations\Exception\NoMigrationsFoundWithCriteria;
8
use Doctrine\Migrations\Exception\UnknownMigrationVersion;
9
use Doctrine\Migrations\Metadata\ExecutedMigrationsSet;
10
use Symfony\Component\Console\Formatter\OutputFormatter;
11
use Symfony\Component\Console\Input\InputArgument;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Output\OutputInterface;
15
use function count;
16
use function getcwd;
17
use function is_string;
18
use function is_writable;
19
use function sprintf;
20
use function substr;
21
22
/**
23
 * The MigrateCommand class is responsible for executing a migration from the current version to another
24
 * version up or down. It will calculate all the migration versions that need to be executed and execute them.
25
 */
26
class MigrateCommand extends DoctrineCommand
27
{
28
    /** @var string */
29
    protected static $defaultName = 'migrations:migrate';
30
31 12
    protected function configure() : void
32
    {
33
        $this
34 12
            ->setAliases(['migrate'])
35 12
            ->setDescription(
36 12
                'Execute a migration to a specified version or the latest available version.'
37
            )
38 12
            ->addArgument(
39 12
                'version',
40 12
                InputArgument::OPTIONAL,
41 12
                'The version FQCN or alias (first, prev, next, latest) to migrate to.',
42 12
                'latest'
43
            )
44 12
            ->addOption(
45 12
                'write-sql',
46 12
                null,
47 12
                InputOption::VALUE_OPTIONAL,
48 12
                'The path to output the migration SQL file instead of executing it. Defaults to current working directory.',
49 12
                false
50
            )
51 12
            ->addOption(
52 12
                'dry-run',
53 12
                null,
54 12
                InputOption::VALUE_NONE,
55 12
                'Execute the migration as a dry run.'
56
            )
57 12
            ->addOption(
58 12
                'query-time',
59 12
                null,
60 12
                InputOption::VALUE_NONE,
61 12
                'Time all the queries individually.'
62
            )
63 12
            ->addOption(
64 12
                'allow-no-migration',
65 12
                null,
66 12
                InputOption::VALUE_NONE,
67 12
                'Do not throw an exception if no migration is available.'
68
            )
69 12
            ->addOption(
70 12
                'all-or-nothing',
71 12
                null,
72 12
                InputOption::VALUE_OPTIONAL,
73 12
                'Wrap the entire migration in a transaction.',
74 12
                false
75
            )
76 12
            ->setHelp(<<<EOT
77 12
The <info>%command.name%</info> command executes a migration to a specified version or the latest available version:
78
79
    <info>%command.full_name%</info>
80
81
You can optionally manually specify the version you wish to migrate to:
82
83
    <info>%command.full_name% FQCN</info>
84
85
You can specify the version you wish to migrate to using an alias:
86
87
    <info>%command.full_name% prev</info>
88
    <info>These alias are defined : first, latest, prev, current and next</info>
89
90
You can specify the version you wish to migrate to using an number against the current version:
91
92
    <info>%command.full_name% current+3</info>
93
94
You can also execute the migration as a <comment>--dry-run</comment>:
95
96
    <info>%command.full_name% FQCN --dry-run</info>
97
98
You can output the would be executed SQL statements to a file with <comment>--write-sql</comment>:
99
100
    <info>%command.full_name% FQCN --write-sql</info>
101
102
Or you can also execute the migration without a warning message which you need to interact with:
103
104
    <info>%command.full_name% --no-interaction</info>
105
106
You can also time all the different queries if you wanna know which one is taking so long:
107
108
    <info>%command.full_name% --query-time</info>
109
110
Use the --all-or-nothing option to wrap the entire migration in a transaction.
111
EOT
112
            );
113
114 12
        parent::configure();
115 12
    }
116
117 12
    public function execute(InputInterface $input, OutputInterface $output) : ?int
118
    {
119 12
        $this->outputHeader($output);
120 12
        $allowNoMigration = $input->getOption('allow-no-migration');
121 12
        $versionAlias     = $input->getArgument('version');
122 12
        $path             = $input->getOption('write-sql');
123
124
        try {
125 12
            $version = $this->getDependencyFactory()->getVersionAliasResolver()->resolveVersionAlias($versionAlias);
0 ignored issues
show
Bug introduced by
It seems like $versionAlias can also be of type null and string[]; however, parameter $alias of Doctrine\Migrations\Vers...::resolveVersionAlias() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

125
            $version = $this->getDependencyFactory()->getVersionAliasResolver()->resolveVersionAlias(/** @scrutinizer ignore-type */ $versionAlias);
Loading history...
126 2
        } catch (UnknownMigrationVersion|NoMigrationsFoundWithCriteria $e) {
127 2
            $this->getVersionNameFromAlias($versionAlias, $output);
0 ignored issues
show
Bug introduced by
It seems like $versionAlias can also be of type null and string[]; however, parameter $versionAlias of Doctrine\Migrations\Tool...tVersionNameFromAlias() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

127
            $this->getVersionNameFromAlias(/** @scrutinizer ignore-type */ $versionAlias, $output);
Loading history...
128
129 2
            return 1;
130
        }
131
132 10
        $planCalculator                = $this->getDependencyFactory()->getMigrationPlanCalculator();
133 10
        $statusCalculator              = $this->getDependencyFactory()->getMigrationStatusCalculator();
134 10
        $executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
135
136 10
        if ($this->checkExecutedUnavailableMigrations($executedUnavailableMigrations, $input, $output) === false) {
137 1
            return 3;
138
        }
139
140 9
        $migratorConfigurationFactory = $this->getDependencyFactory()->getConsoleInputMigratorConfigurationFactory();
141 9
        $migratorConfiguration        = $migratorConfigurationFactory->getMigratorConfiguration($input);
142
143 9
        $plan = $planCalculator->getPlanUntilVersion($version);
144
145 9
        if (count($plan) === 0 && ! $allowNoMigration) {
146 1
            $output->writeln('Could not find any migrations to execute.');
147
148 1
            return 1;
149
        }
150
151 8
        if (count($plan) === 0) {
152 2
            $this->getVersionNameFromAlias($versionAlias, $output);
153
154 2
            return 0;
155
        }
156
157 6
        $migrator = $this->getDependencyFactory()->getMigrator();
158 6
        if ($path !== false) {
159 2
            $migratorConfiguration->setDryRun(true);
160 2
            $sql = $migrator->migrate($plan, $migratorConfiguration);
161
162 2
            $path = is_string($path) ? $path : getcwd();
163
164 2
            if (! is_string($path) || ! is_writable($path)) {
0 ignored issues
show
introduced by
The condition is_string($path) is always true.
Loading history...
165
                $output->writeln('<error>Path not writeable!</error>');
166
167
                return 1;
168
            }
169 2
            $writer = $this->getDependencyFactory()->getQueryWriter();
170 2
            $writer->write($path, $plan->getDirection(), $sql);
171
172 2
            return 0;
173
        }
174
175 4
        $question = 'WARNING! You are about to execute a database migration that could result in schema changes and data loss. Are you sure you wish to continue? (y/n)';
176
177 4
        if (! $migratorConfiguration->isDryRun() && ! $this->canExecute($question, $input, $output)) {
178 2
            $output->writeln('<error>Migration cancelled!</error>');
179
180 2
            return 3;
181
        }
182
183 2
        $this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
184 2
        $migrator->migrate($plan, $migratorConfiguration);
185
186 2
        return 0;
187
    }
188
189 10
    private function checkExecutedUnavailableMigrations(
190
        ExecutedMigrationsSet $executedUnavailableMigrations,
191
        InputInterface $input,
192
        OutputInterface $output
193
    ) : bool {
194 10
        if (count($executedUnavailableMigrations) !== 0) {
195 2
            $output->writeln(sprintf(
196 2
                '<error>WARNING! You have %s previously executed migrations in the database that are not registered migrations.</error>',
197 2
                count($executedUnavailableMigrations)
198
            ));
199
200 2
            foreach ($executedUnavailableMigrations->getItems() as $executedUnavailableMigration) {
201 2
                $output->writeln(sprintf(
202 2
                    '    <comment>>></comment> %s (<comment>%s</comment>)',
203 2
                    $executedUnavailableMigration->getExecutedAt() !== null
204
                        ? $executedUnavailableMigration->getExecutedAt()->format('Y-m-d H:i:s')
205 2
                        : null,
206 2
                    $executedUnavailableMigration->getVersion()
207
                ));
208
            }
209
210 2
            $question = 'Are you sure you wish to continue? (y/n)';
211
212 2
            if (! $this->canExecute($question, $input, $output)) {
213 1
                $output->writeln('<error>Migration cancelled!</error>');
214
215 1
                return false;
216
            }
217
        }
218
219 9
        return true;
220
    }
221
222 4
    private function getVersionNameFromAlias(
223
        string $versionAlias,
224
        OutputInterface $output
225
    ) : void {
226 4
        if ($versionAlias === 'first') {
227 1
            $output->writeln('<error>Already at first version.</error>');
228
229 1
            return;
230
        }
231 3
        if ($versionAlias === 'next' || $versionAlias === 'latest') {
232 1
            $output->writeln('<error>Already at latest version.</error>');
233
234 1
            return;
235
        }
236 2
        if (substr($versionAlias, 0, 7) === 'current') {
237 1
            $output->writeln('<error>The delta couldn\'t be reached.</error>');
238
239 1
            return;
240
        }
241
242 1
        $output->writeln(sprintf(
243 1
            '<error>Unknown version: %s</error>',
244 1
            OutputFormatter::escape($versionAlias)
245
        ));
246 1
    }
247
}
248