add migrate.php

This commit is contained in:
Flatlogic Bot 2026-05-01 18:41:11 +00:00
parent ff296e8bc2
commit c46f356c7c
2 changed files with 274 additions and 61 deletions

273
migrate.php Normal file
View File

@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
if (PHP_SAPI !== 'cli') {
header('Content-Type: text/plain; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
}
@set_time_limit(0);
function migrationOutput(string $message = ''): void
{
echo $message . PHP_EOL;
}
function canRunMigrations(): bool
{
if (PHP_SAPI === 'cli') {
return true;
}
if (session_status() === PHP_SESSION_NONE) {
@session_start();
}
if (($_SESSION['user_role_name'] ?? '') === 'Administrator') {
return true;
}
$remoteAddress = $_SERVER['REMOTE_ADDR'] ?? '';
return in_array($remoteAddress, ['127.0.0.1', '::1'], true);
}
function ensureMigrationsTable(PDO $pdo): void
{
$pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
SQL);
}
function getExecutedMigrations(PDO $pdo): array
{
$stmt = $pdo->query('SELECT migration FROM migrations');
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
return array_fill_keys($rows, true);
}
function getMigrationFiles(): array
{
$sqlFiles = glob(__DIR__ . '/db/migrations/*.sql') ?: [];
$phpFiles = glob(__DIR__ . '/db/migrations/*.php') ?: [];
$files = array_merge($sqlFiles, $phpFiles);
usort($files, static function (string $left, string $right): int {
return strnatcasecmp(basename($left), basename($right));
});
return $files;
}
function splitSqlStatements(string $sql): array
{
$sql = preg_replace('/^\xEF\xBB\xBF/', '', $sql) ?? $sql;
$sql = preg_replace('/\/\*.*?\*\//s', '', $sql) ?? $sql;
$lines = preg_split('/\R/', $sql) ?: [];
$filteredLines = [];
foreach ($lines as $line) {
$trimmed = ltrim($line);
if ($trimmed === '' || str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) {
continue;
}
$filteredLines[] = $line;
}
$cleanSql = implode("\n", $filteredLines);
$statements = [];
$buffer = '';
$inSingleQuote = false;
$inDoubleQuote = false;
$length = strlen($cleanSql);
for ($index = 0; $index < $length; $index++) {
$char = $cleanSql[$index];
$previous = $index > 0 ? $cleanSql[$index - 1] : '';
if ($char === "'" && !$inDoubleQuote && $previous !== '\\') {
$inSingleQuote = !$inSingleQuote;
} elseif ($char === '"' && !$inSingleQuote && $previous !== '\\') {
$inDoubleQuote = !$inDoubleQuote;
}
if ($char === ';' && !$inSingleQuote && !$inDoubleQuote) {
$statement = trim($buffer);
if ($statement !== '') {
$statements[] = $statement;
}
$buffer = '';
continue;
}
$buffer .= $char;
}
$tail = trim($buffer);
if ($tail !== '') {
$statements[] = $tail;
}
return $statements;
}
function isIgnorableMigrationError(PDOException $exception): bool
{
$driverCode = isset($exception->errorInfo[1]) ? (int) $exception->errorInfo[1] : null;
$message = strtolower($exception->getMessage());
$ignorableCodes = [1050, 1060, 1061, 1062, 1091, 1826];
$ignorableSnippets = [
'already exists',
'duplicate column name',
'duplicate key name',
'duplicate entry',
'duplicate foreign key constraint name',
'check that column/key exists',
];
if ($driverCode !== null && in_array($driverCode, $ignorableCodes, true)) {
return true;
}
foreach ($ignorableSnippets as $snippet) {
if (str_contains($message, $snippet)) {
return true;
}
}
return false;
}
function executeSqlMigration(PDO $pdo, string $filePath): void
{
$sql = file_get_contents($filePath);
if ($sql === false) {
throw new RuntimeException('Unable to read SQL migration: ' . basename($filePath));
}
$statements = splitSqlStatements($sql);
if ($statements === []) {
migrationOutput(' - no executable SQL statements found');
return;
}
foreach ($statements as $number => $statement) {
try {
$pdo->exec($statement);
} catch (PDOException $exception) {
if (isIgnorableMigrationError($exception)) {
migrationOutput(' - skipped statement ' . ($number + 1) . ': ' . $exception->getMessage());
continue;
}
throw new RuntimeException(
'SQL migration failed in ' . basename($filePath) . ' at statement ' . ($number + 1) . ': ' . $exception->getMessage(),
0,
$exception
);
}
}
}
function executePhpMigration(string $filePath): void
{
ob_start();
try {
$result = include $filePath;
} catch (Throwable $throwable) {
ob_end_clean();
throw new RuntimeException('PHP migration failed in ' . basename($filePath) . ': ' . $throwable->getMessage(), 0, $throwable);
}
$output = trim((string) ob_get_clean());
if ($result === false) {
throw new RuntimeException('PHP migration returned false: ' . basename($filePath));
}
if ($output !== '') {
foreach (preg_split('/\R/', $output) ?: [] as $line) {
migrationOutput(' ' . $line);
}
}
}
function recordMigration(PDO $pdo, string $migrationName): void
{
$stmt = $pdo->prepare('INSERT INTO migrations (migration) VALUES (:migration)');
$stmt->execute(['migration' => $migrationName]);
}
function runAllMigrations(): int
{
if (!canRunMigrations()) {
if (PHP_SAPI !== 'cli') {
http_response_code(403);
}
migrationOutput('Forbidden: run migrate.php from CLI, localhost, or while logged in as an Administrator.');
return 1;
}
try {
$pdo = db();
ensureMigrationsTable($pdo);
$executed = getExecutedMigrations($pdo);
$files = getMigrationFiles();
if ($files === []) {
migrationOutput('No migration files found in db/migrations.');
return 0;
}
$applied = 0;
$skipped = 0;
migrationOutput('Starting migration run...');
foreach ($files as $filePath) {
$migrationName = basename($filePath);
if (isset($executed[$migrationName])) {
migrationOutput('SKIP ' . $migrationName);
$skipped++;
continue;
}
migrationOutput('RUN ' . $migrationName);
$extension = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION));
if ($extension === 'sql') {
executeSqlMigration($pdo, $filePath);
} elseif ($extension === 'php') {
executePhpMigration($filePath);
} else {
throw new RuntimeException('Unsupported migration type: ' . $migrationName);
}
recordMigration($pdo, $migrationName);
$executed[$migrationName] = true;
$applied++;
migrationOutput('OK ' . $migrationName);
}
migrationOutput('Done. Applied ' . $applied . ' migration(s), skipped ' . $skipped . ' already executed.');
return 0;
} catch (Throwable $throwable) {
if (PHP_SAPI !== 'cli') {
http_response_code(500);
}
migrationOutput('ERROR: ' . $throwable->getMessage());
return 1;
}
}
exit(runAllMigrations());

