Completed
Push — master ( a5c63d...f327c4 )
by Asmir
02:31 queued 12s
created

MigrateCommand::errorForAlias()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 5
nop 2
dl 0
loc 37
ccs 21
cts 21
cp 1
crap 6
rs 8.9617
c 0
b 0
f 0
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\NoMigrationsToExecute;
9
use Doctrine\Migrations\Exception\UnknownMigrationVersion;
10
use Doctrine\Migrations\Metadata\ExecutedMigrationsList;
11
use Symfony\Component\Console\Formatter\OutputFormatter;
12
use Symfony\Component\Console\Input\InputArgument;
13
use Symfony\Component\Console\Input\InputInterface;
14
use Symfony\Component\Console\Input\InputOption;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use function count;
17
use function getcwd;
18
use function in_array;
19
use function is_string;
20
use function is_writable;
21
use function sprintf;
22
use function strpos;
23
24
/**
25
 * The MigrateCommand class is responsible for executing a migration from the current version to another
26
 * version up or down. It will calculate all the migration versions that need to be executed and execute them.
27
 */
28
final class MigrateCommand extends DoctrineCommand
29
{
30
    /** @var string */
31
    protected static $defaultName = 'migrations:migrate';
32
33 24
    protected function configure() : void
34
    {
35
        $this
36 24
            ->setAliases(['migrate'])
37 24
            ->setDescription(
38 24
                'Execute a migration to a specified version or the latest available version.'
39
            )
40 24
            ->addArgument(
41 24
                'version',
42 24
                InputArgument::OPTIONAL,
43 24
                'The version FQCN or alias (first, prev, next, latest) to migrate to.',
44 24
                'latest'
45
            )
46 24
            ->addOption(
47 24
                'write-sql',
48 24
                null,
49 24
                InputOption::VALUE_OPTIONAL,
50 24
                'The path to output the migration SQL file. Defaults to current working directory.',
51 24
                false
52
            )
53 24
            ->addOption(
54 24
                'dry-run',
55 24
                null,
56 24
                InputOption::VALUE_NONE,
57 24
                'Execute the migration as a dry run.'
58
            )
59 24
            ->addOption(
60 24
                'query-time',
61 24
                null,
62 24
                InputOption::VALUE_NONE,
63 24
                'Time all the queries individually.'
64
            )
65 24
            ->addOption(
66 24
                'allow-no-migration',
67 24
                null,
68 24
                InputOption::VALUE_NONE,
69 24
                'Do not throw an exception if no migration is available.'
70
            )
71 24
            ->addOption(
72 24
                'all-or-nothing',
73 24
                null,
74 24
                InputOption::VALUE_OPTIONAL,
75 24
                'Wrap the entire migration in a transaction.',
76 24
                false
77
            )
78 24
            ->setHelp(<<<EOT
79 24
The <info>%command.name%</info> command executes a migration to a specified version or the latest available version:
80
81
    <info>%command.full_name%</info>
82
83
You can optionally manually specify the version you wish to migrate to:
84
85
    <info>%command.full_name% FQCN</info>
86
87
You can specify the version you wish to migrate to using an alias:
88
89
    <info>%command.full_name% prev</info>
90
    <info>These alias are defined : first, latest, prev, current and next</info>
91
92
You can specify the version you wish to migrate to using an number against the current version:
93
94
    <info>%command.full_name% current+3</info>
95
96
You can also execute the migration as a <comment>--dry-run</comment>:
97
98
    <info>%command.full_name% FQCN --dry-run</info>
99
100
You can output the prepared SQL statements to a file with <comment>--write-sql</comment>:
101
102
    <info>%command.full_name% FQCN --write-sql</info>
103
104
Or you can also execute the migration without a warning message which you need to interact with:
105
106
    <info>%command.full_name% --no-interaction</info>
107
108
You can also time all the different queries if you wanna know which one is taking so long:
109
110
    <info>%command.full_name% --query-time</info>
111
112
Use the --all-or-nothing option to wrap the entire migration in a transaction.
113
EOT
114
            );
115
116 24
        parent::configure();
117 24
    }
118
119 24
    protected function execute(InputInterface $input, OutputInterface $output) : int
120
    {
121 24
        $migratorConfigurationFactory = $this->getDependencyFactory()->getConsoleInputMigratorConfigurationFactory();
122 24
        $migratorConfiguration        = $migratorConfigurationFactory->getMigratorConfiguration($input);
123
124 24
        $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?';
125 24
        if (! $migratorConfiguration->isDryRun() && ! $this->canExecute($question, $input)) {
126 3
            $this->io->error('Migration cancelled!');
127
128 3
            return 3;
129
        }
130
131 21
        $this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
132
133 21
        $allowNoMigration = $input->getOption('allow-no-migration');
134 21
        $versionAlias     = $input->getArgument('version');
135
136 21
        $path = $input->getOption('write-sql') ?? getcwd();
137 21
        if (is_string($path) && ! is_writable($path)) {
138
            $this->io->error(sprintf('The path "%s" not writeable!', $path));
139
140
            return 1;
141
        }
142
143
        try {
144 21
            $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

144
            $version = $this->getDependencyFactory()->getVersionAliasResolver()->resolveVersionAlias(/** @scrutinizer ignore-type */ $versionAlias);
Loading history...
145 5
        } catch (UnknownMigrationVersion|NoMigrationsToExecute|NoMigrationsFoundWithCriteria $e) {
146 5
            return $this->errorForAlias($versionAlias, $allowNoMigration);
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...ommand::errorForAlias() 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

146
            return $this->errorForAlias(/** @scrutinizer ignore-type */ $versionAlias, $allowNoMigration);
Loading history...
147
        }
148
149 16
        $planCalculator                = $this->getDependencyFactory()->getMigrationPlanCalculator();
150 16
        $statusCalculator              = $this->getDependencyFactory()->getMigrationStatusCalculator();
151 16
        $executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
152
153 16
        if ($this->checkExecutedUnavailableMigrations($executedUnavailableMigrations, $input) === false) {
154 1
            return 3;
155
        }
156
157 15
        $plan = $planCalculator->getPlanUntilVersion($version);
158
159 15
        if (count($plan) === 0) {
160 5
            return $this->errorForAlias($versionAlias, $allowNoMigration);
161
        }
162
163 10
        $this->getDependencyFactory()->getLogger()->notice(
164 10
            'Migrating' . ($migratorConfiguration->isDryRun() ? ' (dry-run)' : '') . ' {direction} to {to}',
165
            [
166 10
                'direction' => $plan->getDirection(),
167 10
                'to' => (string) $version,
168
            ]
169
        );
170
171 10
        $migrator = $this->getDependencyFactory()->getMigrator();
172 10
        $sql      = $migrator->migrate($plan, $migratorConfiguration);
173
174 10
        if (is_string($path)) {
175 4
            $writer = $this->getDependencyFactory()->getQueryWriter();
176 4
            $writer->write($path, $plan->getDirection(), $sql);
177
        }
178
179 10
        $this->io->newLine();
180
181 10
        return 0;
182
    }
183
184 16
    private function checkExecutedUnavailableMigrations(
185
        ExecutedMigrationsList $executedUnavailableMigrations,
186
        InputInterface $input
187
    ) : bool {
188 16
        if (count($executedUnavailableMigrations) !== 0) {
189 1
            $this->io->warning(sprintf(
190 1
                'You have %s previously executed migrations in the database that are not registered migrations.',
191 1
                count($executedUnavailableMigrations)
192
            ));
193
194 1
            foreach ($executedUnavailableMigrations->getItems() as $executedUnavailableMigration) {
195 1
                $this->io->text(sprintf(
196 1
                    '<comment>>></comment> %s (<comment>%s</comment>)',
197 1
                    $executedUnavailableMigration->getExecutedAt() !== null
198
                        ? $executedUnavailableMigration->getExecutedAt()->format('Y-m-d H:i:s')
199 1
                        : null,
200 1
                    $executedUnavailableMigration->getVersion()
201
                ));
202
            }
203
204 1
            $question = 'Are you sure you wish to continue?';
205
206 1
            if (! $this->canExecute($question, $input)) {
207 1
                $this->io->error('Migration cancelled!');
208
209 1
                return false;
210
            }
211
        }
212
213 15
        return true;
214
    }
215
216 10
    private function errorForAlias(string $versionAlias, bool $allowNoMigration) : int
217
    {
218 10
        if (in_array($versionAlias, ['first', 'next', 'latest'], true) || strpos($versionAlias, 'current') === 0) {
219 8
            $version = $this->getDependencyFactory()->getVersionAliasResolver()->resolveVersionAlias('current');
220
221
            // Allow meaningful message when latest version already reached.
222 8
            if ($versionAlias === 'next' || $versionAlias === 'latest') {
223 4
                $message = sprintf(
224 4
                    'Already at "%s" version ("%s")',
225 4
                    $versionAlias,
226 4
                    (string) $version
227
                );
228
            } else {
229 4
                $message = sprintf(
230 4
                    'The version "%s" couldn\'t be reached, you are at version "%s"',
231 4
                    $versionAlias,
232 4
                    (string) $version
233
                );
234
            }
235
236 8
            if ($allowNoMigration) {
237 4
                $this->io->warning($message);
238
239 4
                return 0;
240
            }
241
242 4
            $this->io->error($message);
243
244 4
            return 1;
245
        }
246
247 2
        $this->io->error(sprintf(
248 2
            'Unknown version: %s',
249 2
            OutputFormatter::escape($versionAlias)
250
        ));
251
252 2
        return 1;
253
    }
254
}
255