first commit.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace Commands;
|
||||
|
||||
use Interfaces\CommandInterface;
|
||||
use Libs\Database;
|
||||
use Libs\Migration as LibsMigration;
|
||||
use Override;
|
||||
use PDO;
|
||||
|
||||
class Migration implements CommandInterface
|
||||
{
|
||||
private const MIGRATIONS_PATH = ROOT_DIR . '/migrations/';
|
||||
private string $migratonsDatabaseFile;
|
||||
private PDO $db;
|
||||
|
||||
public function __construct(public bool $verbose = true)
|
||||
{
|
||||
$key = md5(DB_TYPE . '/' . DB_HOST . '/' . DB_NAME . '/' . DB_USER);
|
||||
$this->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'
|
||||
<?php
|
||||
|
||||
use Libs\Migration;
|
||||
|
||||
return new class () extends Migration {
|
||||
#[Override]
|
||||
public function up(): void
|
||||
{
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function down(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
TEMPLATE;
|
||||
|
||||
file_put_contents($filepath, $template);
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user