BREAKING: project commands implementing CommandInterface must define static describe(): string (intentional, forces documentation).
265 lines
7.8 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|