Skip to content

Commit 314e004

Browse files
authored
Merge pull request #28 from tareq-halaby/tareq-halaby-patch-4
Add MigrationRunner class for database migrations
2 parents 4b96d41 + 70ac7e6 commit 314e004

File tree

1 file changed

+127
-0
lines changed

1 file changed

+127
-0
lines changed

src/MigrationRunner.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SqlPowertools;
6+
7+
/**
8+
* MigrationRunner - Database Migration Execution Engine
9+
*
10+
* Handles running, tracking, and rolling back database schema migrations
11+
* in a safe and deterministic order.
12+
*
13+
* @package SqlPowertools
14+
* @version 1.2.0
15+
*/
16+
class MigrationRunner
17+
{
18+
private const MIGRATIONS_TABLE = 'sql_powertools_migrations';
19+
20+
public function __construct(private readonly \PDO $pdo) {
21+
$this->ensureMigrationsTable();
22+
}
23+
24+
/**
25+
* Run all pending migrations from a given directory.
26+
*/
27+
public function runPending(string $migrationsDir): array
28+
{
29+
$files = $this->getMigrationFiles($migrationsDir);
30+
$ran = $this->getRanMigrations();
31+
$executed = [];
32+
33+
foreach ($files as $file) {
34+
$name = pathinfo($file, PATHINFO_FILENAME);
35+
if (in_array($name, $ran, true)) {
36+
continue;
37+
}
38+
$this->runMigration($file, $name);
39+
$executed[] = $name;
40+
}
41+
42+
return $executed;
43+
}
44+
45+
/**
46+
* Roll back the last batch of migrations.
47+
*/
48+
public function rollback(string $migrationsDir): array
49+
{
50+
$batch = $this->getLastBatch();
51+
if ($batch === null) {
52+
return [];
53+
}
54+
55+
$stmt = $this->pdo->prepare(
56+
'SELECT migration FROM ' . self::MIGRATIONS_TABLE . ' WHERE batch = ? ORDER BY id DESC'
57+
);
58+
$stmt->execute([$batch]);
59+
$migrations = $stmt->fetchAll(\PDO::FETCH_COLUMN);
60+
$rolledBack = [];
61+
62+
foreach ($migrations as $name) {
63+
$file = $migrationsDir . '/' . $name . '.php';
64+
if (file_exists($file)) {
65+
$migration = require $file;
66+
if (is_object($migration) && method_exists($migration, 'down')) {
67+
$migration->down($this->pdo);
68+
}
69+
}
70+
$this->removeMigration($name);
71+
$rolledBack[] = $name;
72+
}
73+
74+
return $rolledBack;
75+
}
76+
77+
private function ensureMigrationsTable(): void
78+
{
79+
$this->pdo->exec('
80+
CREATE TABLE IF NOT EXISTS ' . self::MIGRATIONS_TABLE . ' (
81+
id INT AUTO_INCREMENT PRIMARY KEY,
82+
migration VARCHAR(255) NOT NULL,
83+
batch INT NOT NULL,
84+
executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
85+
)
86+
');
87+
}
88+
89+
private function getMigrationFiles(string $dir): array
90+
{
91+
$files = glob($dir . '/*.php') ?: [];
92+
sort($files);
93+
return $files;
94+
}
95+
96+
private function getRanMigrations(): array
97+
{
98+
$stmt = $this->pdo->query('SELECT migration FROM ' . self::MIGRATIONS_TABLE);
99+
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
100+
}
101+
102+
private function getLastBatch(): ?int
103+
{
104+
$stmt = $this->pdo->query('SELECT MAX(batch) FROM ' . self::MIGRATIONS_TABLE);
105+
$result = $stmt->fetchColumn();
106+
return $result !== false ? (int) $result : null;
107+
}
108+
109+
private function runMigration(string $file, string $name): void
110+
{
111+
$migration = require $file;
112+
if (is_object($migration) && method_exists($migration, 'up')) {
113+
$migration->up($this->pdo);
114+
}
115+
$batch = ($this->getLastBatch() ?? 0) + 1;
116+
$stmt = $this->pdo->prepare(
117+
'INSERT INTO ' . self::MIGRATIONS_TABLE . ' (migration, batch) VALUES (?, ?)'
118+
);
119+
$stmt->execute([$name, $batch]);
120+
}
121+
122+
private function removeMigration(string $name): void
123+
{
124+
$stmt = $this->pdo->prepare('DELETE FROM ' . self::MIGRATIONS_TABLE . ' WHERE migration = ?');
125+
$stmt->execute([$name]);
126+
}
127+
}

0 commit comments

Comments
 (0)