chore: complete update
This commit is contained in:
32
src/Framework/Database/Migration/ApplyMigrations.php
Normal file
32
src/Framework/Database/Migration/ApplyMigrations.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Migration;
|
||||
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Database\DatabaseManager;
|
||||
|
||||
final readonly class ApplyMigrations
|
||||
{
|
||||
public function __construct(
|
||||
private PathProvider $pathProvider,
|
||||
private DatabaseManager $db
|
||||
) {}
|
||||
|
||||
#[ConsoleCommand('db:migrate', 'Apply all migrations')]
|
||||
public function __invoke(): void
|
||||
{
|
||||
$path = $this->pathProvider->resolvePath('src/Domain/Media/Migrations');
|
||||
|
||||
$this->db->migrate($path);
|
||||
}
|
||||
|
||||
#[ConsoleCommand('db:rollback', 'Apply a single migration')]
|
||||
public function up(): void
|
||||
{
|
||||
$path = $this->pathProvider->resolvePath('src/Domain/Media/Migrations');
|
||||
|
||||
$this->db->rollback($path);
|
||||
}
|
||||
}
|
||||
18
src/Framework/Database/Migration/Migration.php
Normal file
18
src/Framework/Database/Migration/Migration.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Migration;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
|
||||
interface Migration
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void;
|
||||
|
||||
public function down(ConnectionInterface $connection): void;
|
||||
|
||||
public function getVersion(): string;
|
||||
|
||||
public function getDescription(): string;
|
||||
}
|
||||
73
src/Framework/Database/Migration/MigrationLoader.php
Normal file
73
src/Framework/Database/Migration/MigrationLoader.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Migration;
|
||||
|
||||
use App\Framework\Database\Exception\DatabaseException;
|
||||
|
||||
final class MigrationLoader
|
||||
{
|
||||
private string $migrationsPath;
|
||||
|
||||
public function __construct(string $migrationsPath)
|
||||
{
|
||||
$this->migrationsPath = $migrationsPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Migration[]
|
||||
*/
|
||||
public function loadMigrations(): array
|
||||
{
|
||||
if (!is_dir($this->migrationsPath)) {
|
||||
throw new DatabaseException("Migrations directory does not exist: {$this->migrationsPath}");
|
||||
}
|
||||
|
||||
$migrations = [];
|
||||
$files = glob($this->migrationsPath . '/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$className = $this->getClassNameFromFile($file);
|
||||
|
||||
if (!$className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
require_once $file;
|
||||
|
||||
if (!class_exists($className)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
if (!$reflection->implementsInterface(Migration::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migration = $reflection->newInstance();
|
||||
$migrations[] = $migration;
|
||||
}
|
||||
|
||||
usort($migrations, fn($a, $b) => $a->getVersion() <=> $b->getVersion());
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
private function getClassNameFromFile(string $file): ?string
|
||||
{
|
||||
$content = file_get_contents($file);
|
||||
|
||||
$namespace = '';
|
||||
if (preg_match('/namespace\s+([^;]+);/i', $content, $matches)) {
|
||||
$namespace = $matches[1] . '\\';
|
||||
}
|
||||
|
||||
if (preg_match('/class\s+([^\s{]+)/i', $content, $matches)) {
|
||||
return $namespace . $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
145
src/Framework/Database/Migration/MigrationRunner.php
Normal file
145
src/Framework/Database/Migration/MigrationRunner.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Migration;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Exception\DatabaseException;
|
||||
use App\Framework\Database\Transaction;
|
||||
|
||||
final class MigrationRunner
|
||||
{
|
||||
private ConnectionInterface $connection;
|
||||
private string $migrationsTable;
|
||||
|
||||
public function __construct(ConnectionInterface $connection, string $migrationsTable = 'migrations')
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->migrationsTable = $migrationsTable;
|
||||
$this->ensureMigrationsTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Migration[] $migrations
|
||||
*/
|
||||
public function migrate(array $migrations): array
|
||||
{
|
||||
$executedMigrations = [];
|
||||
$appliedVersions = $this->getAppliedVersions();
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
if (in_array($migration->getVersion(), $appliedVersions, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Transaction::run($this->connection, function() use ($migration) {
|
||||
echo "Migrating: {$migration->getVersion()} - {$migration->getDescription()}\n";
|
||||
|
||||
$migration->up($this->connection);
|
||||
|
||||
$this->connection->execute(
|
||||
"INSERT INTO {$this->migrationsTable} (version, description, executed_at) VALUES (?, ?, ?)",
|
||||
[$migration->getVersion(), $migration->getDescription(), date('Y-m-d H:i:s')]
|
||||
);
|
||||
});
|
||||
|
||||
$executedMigrations[] = $migration->getVersion();
|
||||
echo "Migrated: {$migration->getVersion()}\n";
|
||||
} catch (\Throwable $e) {
|
||||
throw new DatabaseException(
|
||||
"Migration {$migration->getVersion()} failed: {$e->getMessage()}",
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $executedMigrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Migration[] $migrations
|
||||
*/
|
||||
public function rollback(array $migrations, int $steps = 1): array
|
||||
{
|
||||
$rolledBackMigrations = [];
|
||||
$appliedVersions = $this->getAppliedVersions();
|
||||
|
||||
$sortedMigrations = $migrations;
|
||||
usort($sortedMigrations, fn($a, $b) => $b->getVersion() <=> $a->getVersion());
|
||||
|
||||
$count = 0;
|
||||
foreach ($sortedMigrations as $migration) {
|
||||
if ($count >= $steps) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!in_array($migration->getVersion(), $appliedVersions, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Transaction::run($this->connection, function() use ($migration) {
|
||||
echo "Rolling back: {$migration->getVersion()} - {$migration->getDescription()}\n";
|
||||
|
||||
$migration->down($this->connection);
|
||||
|
||||
$this->connection->execute(
|
||||
"DELETE FROM {$this->migrationsTable} WHERE version = ?",
|
||||
[$migration->getVersion()]
|
||||
);
|
||||
});
|
||||
|
||||
$rolledBackMigrations[] = $migration->getVersion();
|
||||
$count++;
|
||||
echo "Rolled back: {$migration->getVersion()}\n";
|
||||
} catch (\Throwable $e) {
|
||||
throw new DatabaseException(
|
||||
"Rollback {$migration->getVersion()} failed: {$e->getMessage()}",
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $rolledBackMigrations;
|
||||
}
|
||||
|
||||
public function getStatus(array $migrations): array
|
||||
{
|
||||
$appliedVersions = $this->getAppliedVersions();
|
||||
$status = [];
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
$status[] = [
|
||||
'version' => $migration->getVersion(),
|
||||
'description' => $migration->getDescription(),
|
||||
'applied' => in_array($migration->getVersion(), $appliedVersions, true),
|
||||
];
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function getAppliedVersions(): array
|
||||
{
|
||||
return $this->connection->queryColumn(
|
||||
"SELECT version FROM {$this->migrationsTable} ORDER BY executed_at"
|
||||
);
|
||||
}
|
||||
|
||||
private function ensureMigrationsTable(): void
|
||||
{
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
version VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
executed_at DATETIME NOT NULL,
|
||||
INDEX idx_version (version)
|
||||
)";
|
||||
|
||||
$this->connection->execute($sql);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user