Files
breadcrumb-the-shire/bin/module-migrate.php

137 lines
4.9 KiB
PHP
Raw Normal View History

#!/usr/bin/env php
<?php
/**
* Applies pending module migrations for all enabled modules.
*
* Usage: php bin/module-migrate.php
*
* Each module declares a migrations_path in its manifest. SQL files in that
* directory are applied in alphabetical order. Applied files are tracked in the
* module_migrations table to ensure idempotency.
*
* Exit codes:
* 0 all migrations applied (or nothing to do)
* 1 error during migration
*/
declare(strict_types=1);
use MintyPHP\App\Module\ModuleRegistry;
use MintyPHP\DB;
require_once __DIR__ . '/module-cli-bootstrap.php';
function moduleMigrateRun(): int
{
$warningsBefore = moduleCliVendorWarningsIgnoredTotal();
moduleCliBootstrapApp();
$exitCode = 0;
$totalApplied = 0;
try {
$registry = app(ModuleRegistry::class);
$modules = $registry->getModules();
$hasModules = count($modules) > 0;
if (!$hasModules) {
fwrite(STDOUT, "module-migrate: no modules enabled, nothing to do.\n");
} else {
// Ensure tracking table exists (safe if already created by db/updates/ script)
DB::query(
'CREATE TABLE IF NOT EXISTS `module_migrations` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`module_id` VARCHAR(64) NOT NULL,
`filename` VARCHAR(255) NOT NULL,
`applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_module_migration` (`module_id`, `filename`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
);
}
foreach ($modules as $manifest) {
$migrationsPath = $manifest->migrationsPath;
if ($migrationsPath === null || !is_dir($migrationsPath)) {
continue;
}
// Discover SQL files
$files = glob($migrationsPath . '/*.sql');
if ($files === false || count($files) === 0) {
continue;
}
sort($files); // alphabetical order
// Get already-applied filenames
$applied = [];
$rows = DB::select('SELECT filename FROM module_migrations WHERE module_id = ?', [$manifest->id]);
foreach ($rows as $row) {
$applied[$row['filename']] = true;
}
foreach ($files as $file) {
$filename = basename($file);
if (isset($applied[$filename])) {
continue;
}
$sql = file_get_contents($file);
if ($sql === false || trim($sql) === '') {
fwrite(STDERR, sprintf("module-migrate: WARNING skipping empty file %s/%s\n", $manifest->id, $filename));
continue;
}
fwrite(STDOUT, sprintf("module-migrate: applying %s/%s ... ", $manifest->id, $filename));
// Execute migration inside a transaction so partial failures don't leave the DB inconsistent
DB::query('START TRANSACTION');
try {
$statements = array_filter(array_map('trim', explode(';', $sql)), static fn (string $s): bool => $s !== '');
foreach ($statements as $statement) {
DB::query($statement);
}
// Record as applied
DB::query(
'INSERT INTO module_migrations (module_id, filename) VALUES (?, ?)',
[$manifest->id, $filename]
);
DB::query('COMMIT');
} catch (\Throwable $migrationError) {
DB::query('ROLLBACK');
throw new \RuntimeException(
sprintf("Migration %s/%s failed: %s", $manifest->id, $filename, $migrationError->getMessage()),
0,
$migrationError
);
}
fwrite(STDOUT, "OK\n");
$totalApplied++;
}
}
if (!$hasModules) {
// Already reported above.
} elseif ($totalApplied === 0) {
fwrite(STDOUT, "module-migrate: all modules up to date, 0 new migrations.\n");
} else {
fwrite(STDOUT, sprintf("module-migrate: applied %d migration(s).\n", $totalApplied));
}
} catch (Throwable $exception) {
fwrite(STDERR, sprintf("module-migrate: FAILED: %s\n", $exception->getMessage()));
$exitCode = 1;
}
$vendorWarningsIgnored = moduleCliVendorWarningsIgnoredSince($warningsBefore);
fwrite(STDOUT, sprintf("module-migrate: vendor_warnings_ignored=%d\n", $vendorWarningsIgnored));
return $exitCode;
}
if (PHP_SAPI === 'cli' && realpath((string) ($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
exit(moduleMigrateRun());
}