Skip to content

Commit 7dddfc6

Browse files
committed
feat(Console): Add --check option to diff command
This option allows checking for schema differences in a CI/CD environment without generating a file. It exits with a non-zero status code if changes are detected, providing a clear signal for CI workflows and actionable feedback for developers.
1 parent 590b2aa commit 7dddfc6

File tree

3 files changed

+114
-5
lines changed

3 files changed

+114
-5
lines changed

src/Generator/DiffGenerator.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,38 @@ public function __construct(
3737
) {
3838
}
3939

40+
public function hasChanges(string|null $filterExpression, bool $fromEmptySchema = false): bool
41+
{
42+
if ($filterExpression !== null) {
43+
$this->dbalConfiguration->setSchemaAssetsFilter(
44+
static function ($assetName) use ($filterExpression) {
45+
if ($assetName instanceof AbstractAsset) {
46+
$assetName = $assetName->getName();
47+
}
48+
49+
return preg_match($filterExpression, $assetName);
50+
},
51+
);
52+
}
53+
54+
$fromSchema = $fromEmptySchema
55+
? $this->createEmptySchema()
56+
: $this->createFromSchema();
57+
58+
$toSchema = $this->createToSchema();
59+
60+
if (class_exists(ComparatorConfig::class)) {
61+
$comparator = $this->schemaManager->createComparator((new ComparatorConfig())->withReportModifiedIndexes(false));
62+
} else {
63+
$comparator = $this->schemaManager->createComparator();
64+
}
65+
66+
$upSql = $this->platform->getAlterSchemaSQL($comparator->compareSchemas($fromSchema, $toSchema));
67+
$downSql = $this->platform->getAlterSchemaSQL($comparator->compareSchemas($toSchema, $fromSchema));
68+
69+
return $upSql !== [] || $downSql !== [];
70+
}
71+
4072
/** @throws NoChangesDetected */
4173
public function generate(
4274
string $fqcn,

src/Tools/Console/Command/DiffCommand.php

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ protected function configure(): void
3939
$this
4040
->setAliases(['diff'])
4141
->setDescription('Generate a migration by comparing your current database to your mapping information.')
42-
->setHelp(<<<'EOT'
42+
->setHelp(<<<EOT
4343
The <info>%command.name%</info> command generates a migration by comparing your current database to your mapping information:
4444
4545
<info>%command.full_name%</info>
4646
47-
EOT)
47+
EOT
48+
)
4849
->addOption(
4950
'namespace',
5051
null,
@@ -94,6 +95,12 @@ protected function configure(): void
9495
null,
9596
InputOption::VALUE_NONE,
9697
'Generate a full migration as if the current database was empty.',
98+
)
99+
->addOption(
100+
'check',
101+
null,
102+
InputOption::VALUE_NONE,
103+
'Check if a migration is needed and exit with a non-zero status code if it is.',
97104
);
98105
}
99106

@@ -107,13 +114,34 @@ protected function execute(
107114
$filterExpression = null;
108115
}
109116

117+
$fromEmptySchema = $input->getOption('from-empty-schema');
118+
$check = $input->getOption('check');
119+
120+
$diffGenerator = $this->getDependencyFactory()->getDiffGenerator();
121+
122+
if ($check) {
123+
if ($diffGenerator->hasChanges($filterExpression, $fromEmptySchema)) {
124+
$this->io->error([
125+
'The database schema is not in sync with the mapping.',
126+
'Run the following command to generate a migration:',
127+
'',
128+
'bin/console doctrine:migrations:diff',
129+
]);
130+
131+
return 1;
132+
}
133+
134+
$this->io->success('The database schema is in sync with the mapping. No migration required.');
135+
136+
return 0;
137+
}
138+
110139
$formatted = filter_var($input->getOption('formatted'), FILTER_VALIDATE_BOOLEAN);
111140
$nowdocOutput = $input->getOption('nowdoc');
112141
$nowdocOutput = $nowdocOutput === null ? null : filter_var($input->getOption('nowdoc'), FILTER_VALIDATE_BOOLEAN);
113142
$lineLength = (int) $input->getOption('line-length');
114143
$allowEmptyDiff = $input->getOption('allow-empty-diff');
115144
$checkDbPlatform = filter_var($input->getOption('check-database-platform'), FILTER_VALIDATE_BOOLEAN);
116-
$fromEmptySchema = $input->getOption('from-empty-schema');
117145

118146
if ($formatted) {
119147
if (! class_exists(SqlFormatter::class)) {
@@ -135,8 +163,7 @@ protected function execute(
135163
return 3;
136164
}
137165

138-
$fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace);
139-
$diffGenerator = $this->getDependencyFactory()->getDiffGenerator();
166+
$fqcn = $this->getDependencyFactory()->getClassNameGenerator()->generateClassName($namespace);
140167

141168
try {
142169
$path = $diffGenerator->generate(

tests/Tools/Console/Command/DiffCommandTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,56 @@ public function testExecuteWithMultipleDirectories(int|null $input, string $name
182182
self::assertStringContainsString(sprintf('You have selected the "%s" namespace', $namespace), $output);
183183
}
184184

185+
public function testCheckReturnsZeroWhenNoChanges(): void
186+
{
187+
$this->migrationDiffGenerator
188+
->expects(self::once())
189+
->method('hasChanges')
190+
->with(null, false)
191+
->willReturn(false);
192+
193+
$this->migrationDiffGenerator
194+
->expects(self::never())
195+
->method('generate');
196+
197+
$this->diffCommandTester->execute([
198+
'--check' => true,
199+
]);
200+
201+
$output = $this->diffCommandTester->getDisplay(true);
202+
$statusCode = $this->diffCommandTester->getStatusCode();
203+
204+
self::assertStringContainsString('[OK] The database schema is in sync with the mapping. No migration required.', $output);
205+
self::assertSame(0, $statusCode);
206+
}
207+
208+
public function testCheckReturnsOneWhenChanges(): void
209+
{
210+
$this->migrationDiffGenerator
211+
->expects(self::once())
212+
->method('hasChanges')
213+
->with('filter', true)
214+
->willReturn(true);
215+
216+
$this->migrationDiffGenerator
217+
->expects(self::never())
218+
->method('generate');
219+
220+
$this->diffCommandTester->execute([
221+
'--check' => true,
222+
'--filter-expression' => 'filter',
223+
'--from-empty-schema' => true,
224+
]);
225+
226+
$output = $this->diffCommandTester->getDisplay(true);
227+
$statusCode = $this->diffCommandTester->getStatusCode();
228+
229+
self::assertStringContainsString('[ERROR] The database schema is not in sync with the mapping.', $output);
230+
self::assertStringContainsString('Run the following command to generate a migration:', $output);
231+
self::assertStringContainsString('bin/console doctrine:migrations:diff', $output);
232+
self::assertSame(1, $statusCode);
233+
}
234+
185235
protected function setUp(): void
186236
{
187237
$this->migrationDiffGenerator = $this->createMock(DiffGenerator::class);

0 commit comments

Comments
 (0)