120 lines
3.9 KiB
PHP
120 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Core;
|
|
|
|
use PDO;
|
|
use Exception;
|
|
|
|
class MigrationRunner
|
|
{
|
|
private PDO $pdo;
|
|
private string $migrationsDir;
|
|
|
|
/**
|
|
* @param mixed $db Either a PDO instance or the application's Database wrapper
|
|
* @param string $migrationsDir Directory containing .sql migrations
|
|
*/
|
|
public function __construct($db, string $migrationsDir)
|
|
{
|
|
// Normalize to PDO
|
|
if ($db instanceof PDO) {
|
|
$this->pdo = $db;
|
|
} elseif (is_object($db) && method_exists($db, 'getConnection')) {
|
|
$pdo = $db->getConnection();
|
|
if (!$pdo instanceof PDO) {
|
|
throw new Exception('Database wrapper did not return a PDO instance');
|
|
}
|
|
$this->pdo = $pdo;
|
|
} else {
|
|
$type = is_object($db) ? get_class($db) : gettype($db);
|
|
throw new Exception("Unsupported database type: {$type}");
|
|
}
|
|
$this->migrationsDir = rtrim($migrationsDir, '/');
|
|
if (!is_dir($this->migrationsDir)) {
|
|
throw new Exception("Migrations directory not found: {$this->migrationsDir}");
|
|
}
|
|
$this->ensureMigrationsTable();
|
|
}
|
|
|
|
private function ensureMigrationsTable(): void
|
|
{
|
|
$driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
|
if ($driver === 'sqlite') {
|
|
$sql = "CREATE TABLE IF NOT EXISTS migrations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
migration TEXT NOT NULL UNIQUE,
|
|
applied_at TEXT NOT NULL
|
|
)";
|
|
} else {
|
|
$sql = "CREATE TABLE IF NOT EXISTS migrations (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
migration VARCHAR(255) NOT NULL UNIQUE,
|
|
applied_at DATETIME NOT NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
|
}
|
|
$this->pdo->exec($sql);
|
|
}
|
|
|
|
public function listAllMigrations(): array
|
|
{
|
|
$files = glob($this->migrationsDir . '/*.sql');
|
|
sort($files, SORT_NATURAL);
|
|
return array_map('basename', $files);
|
|
}
|
|
|
|
public function listAppliedMigrations(): array
|
|
{
|
|
$stmt = $this->pdo->query('SELECT migration FROM migrations ORDER BY migration ASC');
|
|
return $stmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
|
}
|
|
|
|
public function listPendingMigrations(): array
|
|
{
|
|
$all = $this->listAllMigrations();
|
|
$applied = $this->listAppliedMigrations();
|
|
return array_values(array_diff($all, $applied));
|
|
}
|
|
|
|
public function hasPendingMigrations(): bool
|
|
{
|
|
return count($this->listPendingMigrations()) > 0;
|
|
}
|
|
|
|
public function applyPendingMigrations(): array
|
|
{
|
|
$pending = $this->listPendingMigrations();
|
|
$appliedNow = [];
|
|
if (empty($pending)) {
|
|
return $appliedNow;
|
|
}
|
|
|
|
try {
|
|
$this->pdo->beginTransaction();
|
|
foreach ($pending as $migration) {
|
|
$path = $this->migrationsDir . '/' . $migration;
|
|
$sql = file_get_contents($path);
|
|
if ($sql === false) {
|
|
throw new Exception("Unable to read migration file: {$migration}");
|
|
}
|
|
// Split on ; at line ends, but allow inside procedures? Keep simple for our use-cases
|
|
$statements = array_filter(array_map('trim', preg_split('/;\s*\n/', $sql)));
|
|
foreach ($statements as $stmtSql) {
|
|
if ($stmtSql === '') continue;
|
|
$this->pdo->exec($stmtSql);
|
|
}
|
|
$ins = $this->pdo->prepare('INSERT INTO migrations (migration, applied_at) VALUES (:m, NOW())');
|
|
$ins->execute([':m' => $migration]);
|
|
$appliedNow[] = $migration;
|
|
}
|
|
$this->pdo->commit();
|
|
} catch (Exception $e) {
|
|
if ($this->pdo->inTransaction()) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
|
|
return $appliedNow;
|
|
}
|
|
}
|