- Add comprehensive error handling system with custom error pages - Implement professional enterprise-style design with Tailwind CSS - Create modular HMVC architecture with clean separation of concerns - Add security features: CSRF protection, XSS filtering, Argon2ID hashing - Include CLI tools for development workflow - Add error reporting dashboard with system monitoring - Implement responsive design with consistent slate color scheme - Replace all emoji icons with professional SVG icons - Add comprehensive test suite with PHPUnit - Include database migrations and seeders - Add proper exception handling with fallback pages - Implement template engine with custom syntax support - Add helper functions and facades for clean code - Include proper logging and debugging capabilities
250 lines
6.0 KiB
PHP
250 lines
6.0 KiB
PHP
<?php
|
|
|
|
namespace App\Core\Database;
|
|
|
|
/**
|
|
* NovaCore Database Migrator
|
|
* Handle database migrations
|
|
*/
|
|
class Migrator
|
|
{
|
|
private Connection $connection;
|
|
private string $migrationsPath;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->connection = $this->getConnection();
|
|
$this->migrationsPath = __DIR__ . '/../../database/migrations';
|
|
}
|
|
|
|
/**
|
|
* Get database connection
|
|
*/
|
|
protected function getConnection(): Connection
|
|
{
|
|
$config = include __DIR__ . '/../../Config/database.php';
|
|
$connectionConfig = $config['connections'][$config['default']];
|
|
|
|
return new Connection($connectionConfig);
|
|
}
|
|
|
|
/**
|
|
* Run all pending migrations
|
|
*/
|
|
public function run(): void
|
|
{
|
|
$this->createMigrationsTable();
|
|
|
|
$migrations = $this->getPendingMigrations();
|
|
|
|
foreach ($migrations as $migration) {
|
|
$this->runMigration($migration);
|
|
}
|
|
|
|
echo "Migrations completed successfully!\n";
|
|
}
|
|
|
|
/**
|
|
* Create migrations table
|
|
*/
|
|
private function createMigrationsTable(): void
|
|
{
|
|
$sql = "CREATE TABLE IF NOT EXISTS migrations (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
migration VARCHAR(255) NOT NULL,
|
|
batch INT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)";
|
|
|
|
$this->connection->execute($sql);
|
|
}
|
|
|
|
/**
|
|
* Get pending migrations
|
|
*/
|
|
private function getPendingMigrations(): array
|
|
{
|
|
$migrationFiles = glob($this->migrationsPath . '/*.php');
|
|
$migratedFiles = $this->getMigratedFiles();
|
|
|
|
$pending = [];
|
|
foreach ($migrationFiles as $file) {
|
|
$filename = basename($file);
|
|
if (!in_array($filename, $migratedFiles)) {
|
|
$pending[] = $file;
|
|
}
|
|
}
|
|
|
|
sort($pending);
|
|
return $pending;
|
|
}
|
|
|
|
/**
|
|
* Get already migrated files
|
|
*/
|
|
private function getMigratedFiles(): array
|
|
{
|
|
$sql = "SELECT migration FROM migrations ORDER BY id";
|
|
$results = $this->connection->fetchAll($sql);
|
|
|
|
return array_column($results, 'migration');
|
|
}
|
|
|
|
/**
|
|
* Run single migration
|
|
*/
|
|
private function runMigration(string $file): void
|
|
{
|
|
$filename = basename($file);
|
|
$className = $this->getMigrationClassName($filename);
|
|
|
|
require_once $file;
|
|
|
|
if (!class_exists($className)) {
|
|
throw new \Exception("Migration class {$className} not found in {$file}");
|
|
}
|
|
|
|
$migration = new $className();
|
|
$migration->up();
|
|
|
|
// Record migration
|
|
$this->recordMigration($filename);
|
|
|
|
echo "✓ {$filename}\n";
|
|
}
|
|
|
|
/**
|
|
* Get migration class name
|
|
*/
|
|
private function getMigrationClassName(string $filename): string
|
|
{
|
|
$name = pathinfo($filename, PATHINFO_FILENAME);
|
|
$parts = explode('_', $name);
|
|
|
|
// Remove timestamp
|
|
array_shift($parts);
|
|
|
|
// Convert to PascalCase
|
|
$className = '';
|
|
foreach ($parts as $part) {
|
|
$className .= ucfirst($part);
|
|
}
|
|
|
|
return $className;
|
|
}
|
|
|
|
/**
|
|
* Record migration
|
|
*/
|
|
private function recordMigration(string $filename): void
|
|
{
|
|
$batch = $this->getNextBatchNumber();
|
|
|
|
$sql = "INSERT INTO migrations (migration, batch) VALUES (?, ?)";
|
|
$this->connection->execute($sql, [$filename, $batch]);
|
|
}
|
|
|
|
/**
|
|
* Get next batch number
|
|
*/
|
|
private function getNextBatchNumber(): int
|
|
{
|
|
$sql = "SELECT MAX(batch) as max_batch FROM migrations";
|
|
$result = $this->connection->fetch($sql);
|
|
|
|
return ($result['max_batch'] ?? 0) + 1;
|
|
}
|
|
|
|
/**
|
|
* Rollback last batch
|
|
*/
|
|
public function rollback(): void
|
|
{
|
|
$lastBatch = $this->getLastBatchNumber();
|
|
|
|
if (!$lastBatch) {
|
|
echo "No migrations to rollback.\n";
|
|
return;
|
|
}
|
|
|
|
$migrations = $this->getMigrationsByBatch($lastBatch);
|
|
|
|
foreach (array_reverse($migrations) as $migration) {
|
|
$this->rollbackMigration($migration);
|
|
}
|
|
|
|
echo "Rollback completed successfully!\n";
|
|
}
|
|
|
|
/**
|
|
* Get last batch number
|
|
*/
|
|
private function getLastBatchNumber(): ?int
|
|
{
|
|
$sql = "SELECT MAX(batch) as max_batch FROM migrations";
|
|
$result = $this->connection->fetch($sql);
|
|
|
|
return $result['max_batch'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get migrations by batch
|
|
*/
|
|
private function getMigrationsByBatch(int $batch): array
|
|
{
|
|
$sql = "SELECT migration FROM migrations WHERE batch = ? ORDER BY id DESC";
|
|
$results = $this->connection->fetchAll($sql, [$batch]);
|
|
|
|
return array_column($results, 'migration');
|
|
}
|
|
|
|
/**
|
|
* Rollback single migration
|
|
*/
|
|
private function rollbackMigration(string $filename): void
|
|
{
|
|
$file = $this->migrationsPath . '/' . $filename;
|
|
|
|
if (!file_exists($file)) {
|
|
echo "Warning: Migration file {$filename} not found.\n";
|
|
return;
|
|
}
|
|
|
|
$className = $this->getMigrationClassName($filename);
|
|
|
|
require_once $file;
|
|
|
|
if (!class_exists($className)) {
|
|
echo "Warning: Migration class {$className} not found.\n";
|
|
return;
|
|
}
|
|
|
|
$migration = new $className();
|
|
$migration->down();
|
|
|
|
// Remove migration record
|
|
$sql = "DELETE FROM migrations WHERE migration = ?";
|
|
$this->connection->execute($sql, [$filename]);
|
|
|
|
echo "✓ Rolled back {$filename}\n";
|
|
}
|
|
|
|
/**
|
|
* Get migration status
|
|
*/
|
|
public function status(): void
|
|
{
|
|
$migrationFiles = glob($this->migrationsPath . '/*.php');
|
|
$migratedFiles = $this->getMigratedFiles();
|
|
|
|
echo "Migration Status:\n";
|
|
echo "================\n";
|
|
|
|
foreach ($migrationFiles as $file) {
|
|
$filename = basename($file);
|
|
$status = in_array($filename, $migratedFiles) ? '✓' : '✗';
|
|
echo "{$status} {$filename}\n";
|
|
}
|
|
}
|
|
}
|