Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

298 wiersze
8.0KB

  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. $tableExists = $this->database->first(
  16. "SELECT 1 AS tbl FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'migrations'"
  17. );
  18. if (!$tableExists) {
  19. $this->database->execute(
  20. 'CREATE TABLE migrations (
  21. id INT IDENTITY(1,1) NOT NULL,
  22. migration NVARCHAR(255) NOT NULL,
  23. ran_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  24. CONSTRAINT PK_migrations PRIMARY KEY (id),
  25. CONSTRAINT UQ_migrations_migration UNIQUE (migration)
  26. )'
  27. );
  28. }
  29. $this->database->execute(
  30. 'DELETE FROM migrations
  31. WHERE id NOT IN (
  32. SELECT MIN(id)
  33. FROM migrations
  34. GROUP BY migration
  35. )'
  36. );
  37. $files = array_map('basename', $this->migrationFiles());
  38. if ($files === []) {
  39. $this->database->execute('DELETE FROM migrations');
  40. return;
  41. }
  42. $placeholders = [];
  43. $parameters = [];
  44. foreach ($files as $index => $file) {
  45. $placeholder = 'migration_' . $index;
  46. $placeholders[] = ':' . $placeholder;
  47. $parameters[$placeholder] = $file;
  48. }
  49. $this->database->execute(
  50. 'DELETE FROM migrations WHERE migration NOT IN (' . implode(', ', $placeholders) . ')',
  51. $parameters
  52. );
  53. }
  54. public function status(): array
  55. {
  56. $this->ensureTable();
  57. $ran = $this->database->query('SELECT migration, ran_at FROM migrations ORDER BY id ASC');
  58. $ranByName = [];
  59. foreach ($ran as $row) {
  60. $ranByName[$row['migration']] = $row['ran_at'];
  61. }
  62. $files = $this->migrationFiles();
  63. $status = [];
  64. foreach ($files as $file) {
  65. $name = basename($file);
  66. $status[] = [
  67. 'migration' => $name,
  68. 'ran' => array_key_exists($name, $ranByName),
  69. 'ran_at' => $ranByName[$name] ?? null,
  70. ];
  71. }
  72. return $status;
  73. }
  74. public function runPending(): array
  75. {
  76. $this->ensureTable();
  77. $ran = $this->database->query('SELECT migration FROM migrations');
  78. $ranNames = array_column($ran, 'migration');
  79. $files = $this->migrationFiles();
  80. $ranMigrations = [];
  81. foreach ($files as $file) {
  82. $name = basename($file);
  83. if (in_array($name, $ranNames, true)) {
  84. continue;
  85. }
  86. $migration = $this->loadMigration($file);
  87. $this->database->pdo()->beginTransaction();
  88. try {
  89. $migration->up($this->database);
  90. $this->database->execute(
  91. 'INSERT INTO migrations (migration)
  92. SELECT :m1 WHERE NOT EXISTS (SELECT 1 FROM migrations WHERE migration = :m2)',
  93. ['m1' => $name, 'm2' => $name]
  94. );
  95. $this->database->pdo()->commit();
  96. $ranMigrations[] = $name;
  97. } catch (\Throwable $exception) {
  98. $this->database->pdo()->rollBack();
  99. throw $exception;
  100. }
  101. }
  102. return $ranMigrations;
  103. }
  104. public function rollback(int $steps = 1): array
  105. {
  106. $this->ensureTable();
  107. $steps = max(1, $steps);
  108. $applied = $this->database->query(
  109. "SELECT TOP ({$steps}) MAX(id) AS id, migration
  110. FROM migrations
  111. GROUP BY migration
  112. ORDER BY MAX(id) DESC"
  113. );
  114. $rolledBack = [];
  115. foreach ($applied as $row) {
  116. $file = $this->path . '/' . $row['migration'];
  117. if (!file_exists($file)) {
  118. throw new \RuntimeException("Migration file not found for rollback: {$row['migration']}");
  119. }
  120. $migration = $this->loadMigration($file);
  121. $this->database->pdo()->beginTransaction();
  122. try {
  123. $migration->down($this->database);
  124. $this->database->execute(
  125. 'DELETE FROM migrations WHERE id = :id',
  126. ['id' => $row['id']]
  127. );
  128. $this->database->pdo()->commit();
  129. $rolledBack[] = $row['migration'];
  130. } catch (\Throwable $exception) {
  131. $this->database->pdo()->rollBack();
  132. throw $exception;
  133. }
  134. }
  135. return $rolledBack;
  136. }
  137. public function fresh(): array
  138. {
  139. $this->ensureTable();
  140. $files = array_reverse($this->migrationFiles());
  141. $rolledBack = [];
  142. foreach ($files as $file) {
  143. $migration = $this->loadMigration($file);
  144. $name = basename($file);
  145. $this->database->pdo()->beginTransaction();
  146. try {
  147. $migration->down($this->database);
  148. $this->database->pdo()->commit();
  149. $rolledBack[] = $name;
  150. } catch (\Throwable $exception) {
  151. $this->database->pdo()->rollBack();
  152. throw $exception;
  153. }
  154. }
  155. $this->database->execute('DELETE FROM migrations');
  156. $ran = $this->runPending();
  157. return [
  158. 'rolled_back' => $rolledBack,
  159. 'migrated' => $ran,
  160. ];
  161. }
  162. public function make(string $name): string
  163. {
  164. $slug = trim(strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', $name) ?? ''), '_');
  165. if ($slug === '') {
  166. throw new \InvalidArgumentException('Migration name must contain letters or numbers.');
  167. }
  168. if (!is_dir($this->path)) {
  169. mkdir($this->path, 0777, true);
  170. }
  171. $timestamp = date('Ymd_His');
  172. $filename = $timestamp . '_' . $slug . '.php';
  173. $path = $this->path . '/' . $filename;
  174. if (file_exists($path)) {
  175. throw new \RuntimeException("Migration already exists: {$filename}");
  176. }
  177. $template = <<<PHP
  178. <?php
  179. declare(strict_types=1);
  180. use Core\Database;
  181. use Core\Migration;
  182. return new class extends Migration
  183. {
  184. public function up(Database \$database): void
  185. {
  186. // Write the forward migration here.
  187. }
  188. public function down(Database \$database): void
  189. {
  190. // Write the rollback migration here.
  191. }
  192. };
  193. PHP;
  194. file_put_contents($path, $template . PHP_EOL);
  195. return $path;
  196. }
  197. private function migrationFiles(): array
  198. {
  199. $files = glob($this->path . '/*.php') ?: [];
  200. sort($files);
  201. return $files;
  202. }
  203. private function loadMigration(string $file): Migration
  204. {
  205. $migration = require $file;
  206. if ($migration instanceof Migration) {
  207. return $migration;
  208. }
  209. if (is_callable($migration)) {
  210. return new class ($migration, basename($file)) extends Migration
  211. {
  212. private $callback;
  213. private string $name;
  214. public function __construct(callable $callback, string $name)
  215. {
  216. $this->callback = $callback;
  217. $this->name = $name;
  218. }
  219. public function up(Database $database): void
  220. {
  221. ($this->callback)($database);
  222. }
  223. public function down(Database $database): void
  224. {
  225. throw new \RuntimeException("Migration {$this->name} cannot be rolled back because it has no down() method.");
  226. }
  227. };
  228. }
  229. throw new \RuntimeException('Migration files must return a Migration instance.');
  230. }
  231. }

Powered by TurnKey Linux.