View File

@ -1,62 +1,2 @@
<?php <?php
require_once 'db/config.php'; require_once __DIR__ . '/migrate.php';
try {
$pdo = db();
// Check if migrations table exists
$stmt = $pdo->query("SHOW TABLES LIKE 'migrations'");
if ($stmt->rowCount() == 0) {
$pdo->exec("CREATE TABLE migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration VARCHAR(255) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
echo "Created migrations table.\n";
}
$stmt = $pdo->query("SELECT migration FROM migrations");
$executed = $stmt->fetchAll(PDO::FETCH_COLUMN);
$files = glob('db/migrations/*.sql');
sort($files);
foreach ($files as $file) {
$migrationName = basename($file);
if (!in_array($migrationName, $executed)) {
echo "Executing: $migrationName...\n";
$sql = file_get_contents($file);
try {
$pdo->exec($sql);
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
$stmt->execute([$migrationName]);
echo "Success: $migrationName\n";
} catch (PDOException $e) {
echo "Error executing $migrationName: " . $e->getMessage() . "\n";
}
}
}
} catch (Exception $e) {
echo "DB Error: " . $e->getMessage() . "\n";
}
$php_files = glob('db/migrations/*.php');
sort($php_files);
foreach ($php_files as $pfile) {
$migrationName = basename($pfile);
if (!in_array($migrationName, $executed)) {
echo "Executing PHP: $migrationName...\n";
try {
include $pfile;
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
$stmt->execute([$migrationName]);
echo "Success: $migrationName\n";
} catch (Exception $e) {
echo "Error executing $migrationName: " . $e->getMessage() . "\n";
}
}
}
echo "Done checking migrations.\n";