Files
duckbrain-toolbox/packages/commands/src/Commands/Migration.php
T
kj c2931245bb feat(cli): pure alias-based dispatcher in project bin; Commands\Toolbox entry point; describe() required on CommandInterface
BREAKING: project commands implementing CommandInterface must define
static describe(): string (intentional, forces documentation).
2026-10-06 15:14:33 -03:00

265 lines
7.8 KiB
PHP

<?php
namespace Commands;
use Interfaces\CommandInterface;
use Libs\Database;
use Libs\Migration as LibsMigration;
use Override;
use PDO;
class Migration implements CommandInterface
{
public static function describe(): string
{
return 'Migraciones de base de datos (create, migrate, rollback, fresh, clear)';
}
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);
}
}
}