Failed Conditions
Pull Request — master (#941)
by Vincent
02:50
created

DiffCommand::checkNewMigrations()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 21
ccs 0
cts 10
cp 0
crap 12
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Migrations\Tools\Console\Command;
6
7
use Doctrine\Migrations\Generator\Exception\NoChangesDetected;
8
use Doctrine\Migrations\Metadata\AvailableMigrationsList;
9
use Doctrine\Migrations\Metadata\ExecutedMigrationsSet;
10
use Doctrine\Migrations\Tools\Console\Exception\InvalidOptionUsage;
11
use OutOfBoundsException;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Output\OutputInterface;
15
use function addslashes;
16
use function assert;
17
use function class_exists;
18
use function count;
19
use function filter_var;
20
use function is_string;
21
use function key;
22
use function sprintf;
23
use const FILTER_VALIDATE_BOOLEAN;
24
25
/**
26
 * The DiffCommand class is responsible for generating a migration by comparing your current database schema to
27
 * your mapping information.
28
 */
29
class DiffCommand extends DoctrineCommand
30
{
31
    /** @var string */
32
    protected static $defaultName = 'migrations:diff';
33
34
    protected function configure() : void
35
    {
36
        parent::configure();
37
38
        $this
39
            ->setAliases(['diff'])
40
            ->setDescription('Generate a migration by comparing your current database to your mapping information.')
41
            ->setHelp(<<<EOT
42
The <info>%command.name%</info> command generates a migration by comparing your current database to your mapping information:
43
44
    <info>%command.full_name%</info>
45
46
You can optionally specify a <comment>--editor-cmd</comment> option to open the generated file in your favorite editor:
47
48
    <info>%command.full_name% --editor-cmd=mate</info>
49
EOT
50
            )
51
            ->addOption(
52
                'namespace',
53
                null,
54
                InputOption::VALUE_REQUIRED,
55
                'The namespace to use for the migration (must be in the list of configured namespaces)'
56
            )
57
            ->addOption(
58
                'editor-cmd',
59
                null,
60
                InputOption::VALUE_REQUIRED,
61
                'Open file with this command upon creation.'
62
            )
63
            ->addOption(
64
                'filter-expression',
65
                null,
66
                InputOption::VALUE_REQUIRED,
67
                'Tables which are filtered by Regular Expression.'
68
            )
69
            ->addOption(
70
                'formatted',
71
                null,
72
                InputOption::VALUE_NONE,
73
                'Format the generated SQL.'
74
            )
75
            ->addOption(
76
                'line-length',
77
                null,
78
                InputOption::VALUE_REQUIRED,
79
                'Max line length of unformatted lines.',
80
                120
81
            )
82
            ->addOption(
83
                'check-database-platform',
84
                null,
85
                InputOption::VALUE_OPTIONAL,
86
                'Check Database Platform to the generated code.',
87
                false
88
            )
89
            ->addOption(
90
                'allow-empty-diff',
91
                null,
92
                InputOption::VALUE_NONE,
93
                'Do not throw an exception when no changes are detected.'
94
            );
95
    }
96
97
    /**
98
     * @throws InvalidOptionUsage
99
     */
100
    public function execute(
101
        InputInterface $input,
102
        OutputInterface $output
103
    ) : ?int {
104
        $filterExpression = (string) $input->getOption('filter-expression') ?: null;
105
        $formatted        = filter_var($input->getOption('formatted'), FILTER_VALIDATE_BOOLEAN);
106
        $lineLength       = (int) $input->getOption('line-length');
107
        $allowEmptyDiff   = $input->getOption('allow-empty-diff');
108
        $checkDbPlatform  = filter_var($input->getOption('check-database-platform'), FILTER_VALIDATE_BOOLEAN);
109
        $namespace        = $input->getOption('namespace') ?: null;
110
        if ($formatted) {
111
            if (! class_exists('SqlFormatter')) {
112
                throw InvalidOptionUsage::new(
113
                    'The "--formatted" option can only be used if the sql formatter is installed. Please run "composer require jdorn/sql-formatter".'
114
                );
115
            }
116
        }
117
118
        $configuration = $this->getDependencyFactory()->getConfiguration();
119
120
        $dirs = $configuration->getMigrationDirectories();
121
        if ($namespace === null) {
122
            $namespace = key($dirs);
123
        } elseif (! isset($dirs[$namespace])) {
124
            throw new OutOfBoundsException(sprintf('Path not defined for the namespace %s', $namespace));
125
        }
126
127
        assert(is_string($namespace));
128
129
        $statusCalculator              = $this->getDependencyFactory()->getMigrationStatusCalculator();
130
        $executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
131
        $newMigrations                 = $statusCalculator->getNewMigrations();
132
133
        if ($this->checkNewMigrations($newMigrations, $input, $output) === false) {
134
            return 3;
135
        }
136
137
        if ($this->checkExecutedUnavailableMigrations($executedUnavailableMigrations, $input, $output) === false) {
138
            return 3;
139
        }
140
141
        $fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace);
142
143
        $diffGenerator = $this->getDependencyFactory()->getDiffGenerator();
144
145
        try {
146
            $path = $diffGenerator->generate(
147
                $fqcn,
148
                $filterExpression,
149
                $formatted,
150
                $lineLength,
151
                $checkDbPlatform
152
            );
153
        } catch (NoChangesDetected $exception) {
154
            if ($allowEmptyDiff) {
155
                $output->writeln($exception->getMessage());
156
157
                return 0;
158
            }
159
160
            throw $exception;
161
        }
162
163
        $editorCommand = $input->getOption('editor-cmd');
164
165
        if ($editorCommand !== null) {
166
            assert(is_string($editorCommand));
167
            $this->procOpen($editorCommand, $path);
168
        }
169
170
        $output->writeln([
171
            sprintf('Generated new migration class to "<info>%s</info>"', $path),
172
            '',
173
            sprintf(
174
                'To run just this migration for testing purposes, you can use <info>migrations:execute --up \'%s\'</info>',
175
                addslashes($fqcn)
176
            ),
177
            '',
178
            sprintf(
179
                'To revert the migration you can use <info>migrations:execute --down \'%s\'</info>',
180
                addslashes($fqcn)
181
            ),
182
        ]);
183
184
        return 0;
185
    }
186
187
    private function checkNewMigrations(
188
        AvailableMigrationsList $newMigrations,
189
        InputInterface $input,
190
        OutputInterface $output
191
    ) : bool {
192
        if (count($newMigrations) !== 0) {
193
            $output->writeln(sprintf(
194
                '<error>WARNING! You have %s available migrations to execute.</error>',
195
                count($newMigrations)
196
            ));
197
198
            $question = 'Are you sure you wish to continue? (y/n)';
199
200
            if (! $this->canExecute($question, $input, $output)) {
201
                $output->writeln('<error>Migration cancelled!</error>');
202
203
                return false;
204
            }
205
        }
206
207
        return true;
208
    }
209
210
    private function checkExecutedUnavailableMigrations(
211
        ExecutedMigrationsSet $executedUnavailableMigrations,
212
        InputInterface $input,
213
        OutputInterface $output
214
    ) : bool {
215
        if (count($executedUnavailableMigrations) !== 0) {
216
            $output->writeln(sprintf(
217
                '<error>WARNING! You have %s previously executed migrations in the database that are not registered migrations.</error>',
218
                count($executedUnavailableMigrations)
219
            ));
220
221
            foreach ($executedUnavailableMigrations->getItems() as $executedUnavailableMigration) {
222
                $output->writeln(sprintf(
223
                    '    <comment>>></comment> %s (<comment>%s</comment>)',
224
                    $executedUnavailableMigration->getExecutedAt() !== null
225
                        ? $executedUnavailableMigration->getExecutedAt()->format('Y-m-d H:i:s')
226
                        : null,
227
                    $executedUnavailableMigration->getVersion()
228
                ));
229
            }
230
231
            $question = 'Are you sure you wish to continue? (y/n)';
232
233
            if (! $this->canExecute($question, $input, $output)) {
234
                $output->writeln('<error>Migration cancelled!</error>');
235
236
                return false;
237
            }
238
        }
239
240
        return true;
241
    }
242
}
243