選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

234 行
6.3KB

  1. <?php
  2. declare(strict_types=1);
  3. namespace Core;
  4. class MigrationManager
  5. {
  6. protected Database $database;
  7. protected string $path;
  8. public function __construct(Database $database, string $path)
  9. {
  10. $this->database = $database;
  11. $this->path = rtrim($path, '/');
  12. }
  13. public function ensureTable(): void
  14. {
  15. $this->database->execute(
  16. 'CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, migration VARCHAR(255) NOT NULL, ran_at DATETIME DEFAULT CURRENT_TIMESTAMP)'
  17. );
  18. $this->database->execute(
  19. 'CREATE UNIQUE INDEX IF NOT EXISTS idx_migrations_migration_unique ON migrations (migration)'
  20. );
  21. }
  22. public function status(): array
  23. {
  24. $this->ensureTable();
  25. $ran = $this->database->query('SELECT migration, ran_at FROM migrations ORDER BY id ASC');
  26. $ranByName = [];
  27. foreach ($ran as $row) {
  28. $ranByName[$row['migration']] = $row['ran_at'];
  29. }
  30. $files = $this->migrationFiles();
  31. $status = [];
  32. foreach ($files as $file) {
  33. $name = basename($file);
  34. $status[] = [
  35. 'migration' => $name,
  36. 'ran' => array_key_exists($name, $ranByName),
  37. 'ran_at' => $ranByName[$name] ?? null,
  38. ];
  39. }
  40. return $status;
  41. }
  42. public function runPending(): array
  43. {
  44. $this->ensureTable();
  45. $ran = $this->database->query('SELECT migration FROM migrations');
  46. $ranNames = array_column($ran, 'migration');
  47. $files = $this->migrationFiles();
  48. $ranMigrations = [];
  49. foreach ($files as $file) {
  50. $name = basename($file);
  51. if (in_array($name, $ranNames, true)) {
  52. continue;
  53. }
  54. $migration = $this->loadMigration($file);
  55. $this->database->transaction(function (Database $db) use ($migration, $name, &$ranMigrations): void {
  56. $migration->up($db);
  57. if ($db->first('SELECT id FROM migrations WHERE migration = :migration', ['migration' => $name]) === null) {
  58. $db->execute('INSERT INTO migrations (migration) VALUES (:migration)', ['migration' => $name]);
  59. }
  60. $ranMigrations[] = $name;
  61. });
  62. }
  63. return $ranMigrations;
  64. }
  65. public function rollback(int $steps = 1): array
  66. {
  67. $this->ensureTable();
  68. $steps = max(1, $steps);
  69. $applied = $this->database->query(
  70. 'SELECT id, migration FROM migrations ORDER BY id DESC LIMIT :steps',
  71. ['steps' => $steps]
  72. );
  73. $rolledBack = [];
  74. foreach ($applied as $row) {
  75. $file = $this->path . '/' . $row['migration'];
  76. if (!file_exists($file)) {
  77. throw new \RuntimeException("Migration file not found for rollback: {$row['migration']}");
  78. }
  79. $migration = $this->loadMigration($file);
  80. $this->database->transaction(function (Database $db) use ($migration, $row, &$rolledBack): void {
  81. $migration->down($db);
  82. $db->execute('DELETE FROM migrations WHERE id = :id', ['id' => $row['id']]);
  83. $rolledBack[] = $row['migration'];
  84. });
  85. }
  86. return $rolledBack;
  87. }
  88. public function fresh(): array
  89. {
  90. $this->ensureTable();
  91. $files = array_reverse($this->migrationFiles());
  92. $rolledBack = [];
  93. foreach ($files as $file) {
  94. $migration = $this->loadMigration($file);
  95. $name = basename($file);
  96. $this->database->transaction(function (Database $db) use ($migration, $name, &$rolledBack): void {
  97. $migration->down($db);
  98. $rolledBack[] = $name;
  99. });
  100. }
  101. $this->database->execute('DELETE FROM migrations');
  102. $ran = $this->runPending();
  103. return [
  104. 'rolled_back' => $rolledBack,
  105. 'migrated' => $ran,
  106. ];
  107. }
  108. public function make(string $name): string
  109. {
  110. $slug = trim(strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', $name) ?? ''), '_');
  111. if ($slug === '') {
  112. throw new \InvalidArgumentException('Migration name must contain letters or numbers.');
  113. }
  114. if (!is_dir($this->path)) {
  115. mkdir($this->path, 0777, true);
  116. }
  117. $timestamp = date('Ymd_His');
  118. $filename = $timestamp . '_' . $slug . '.php';
  119. $path = $this->path . '/' . $filename;
  120. if (file_exists($path)) {
  121. throw new \RuntimeException("Migration already exists: {$filename}");
  122. }
  123. $template = <<<PHP
  124. <?php
  125. declare(strict_types=1);
  126. use Core\Database;
  127. use Core\Migration;
  128. return new class extends Migration
  129. {
  130. public function up(Database \$database): void
  131. {
  132. // Write the forward migration here.
  133. }
  134. public function down(Database \$database): void
  135. {
  136. // Write the rollback migration here.
  137. }
  138. };
  139. PHP;
  140. file_put_contents($path, $template . PHP_EOL);
  141. return $path;
  142. }
  143. private function migrationFiles(): array
  144. {
  145. $files = glob($this->path . '/*.php') ?: [];
  146. sort($files);
  147. return $files;
  148. }
  149. private function loadMigration(string $file): Migration
  150. {
  151. $migration = require $file;
  152. if ($migration instanceof Migration) {
  153. return $migration;
  154. }
  155. if (is_callable($migration)) {
  156. return new class ($migration, basename($file)) extends Migration
  157. {
  158. private $callback;
  159. private string $name;
  160. public function __construct(callable $callback, string $name)
  161. {
  162. $this->callback = $callback;
  163. $this->name = $name;
  164. }
  165. public function up(Database $database): void
  166. {
  167. ($this->callback)($database);
  168. }
  169. public function down(Database $database): void
  170. {
  171. throw new \RuntimeException("Migration {$this->name} cannot be rolled back because it has no down() method.");
  172. }
  173. };
  174. }
  175. throw new \RuntimeException('Migration files must return a Migration instance.');
  176. }
  177. }

Powered by TurnKey Linux.