Skip to content

Commit 5b3e769

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 5b3e769

File tree

3 files changed

+111
-5
lines changed

3 files changed

+111
-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: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ protected function configure(): void
4242
->setHelp(<<<'EOT'
4343
The <info>%command.name%</info> command generates a migration by comparing your current database to your mapping information:
4444
45-
<info>%command.full_name%</info>
45+
<info>%command.full_name%</info>
4646

47-
EOT)
47+
EOT,)
4848
->addOption(
4949
'namespace',
5050
null,
@@ -94,6 +94,12 @@ protected function configure(): void
9494
null,
9595
InputOption::VALUE_NONE,
9696
'Generate a full migration as if the current database was empty.',
97+
)
98+
->addOption(
99+
'check',
100+
null,
101+
InputOption::VALUE_NONE,
102+
'Check if a migration is needed and exit with a non-zero status code if it is.',
97103
);
98104
}
99105

@@ -107,13 +113,34 @@ protected function execute(
107113
$filterExpression = null;
108114
}
109115

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

118145
if ($formatted) {
119146
if (! class_exists(SqlFormatter::class)) {
@@ -135,8 +162,7 @@ protected function execute(
135162
return 3;
136163
}
137164

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

141167
try {
142168
$path = $diffGenerator->generate(

tests/Tools/Console/Command/DiffCommandTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,54 @@ 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(['--check' => true]);
198+
199+
$output = $this->diffCommandTester->getDisplay(true);
200+
$statusCode = $this->diffCommandTester->getStatusCode();
201+
202+
self::assertStringContainsString('[OK] The database schema is in sync with the mapping. No migration required.', $output);
203+
self::assertSame(0, $statusCode);
204+
}
205+
206+
public function testCheckReturnsOneWhenChanges(): void
207+
{
208+
$this->migrationDiffGenerator
209+
->expects(self::once())
210+
->method('hasChanges')
211+
->with('filter', true)
212+
->willReturn(true);
213+
214+
$this->migrationDiffGenerator
215+
->expects(self::never())
216+
->method('generate');
217+
218+
$this->diffCommandTester->execute([
219+
'--check' => true,
220+
'--filter-expression' => 'filter',
221+
'--from-empty-schema' => true,
222+
]);
223+
224+
$output = $this->diffCommandTester->getDisplay(true);
225+
$statusCode = $this->diffCommandTester->getStatusCode();
226+
227+
self::assertStringContainsString('[ERROR] The database schema is not in sync with the mapping.', $output);
228+
self::assertStringContainsString('Run the following command to generate a migration:', $output);
229+
self::assertStringContainsString('bin/console doctrine:migrations:diff', $output);
230+
self::assertSame(1, $statusCode);
231+
}
232+
185233
protected function setUp(): void
186234
{
187235
$this->migrationDiffGenerator = $this->createMock(DiffGenerator::class);

0 commit comments

Comments
 (0)