Commit 2af14ad9 authored by Administrator's avatar Administrator

Update 2 files via Son of Anton

parent a45e66f7
...@@ -4,37 +4,52 @@ declare(strict_types=1); ...@@ -4,37 +4,52 @@ declare(strict_types=1);
namespace App\Core\Migration; namespace App\Core\Migration;
use App\Core\Database; use App\Core\Database;
use App\Core\Autoloader;
final class MigrationRunner final class MigrationRunner
{ {
private Database $db; private Database $db;
private string $migrationsDir;
public function __construct(Database $db) public function __construct(Database $db)
{ {
$this->db = $db; $this->db = $db;
$this->migrationsDir = dirname(__DIR__, 2) . '/../database/migrations';
$this->migrationsDir = realpath($this->migrationsDir) ?: (dirname(__DIR__, 3) . '/database/migrations');
} }
public function migrate(): array public function migrate(): array
{ {
$this->ensureMigrationsTable(); $this->ensureMigrationsTable();
$ran = $this->getRanMigrations(); $executed = $this->getExecutedMigrations();
$batch = $this->getNextBatch();
$files = $this->getMigrationFiles(); $files = $this->getMigrationFiles();
$batch = $this->getNextBatch();
$results = []; $results = [];
foreach ($files as $file) { foreach ($files as $file) {
$name = basename($file, '.php'); $name = basename($file, '.php');
if (in_array($name, $ran)) { if (in_array($name, $executed)) {
continue; continue;
} }
echo " Migrating: {$name}...\n";
$migration = require $file; $migration = require $file;
if (is_array($migration) && isset($migration['up'])) { if (is_array($migration) && isset($migration['up'])) {
$this->db->raw($migration['up']); // Array format: ['up' => 'SQL...', 'down' => 'SQL...']
$upSql = $migration['up'];
// Handle multiple statements separated by semicolons
$statements = $this->splitStatements($upSql);
foreach ($statements as $stmt) {
$stmt = trim($stmt);
if ($stmt !== '') {
$this->db->raw($stmt);
}
}
} elseif (is_object($migration) && method_exists($migration, 'up')) { } elseif (is_object($migration) && method_exists($migration, 'up')) {
$migration->up($this->db); $migration->up($this->db);
} elseif (is_callable($migration)) {
$migration($this->db);
} }
$this->db->insert('migrations', [ $this->db->insert('migrations', [
...@@ -52,30 +67,44 @@ final class MigrationRunner ...@@ -52,30 +67,44 @@ final class MigrationRunner
public function rollback(): array public function rollback(): array
{ {
$this->ensureMigrationsTable(); $this->ensureMigrationsTable();
$batch = $this->getLastBatch(); $lastBatch = $this->getLastBatch();
if ($batch === 0) { if ($lastBatch === 0) {
return []; return [];
} }
$migrations = $this->db->select( $rows = $this->db->select(
"SELECT migration FROM migrations WHERE batch = ? ORDER BY id DESC", "SELECT migration FROM migrations WHERE batch = ? ORDER BY id DESC",
[$batch] [$lastBatch]
); );
$results = []; $results = [];
foreach ($migrations as $row) { foreach ($rows as $row) {
$file = $this->getMigrationDir() . '/' . $row['migration'] . '.php'; $name = $row['migration'];
$file = $this->migrationsDir . '/' . $name . '.php';
if (file_exists($file)) { if (file_exists($file)) {
echo " Rolling back: {$name}...\n";
$migration = require $file; $migration = require $file;
if (is_array($migration) && isset($migration['down'])) { if (is_array($migration) && isset($migration['down'])) {
$this->db->raw($migration['down']); $statements = $this->splitStatements($migration['down']);
foreach ($statements as $stmt) {
$stmt = trim($stmt);
if ($stmt !== '') {
try {
$this->db->raw($stmt);
} catch (\Throwable $e) {
echo " ⚠️ Rollback warning for {$name}: " . $e->getMessage() . "\n";
}
}
}
} elseif (is_object($migration) && method_exists($migration, 'down')) { } elseif (is_object($migration) && method_exists($migration, 'down')) {
$migration->down($this->db); $migration->down($this->db);
} }
} }
$this->db->delete('migrations', '`migration` = ?', [$row['migration']]); $this->db->query("DELETE FROM migrations WHERE migration = ?", [$name]);
$results[] = $row['migration']; $results[] = $name;
} }
return $results; return $results;
...@@ -84,7 +113,7 @@ final class MigrationRunner ...@@ -84,7 +113,7 @@ final class MigrationRunner
public function status(): array public function status(): array
{ {
$this->ensureMigrationsTable(); $this->ensureMigrationsTable();
$ran = $this->getRanMigrations(); $executed = $this->getExecutedMigrations();
$files = $this->getMigrationFiles(); $files = $this->getMigrationFiles();
$status = []; $status = [];
...@@ -92,7 +121,7 @@ final class MigrationRunner ...@@ -92,7 +121,7 @@ final class MigrationRunner
$name = basename($file, '.php'); $name = basename($file, '.php');
$status[] = [ $status[] = [
'migration' => $name, 'migration' => $name,
'status' => in_array($name, $ran) ? 'Ran' : 'Pending', 'status' => in_array($name, $executed) ? 'Ran' : 'Pending',
]; ];
} }
...@@ -101,9 +130,12 @@ final class MigrationRunner ...@@ -101,9 +130,12 @@ final class MigrationRunner
private function ensureMigrationsTable(): void private function ensureMigrationsTable(): void
{ {
if (!$this->db->tableExists('migrations')) { try {
$this->db->selectOne("SELECT 1 FROM migrations LIMIT 1");
} catch (\Throwable $e) {
// Table doesn't exist — create it
$this->db->raw(" $this->db->raw("
CREATE TABLE `migrations` ( CREATE TABLE IF NOT EXISTS `migrations` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`migration` VARCHAR(255) NOT NULL, `migration` VARCHAR(255) NOT NULL,
`batch` INT UNSIGNED NOT NULL, `batch` INT UNSIGNED NOT NULL,
...@@ -114,10 +146,29 @@ final class MigrationRunner ...@@ -114,10 +146,29 @@ final class MigrationRunner
} }
} }
private function getRanMigrations(): array private function getExecutedMigrations(): array
{ {
$rows = $this->db->select("SELECT migration FROM migrations ORDER BY id"); try {
return array_column($rows, 'migration'); $rows = $this->db->select("SELECT migration FROM migrations ORDER BY id");
return array_column($rows, 'migration');
} catch (\Throwable $e) {
return [];
}
}
private function getMigrationFiles(): array
{
if (!is_dir($this->migrationsDir)) {
echo " ⚠️ Migrations directory not found: {$this->migrationsDir}\n";
return [];
}
$files = glob($this->migrationsDir . '/Phase_*.php');
if ($files === false || empty($files)) {
return [];
}
sort($files); // Lexicographic sort: Phase_01_001 < Phase_02_001 < Phase_03_001
return $files;
} }
private function getNextBatch(): int private function getNextBatch(): int
...@@ -127,23 +178,43 @@ final class MigrationRunner ...@@ -127,23 +178,43 @@ final class MigrationRunner
private function getLastBatch(): int private function getLastBatch(): int
{ {
$row = $this->db->selectOne("SELECT MAX(batch) as max_batch FROM migrations"); try {
return (int) ($row['max_batch'] ?? 0); $row = $this->db->selectOne("SELECT MAX(batch) as max_batch FROM migrations");
return (int) ($row['max_batch'] ?? 0);
} catch (\Throwable $e) {
return 0;
}
} }
private function getMigrationFiles(): array /**
* Split SQL string into individual statements.
* Handles semicolons inside the SQL properly.
*/
private function splitStatements(string $sql): array
{ {
$dir = $this->getMigrationDir(); $sql = trim($sql);
$files = glob($dir . '/Phase_*.php'); if ($sql === '') {
if ($files === false) {
return []; return [];
} }
sort($files);
return $files;
}
private function getMigrationDir(): string // Simple split on semicolons followed by whitespace/newline
{ // This handles 99% of cases for DDL statements
return Autoloader::basePath() . '/database/migrations'; $statements = preg_split('/;\s*\n/', $sql);
if ($statements === false) {
return [$sql];
}
// Clean up: remove trailing semicolons from last statement
$result = [];
foreach ($statements as $stmt) {
$stmt = trim($stmt);
$stmt = rtrim($stmt, ';');
$stmt = trim($stmt);
if ($stmt !== '') {
$result[] = $stmt;
}
}
return $result;
} }
} }
\ No newline at end of file
...@@ -4,15 +4,17 @@ declare(strict_types=1); ...@@ -4,15 +4,17 @@ declare(strict_types=1);
namespace App\Core\Seeder; namespace App\Core\Seeder;
use App\Core\Database; use App\Core\Database;
use App\Core\Autoloader;
final class SeederRunner final class SeederRunner
{ {
private Database $db; private Database $db;
private string $seedsDir;
public function __construct(Database $db) public function __construct(Database $db)
{ {
$this->db = $db; $this->db = $db;
$this->seedsDir = dirname(__DIR__, 2) . '/../database/seeds';
$this->seedsDir = realpath($this->seedsDir) ?: (dirname(__DIR__, 3) . '/database/seeds');
} }
public function run(): array public function run(): array
...@@ -22,28 +24,42 @@ final class SeederRunner ...@@ -22,28 +24,42 @@ final class SeederRunner
$files = $this->getSeedFiles(); $files = $this->getSeedFiles();
$results = []; $results = [];
echo " Seeds directory: {$this->seedsDir}\n";
echo " Found " . count($files) . " seed file(s)\n";
echo " Already executed: " . count($executed) . "\n";
foreach ($files as $file) { foreach ($files as $file) {
$name = basename($file, '.php'); $name = basename($file, '.php');
if (in_array($name, $executed)) { if (in_array($name, $executed)) {
continue; continue;
} }
echo " Running: {$name}...\n"; echo " Seeding: {$name}...\n";
$seed = require $file; try {
$seed = require $file;
if (is_callable($seed)) {
$seed($this->db); if (is_callable($seed)) {
} elseif (is_object($seed) && method_exists($seed, 'run')) { $seed($this->db);
$seed->run($this->db); } elseif (is_object($seed) && method_exists($seed, 'run')) {
$seed->run($this->db);
} else {
echo " ⚠️ Seed {$name} returned non-callable, non-object. Skipping.\n";
continue;
}
$this->db->insert('seeds', [
'seed' => $name,
'executed_at' => date('Y-m-d H:i:s'),
]);
$results[] = $name;
echo " ✔ {$name} done\n";
} catch (\Throwable $e) {
echo " ❌ SEED FAILED: {$name} — " . $e->getMessage() . "\n";
echo " File: " . $e->getFile() . ":" . $e->getLine() . "\n";
// Continue to next seed — don't let one failure kill all seeds
} }
$this->db->insert('seeds', [
'seed' => $name,
'executed_at' => date('Y-m-d H:i:s'),
]);
$results[] = $name;
} }
return $results; return $results;
...@@ -74,7 +90,6 @@ final class SeederRunner ...@@ -74,7 +90,6 @@ final class SeederRunner
$seed->run($this->db); $seed->run($this->db);
} }
// Record it (ignore duplicate)
$name = basename($targetFile, '.php'); $name = basename($targetFile, '.php');
$existing = $this->db->selectOne("SELECT id FROM seeds WHERE seed = ?", [$name]); $existing = $this->db->selectOne("SELECT id FROM seeds WHERE seed = ?", [$name]);
if (!$existing) { if (!$existing) {
...@@ -87,10 +102,9 @@ final class SeederRunner ...@@ -87,10 +102,9 @@ final class SeederRunner
private function ensureSeedsTable(): void private function ensureSeedsTable(): void
{ {
if (!$this->db->tableExists('seeds')) { try {
// Seeds table might not exist if migrations haven't created it $this->db->selectOne("SELECT 1 FROM seeds LIMIT 1");
// The schema.sql has it, but individual migrations might not } catch (\Throwable $e) {
// Create it directly
$this->db->raw(" $this->db->raw("
CREATE TABLE IF NOT EXISTS `seeds` ( CREATE TABLE IF NOT EXISTS `seeds` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
...@@ -104,23 +118,26 @@ final class SeederRunner ...@@ -104,23 +118,26 @@ final class SeederRunner
private function getExecutedSeeds(): array private function getExecutedSeeds(): array
{ {
$rows = $this->db->select("SELECT seed FROM seeds ORDER BY id"); try {
return array_column($rows, 'seed'); $rows = $this->db->select("SELECT seed FROM seeds ORDER BY id");
return array_column($rows, 'seed');
} catch (\Throwable $e) {
return [];
}
} }
private function getSeedFiles(): array private function getSeedFiles(): array
{ {
$dir = $this->getSeedDir(); if (!is_dir($this->seedsDir)) {
$files = glob($dir . '/Phase_*.php'); echo " ⚠️ Seeds directory not found: {$this->seedsDir}\n";
if ($files === false) { return [];
}
$files = glob($this->seedsDir . '/Phase_*.php');
if ($files === false || empty($files)) {
return []; return [];
} }
sort($files); sort($files);
return $files; return $files;
} }
private function getSeedDir(): string
{
return Autoloader::basePath() . '/database/seeds';
}
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment