Migrator.php 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <?php
  2. declare(strict_types=1);
  3. namespace Glacier\Core;
  4. use PDO;
  5. /**
  6. * SQL 迁移执行器:database/ 下 0001_xxx.sql 顺序执行(单库多表)。
  7. * 执行记录写入 g_migrations 表,重复执行幂等(事务内原子应用)。
  8. */
  9. final class Migrator
  10. {
  11. private PDO $pdo;
  12. private string $dir;
  13. public function __construct(PDO $pdo, string $dir)
  14. {
  15. $this->pdo = $pdo;
  16. $this->dir = rtrim($dir, '/\\');
  17. }
  18. /**
  19. * 应用所有未执行的迁移,返回本次执行的迁移名列表。
  20. * 注意:MySQL/MariaDB 的 DDL(CREATE/ALTER)会隐式提交,事务包裹无意义,
  21. * 故直接顺序执行;迁移 SQL 遵循幂等约定(IF NOT EXISTS / ON DUPLICATE KEY 等),
  22. * 失败重跑安全。
  23. */
  24. public function up(): array
  25. {
  26. $this->ensureTable();
  27. $applied = $this->applied();
  28. $executed = [];
  29. foreach ($this->files() as $name => $sql) {
  30. if (isset($applied[$name])) {
  31. continue;
  32. }
  33. try {
  34. $this->pdo->exec($sql);
  35. $stmt = $this->pdo->prepare('INSERT INTO g_migrations (name) VALUES (?)');
  36. $stmt->execute([$name]);
  37. $executed[] = $name;
  38. } catch (\Throwable $e) {
  39. throw new \RuntimeException("迁移 {$name} 失败: " . $e->getMessage(), 0, $e);
  40. }
  41. }
  42. return $executed;
  43. }
  44. /** 全部迁移的应用状态。 */
  45. public function status(): array
  46. {
  47. $this->ensureTable();
  48. $applied = $this->applied();
  49. $result = [];
  50. foreach ($this->files() as $name => $sql) {
  51. $result[] = ['name' => $name, 'applied' => isset($applied[$name])];
  52. }
  53. return $result;
  54. }
  55. /** @return array<string,string> 按编号排序的迁移 SQL。 */
  56. private function files(): array
  57. {
  58. $files = [];
  59. foreach (glob($this->dir . '/*.sql') ?: [] as $file) {
  60. if (str_ends_with($file, '.down.sql')) {
  61. continue; // 回滚文件由 down 流程单独处理(本期未启用)
  62. }
  63. $files[basename($file)] = (string) file_get_contents($file);
  64. }
  65. ksort($files);
  66. return $files;
  67. }
  68. /** @return array<string,bool> 已应用迁移名集合。 */
  69. private function applied(): array
  70. {
  71. $rows = $this->pdo->query('SELECT name FROM g_migrations')->fetchAll(PDO::FETCH_COLUMN);
  72. return array_fill_keys($rows, true);
  73. }
  74. private function ensureTable(): void
  75. {
  76. $this->pdo->exec(
  77. "CREATE TABLE IF NOT EXISTS g_migrations (
  78. id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  79. name VARCHAR(255) NOT NULL UNIQUE,
  80. applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
  81. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
  82. );
  83. }
  84. }