diff --git a/docs/en/index.rst b/docs/en/index.rst
index 26b41793..f6103255 100644
--- a/docs/en/index.rst
+++ b/docs/en/index.rst
@@ -550,6 +550,21 @@ You can also output the results as a JSON formatted string using the
You can also use the ``--source``, ``--connection`` and ``--plugin`` options
just like for the ``migrate`` command.
+Cleaning up missing migrations
+-------------------------------
+
+Sometimes migration files may be deleted from the filesystem but still exist
+in the phinxlog table. These migrations will be marked as **MISSING** in the
+status output. You can remove these entries from the phinxlog table using the
+``--cleanup`` option:
+
+.. code-block:: bash
+
+ bin/cake migrations status --cleanup
+
+This will remove all migration entries from the phinxlog table that no longer
+have corresponding migration files in the filesystem.
+
Marking a migration as migrated
===============================
diff --git a/src/Command/StatusCommand.php b/src/Command/StatusCommand.php
index 442a0e2f..1c773436 100644
--- a/src/Command/StatusCommand.php
+++ b/src/Command/StatusCommand.php
@@ -63,6 +63,8 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'',
'migrations status -c secondary',
'migrations status -c secondary -f json',
+ 'migrations status --cleanup',
+ 'Remove *MISSING* migrations from the phinxlog table',
])->addOption('plugin', [
'short' => 'p',
'help' => 'The plugin to run migrations for',
@@ -79,6 +81,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'help' => 'The output format: text or json. Defaults to text.',
'choices' => ['text', 'json'],
'default' => 'text',
+ ])->addOption('cleanup', [
+ 'help' => 'Remove MISSING migrations from the phinxlog table',
+ 'boolean' => true,
+ 'default' => false,
]);
return $parser;
@@ -95,6 +101,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
{
/** @var string|null $format */
$format = $args->getOption('format');
+ $clean = $args->getOption('cleanup');
$factory = new ManagerFactory([
'plugin' => $args->getOption('plugin'),
@@ -103,6 +110,18 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
'dry-run' => $args->getOption('dry-run'),
]);
$manager = $factory->createManager($io);
+
+ if ($clean) {
+ $removed = $manager->cleanupMissingMigrations();
+ if ($removed === 0) {
+ $io->out('No missing migrations to clean up.');
+ } else {
+ $io->out(sprintf('Removed %d missing migration(s) from migration log.', $removed));
+ }
+
+ return Command::CODE_SUCCESS;
+ }
+
$migrations = $manager->printStatus($format);
switch ($format) {
diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php
index 37bdc9b4..226bfb23 100644
--- a/src/Migration/Manager.php
+++ b/src/Migration/Manager.php
@@ -1183,4 +1183,47 @@ public function resetSeeds(): void
{
$this->seeds = null;
}
+
+ /**
+ * Cleanup missing migrations from the phinxlog table
+ *
+ * Removes entries from the phinxlog table for migrations that no longer exist
+ * in the migrations directory (marked as MISSING in status output).
+ *
+ * @return int The number of missing migrations removed
+ */
+ public function cleanupMissingMigrations(): int
+ {
+ $defaultMigrations = $this->getMigrations();
+ $env = $this->getEnvironment();
+ $versions = $env->getVersionLog();
+ $adapter = $env->getAdapter();
+
+ // Find missing migrations (those in phinxlog but not in filesystem)
+ $missingVersions = [];
+ foreach ($versions as $versionId => $versionInfo) {
+ if (!isset($defaultMigrations[$versionId])) {
+ $missingVersions[] = $versionId;
+ }
+ }
+
+ if (!$missingVersions) {
+ return 0;
+ }
+
+ // Remove missing migrations from phinxlog
+ $adapter->beginTransaction();
+ try {
+ $delete = $adapter->getDeleteBuilder()
+ ->from($env->getSchemaTableName())
+ ->where(['version IN' => $missingVersions]);
+ $delete->execute();
+ $adapter->commitTransaction();
+ } catch (Exception $e) {
+ $adapter->rollbackTransaction();
+ throw $e;
+ }
+
+ return count($missingVersions);
+ }
}
diff --git a/tests/TestCase/Command/CompletionTest.php b/tests/TestCase/Command/CompletionTest.php
index 46d8405d..b30b2304 100644
--- a/tests/TestCase/Command/CompletionTest.php
+++ b/tests/TestCase/Command/CompletionTest.php
@@ -124,7 +124,7 @@ public function testMigrationsOptionsStatus()
$this->exec('completion options migrations.migrations status');
$this->assertCount(1, $this->_out->messages());
$output = $this->_out->messages()[0];
- $expected = '--connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v';
+ $expected = '--cleanup --connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v';
$outputExplode = explode(' ', trim($output));
sort($outputExplode);
$expectedExplode = explode(' ', $expected);
diff --git a/tests/TestCase/Command/StatusCommandTest.php b/tests/TestCase/Command/StatusCommandTest.php
index d9562b73..e1ea9102 100644
--- a/tests/TestCase/Command/StatusCommandTest.php
+++ b/tests/TestCase/Command/StatusCommandTest.php
@@ -78,4 +78,46 @@ public function testExecuteConnectionDoesNotExist(): void
$this->expectException(RuntimeException::class);
$this->exec('migrations status -c lolnope');
}
+
+ public function testCleanNoMissingMigrations(): void
+ {
+ $this->exec('migrations status -c test --cleanup');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('No missing migrations to clean up.');
+ }
+
+ public function testCleanWithMissingMigrations(): void
+ {
+ // First, insert a fake migration entry that doesn't exist in filesystem
+ $table = $this->fetchTable('Phinxlog');
+ $entity = $table->newEntity([
+ 'version' => 99999999999999,
+ 'migration_name' => 'FakeMissingMigration',
+ 'start_time' => '2024-01-01 00:00:00',
+ 'end_time' => '2024-01-01 00:00:01',
+ 'breakpoint' => false,
+ ]);
+ $table->save($entity);
+
+ // Verify the fake migration is in the table
+ $count = $table->find()->where(['version' => 99999999999999])->count();
+ $this->assertEquals(1, $count);
+
+ // Run the clean command
+ $this->exec('migrations status -c test --cleanup');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('Removed 1 missing migration(s) from migration log.');
+
+ // Verify the fake migration was removed
+ $count = $table->find()->where(['version' => 99999999999999])->count();
+ $this->assertEquals(0, $count);
+ }
+
+ public function testCleanHelp(): void
+ {
+ $this->exec('migrations status --help');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('--cleanup');
+ $this->assertOutputContains('Remove MISSING migrations from the phinxlog table');
+ }
}