#!/usr/bin/env php getModules(); $hasModules = count($modules) > 0; if (!$hasModules) { fwrite(STDOUT, "module-migrate: no modules enabled, nothing to do.\n"); return 0; } // 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 = ?', (string) $manifest->id); foreach ((array) $rows as $row) { if (!is_array($row)) { continue; } $filename = trim((string) (($row['module_migrations']['filename'] ?? $row['filename'] ?? ''))); if ($filename !== '') { $applied[$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 = moduleMigrateSplitSqlStatements($sql); foreach ($statements as $statement) { DB::query($statement); } // Record as applied DB::query( 'INSERT INTO module_migrations (module_id, filename) VALUES (?, ?)', (string) $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 ($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)); } return 0; } ); }); } /** * @return list */ function moduleMigrateSplitSqlStatements(string $sql): array { if (preg_match('/^\s*DELIMITER\s+/mi', $sql) === 1) { throw new RuntimeException('DELIMITER directives are not supported in module migration SQL files.'); } $statements = []; $buffer = ''; $length = strlen($sql); $inSingleQuote = false; $inDoubleQuote = false; $inBacktick = false; $inLineComment = false; $inBlockComment = false; $escapeNext = false; for ($i = 0; $i < $length; $i++) { $char = $sql[$i]; $next = $i + 1 < $length ? $sql[$i + 1] : ''; if ($inLineComment) { $buffer .= $char; if ($char === "\n") { $inLineComment = false; } continue; } if ($inBlockComment) { $buffer .= $char; if ($char === '*' && $next === '/') { $buffer .= $next; $i++; $inBlockComment = false; } continue; } if (!$inSingleQuote && !$inDoubleQuote && !$inBacktick) { if ($char === '-' && $next === '-' && ($i + 2 >= $length || ctype_space($sql[$i + 2]))) { $buffer .= $char . $next; $i++; $inLineComment = true; continue; } if ($char === '#') { $buffer .= $char; $inLineComment = true; continue; } if ($char === '/' && $next === '*') { $buffer .= $char . $next; $i++; $inBlockComment = true; continue; } } if ($escapeNext) { $buffer .= $char; $escapeNext = false; continue; } if (($inSingleQuote || $inDoubleQuote) && $char === '\\') { $buffer .= $char; $escapeNext = true; continue; } if (!$inDoubleQuote && !$inBacktick && $char === '\'') { $inSingleQuote = !$inSingleQuote; $buffer .= $char; continue; } if (!$inSingleQuote && !$inBacktick && $char === '"') { $inDoubleQuote = !$inDoubleQuote; $buffer .= $char; continue; } if (!$inSingleQuote && !$inDoubleQuote && $char === '`') { $inBacktick = !$inBacktick; $buffer .= $char; continue; } if (!$inSingleQuote && !$inDoubleQuote && !$inBacktick && $char === ';') { $statement = trim($buffer); if ($statement !== '') { $statements[] = $statement; } $buffer = ''; continue; } $buffer .= $char; } $tail = trim($buffer); if ($tail !== '') { $statements[] = $tail; } return $statements; } if (PHP_SAPI === 'cli' && realpath((string) ($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) { fwrite(STDERR, "Hint: prefer `php bin/console module:migrate` instead.\n"); exit(moduleMigrateRun()); }