migratonsDatabaseFile = "migrations-{$key}.sqlite"; $this->db = Database::getInstance( type: 'sqlite', name: static::MIGRATIONS_PATH . $this->migratonsDatabaseFile, ); } #[Override] public function run(): void { $args = func_get_args(); $this->ensureDatabaseExists(); $command = $args[0] ?? ''; switch ($command) { case 'migrate': $this->migrate(); break; case 'rollback': $this->rollback((int)($args[1] ?? 0)); break; case 'create': $this->create((string)($args[1] ?? 'migration')); break; case 'fresh': $this->fresh(); break; case 'clear': $this->dropAllTables(); break; default: $this->log('Invalid migration command.'); break; } } /** * Ensures that the migrations table exists in the database. * * @return void */ public function ensureDatabaseExists(): void { $this->db->query( "CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY, migration TEXT, batch INTEGER)" ); } /** * Runs all pending migrations. * * @return void */ public function migrate(): void { $files = glob(rtrim(static::MIGRATIONS_PATH, '/') . '/*.php'); $quantity = 1; foreach ($files as $file) { // Verificamos si la migración ya ha sigo realizada $filename = basename($file); $prepared = $this->db->prepare("SELECT id FROM migrations WHERE migration = ?"); $prepared->execute([$filename]); if ((bool) $prepared->fetch()) { continue; } // Obtener la migración $migration = require $file; if ($migration instanceof LibsMigration) { if ($this->verbose) { $this->log('Running migration: %s', $filename); } $migration->up(); // agregar la migracion a la base de datos $stmt = $this->db->prepare("INSERT INTO migrations (migration, batch) VALUES (?, ?)"); $stmt->execute([$filename, $quantity]); $quantity++; } } $this->log("Migrations completed successfully."); } /** * Rolls back a specified number of migrations or by default rollback lates batch of it. * * @param int $steps The number of migrations to rollback. * * @return void */ public function rollback(int $steps = 0): void { if ($steps <= 0) { $steps = $this->db->query( "SELECT batch FROM migrations ORDER BY id DESC LIMIT 1" )->fetchColumn(); if ($steps === false) { $this->log("No migrations to rollback."); return; } } $stmt = $this->db->query("SELECT migration FROM migrations ORDER BY id DESC LIMIT $steps"); $migrations = $stmt->fetchAll(PDO::FETCH_COLUMN); foreach ($migrations as $filename) { $file = static::MIGRATIONS_PATH . $filename; if (file_exists($file)) { $migration = require $file; if ($migration instanceof LibsMigration) { $this->log('Rolling back migration: %s', $filename); $migration->down(); } } $this->db->prepare("DELETE FROM migrations WHERE migration = ?")->execute([$filename]); } $this->log('Rollback completed successfully.'); } /** * Creates a new migration file. * * @param string $name * * @return void */ public function create(string $name = 'migration'): void { $filename = date('Y_m_d_His') . '_' . $name . '.php'; $filepath = static::MIGRATIONS_PATH . $filename; $template = <<<'TEMPLATE' log('%s migration created.', $filename); } /** * Refreshes the database by deleting the database and running all migrations. * * @return void */ public function fresh(): void { $this->dropAllTables(); $this->migrate(); $this->log("Database refreshed successfully."); } /** * Drops all tables from the main database and clears the migrations control table. * * @return void * @throws \Exception If the database driver is not supported. */ private function dropAllTables(): void { if (DB_TYPE == 'sqlite') { $mainDatabase = Database::getInstance( type: DB_TYPE, name: DB_NAME ); } else { $mainDatabase = Database::getInstance( DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASS ); } $driver = $mainDatabase->getAttribute(PDO::ATTR_DRIVER_NAME); // Query to get all tables based on the driver $query = match ($driver) { 'mysql' => "SHOW TABLES", 'pgsql' => "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'", 'sqlite' => "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", default => throw new \Exception("Driver $driver not supported for fresh command"), }; $stmt = $mainDatabase->query($query); $tables = $stmt->fetchAll(PDO::FETCH_COLUMN); foreach ($tables as $table) { // Disable foreign key checks to avoid constraint errors if ($driver === 'mysql') { $mainDatabase->exec("SET FOREIGN_KEY_CHECKS = 0"); } if ($driver === 'pgsql') { $mainDatabase->exec("SET session_replication_role = 'replica'"); } $quotedTable = match ($driver) { 'mysql' => "`$table`", 'pgsql', 'sqlite' => "\"$table\"", default => $table, }; $cascade = $driver === 'sqlite' ? '' : ' CASCADE'; $mainDatabase->exec("DROP TABLE IF EXISTS $quotedTable$cascade"); } if ($driver === 'mysql') { $mainDatabase->exec("SET FOREIGN_KEY_CHECKS = 1"); } if ($driver === 'pgsql') { $mainDatabase->exec("SET session_replication_role = 'origin'"); } // Clear the migration control table in the SQLite DB $this->db->exec("DELETE FROM migrations"); $this->log("Database schema cleared. Ready for new migrations."); } private function log(string $msg, mixed ...$args): void { if ($this->verbose) { printf("$msg\n", ...$args); } } }