|
- <?php
-
- declare(strict_types=1);
-
- namespace Core;
-
- class MigrationManager
- {
- protected Database $database;
- protected string $path;
-
- public function __construct(Database $database, string $path)
- {
- $this->database = $database;
- $this->path = rtrim($path, '/');
- }
-
- public function ensureTable(): void
- {
- $this->database->execute(
- 'CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, migration VARCHAR(255) NOT NULL, ran_at DATETIME DEFAULT CURRENT_TIMESTAMP)'
- );
-
- $this->database->execute(
- 'CREATE UNIQUE INDEX IF NOT EXISTS idx_migrations_migration_unique ON migrations (migration)'
- );
- }
-
- public function status(): array
- {
- $this->ensureTable();
-
- $ran = $this->database->query('SELECT migration, ran_at FROM migrations ORDER BY id ASC');
- $ranByName = [];
-
- foreach ($ran as $row) {
- $ranByName[$row['migration']] = $row['ran_at'];
- }
-
- $files = $this->migrationFiles();
-
- $status = [];
-
- foreach ($files as $file) {
- $name = basename($file);
- $status[] = [
- 'migration' => $name,
- 'ran' => array_key_exists($name, $ranByName),
- 'ran_at' => $ranByName[$name] ?? null,
- ];
- }
-
- return $status;
- }
-
- public function runPending(): array
- {
- $this->ensureTable();
-
- $ran = $this->database->query('SELECT migration FROM migrations');
- $ranNames = array_column($ran, 'migration');
- $files = $this->migrationFiles();
- $ranMigrations = [];
-
- foreach ($files as $file) {
- $name = basename($file);
-
- if (in_array($name, $ranNames, true)) {
- continue;
- }
-
- $migration = $this->loadMigration($file);
-
- $this->database->transaction(function (Database $db) use ($migration, $name, &$ranMigrations): void {
- $migration->up($db);
-
- if ($db->first('SELECT id FROM migrations WHERE migration = :migration', ['migration' => $name]) === null) {
- $db->execute('INSERT INTO migrations (migration) VALUES (:migration)', ['migration' => $name]);
- }
-
- $ranMigrations[] = $name;
- });
- }
-
- return $ranMigrations;
- }
-
- public function rollback(int $steps = 1): array
- {
- $this->ensureTable();
-
- $steps = max(1, $steps);
- $applied = $this->database->query(
- 'SELECT id, migration FROM migrations ORDER BY id DESC LIMIT :steps',
- ['steps' => $steps]
- );
- $rolledBack = [];
-
- foreach ($applied as $row) {
- $file = $this->path . '/' . $row['migration'];
-
- if (!file_exists($file)) {
- throw new \RuntimeException("Migration file not found for rollback: {$row['migration']}");
- }
-
- $migration = $this->loadMigration($file);
-
- $this->database->transaction(function (Database $db) use ($migration, $row, &$rolledBack): void {
- $migration->down($db);
- $db->execute('DELETE FROM migrations WHERE id = :id', ['id' => $row['id']]);
- $rolledBack[] = $row['migration'];
- });
- }
-
- return $rolledBack;
- }
-
- public function fresh(): array
- {
- $this->ensureTable();
-
- $files = array_reverse($this->migrationFiles());
- $rolledBack = [];
-
- foreach ($files as $file) {
- $migration = $this->loadMigration($file);
- $name = basename($file);
-
- $this->database->transaction(function (Database $db) use ($migration, $name, &$rolledBack): void {
- $migration->down($db);
- $rolledBack[] = $name;
- });
- }
-
- $this->database->execute('DELETE FROM migrations');
- $ran = $this->runPending();
-
- return [
- 'rolled_back' => $rolledBack,
- 'migrated' => $ran,
- ];
- }
-
- public function make(string $name): string
- {
- $slug = trim(strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', $name) ?? ''), '_');
-
- if ($slug === '') {
- throw new \InvalidArgumentException('Migration name must contain letters or numbers.');
- }
-
- if (!is_dir($this->path)) {
- mkdir($this->path, 0777, true);
- }
-
- $timestamp = date('Ymd_His');
- $filename = $timestamp . '_' . $slug . '.php';
- $path = $this->path . '/' . $filename;
-
- if (file_exists($path)) {
- throw new \RuntimeException("Migration already exists: {$filename}");
- }
-
- $template = <<<PHP
- <?php
-
- declare(strict_types=1);
-
- use Core\Database;
- use Core\Migration;
-
- return new class extends Migration
- {
- public function up(Database \$database): void
- {
- // Write the forward migration here.
- }
-
- public function down(Database \$database): void
- {
- // Write the rollback migration here.
- }
- };
- PHP;
-
- file_put_contents($path, $template . PHP_EOL);
-
- return $path;
- }
-
- private function migrationFiles(): array
- {
- $files = glob($this->path . '/*.php') ?: [];
- sort($files);
-
- return $files;
- }
-
- private function loadMigration(string $file): Migration
- {
- $migration = require $file;
-
- if ($migration instanceof Migration) {
- return $migration;
- }
-
- if (is_callable($migration)) {
- return new class ($migration, basename($file)) extends Migration
- {
- private $callback;
- private string $name;
-
- public function __construct(callable $callback, string $name)
- {
- $this->callback = $callback;
- $this->name = $name;
- }
-
- public function up(Database $database): void
- {
- ($this->callback)($database);
- }
-
- public function down(Database $database): void
- {
- throw new \RuntimeException("Migration {$this->name} cannot be rolled back because it has no down() method.");
- }
- };
- }
-
- throw new \RuntimeException('Migration files must return a Migration instance.');
- }
- }
|