add migrate.php
This commit is contained in:
parent
ff296e8bc2
commit
c46f356c7c
273
migrate.php
Normal file
273
migrate.php
Normal 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());
|
||||
@ -1,62 +1,2 @@
|
||||
<?php
|
||||
require_once 'db/config.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";
|
||||
require_once __DIR__ . '/migrate.php';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user