feat(core): Konfiguration, PDO-Datenbankschicht und Migrationen
SQLite als Standard (keine Einrichtung noetig, WAL aktiviert), optional MySQL/MariaDB. Beide Treiber nutzen dieselben vorbereiteten Statements. Die Migrationen laufen automatisch beim ersten Request; jede Migration liefert das SQL je Treiber. Damit entfaellt ein manueller Schritt beim Deployment ueber FTP. data/ enthaelt eine eigene .htaccess als zweite Absicherung, falls das Verzeichnis beim Hoster doch einmal im Webroot landet.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
/**
|
||||
* Sehr einfache Migrationen: jede Migration ist eine PHP-Datei in
|
||||
* /migrations, die ein Array ['sqlite' => [...SQL], 'mysql' => [...SQL]]
|
||||
* zurueckgibt. Bereits ausgefuehrte Migrationen werden in der Tabelle
|
||||
* "migrations" vermerkt. Laeuft automatisch beim ersten Request.
|
||||
*/
|
||||
final class Migrator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Database $db,
|
||||
private readonly string $migrationsDir,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> Namen der ausgefuehrten Migrationen
|
||||
*/
|
||||
public function migrate(): array
|
||||
{
|
||||
$this->ensureMigrationsTable();
|
||||
|
||||
$applied = [];
|
||||
$already = $this->appliedMigrations();
|
||||
|
||||
foreach ($this->availableMigrations() as $name => $file) {
|
||||
if (in_array($name, $already, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<string,list<string>> $definition */
|
||||
$definition = require $file;
|
||||
$statements = $definition[$this->db->driver()] ?? $definition['sqlite'] ?? [];
|
||||
|
||||
foreach ($statements as $sql) {
|
||||
$sql = trim($sql);
|
||||
if ($sql !== '') {
|
||||
$this->db->pdo()->exec($sql);
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->execute(
|
||||
'INSERT INTO migrations (name, applied_at) VALUES (?, ?)',
|
||||
[$name, gmdate('Y-m-d H:i:s')],
|
||||
);
|
||||
$applied[] = $name;
|
||||
}
|
||||
|
||||
return $applied;
|
||||
}
|
||||
|
||||
private function ensureMigrationsTable(): void
|
||||
{
|
||||
$sql = $this->db->driver() === 'mysql'
|
||||
? 'CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(190) NOT NULL UNIQUE,
|
||||
applied_at DATETIME NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
: 'CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
applied_at TEXT NOT NULL
|
||||
)';
|
||||
|
||||
$this->db->pdo()->exec($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function appliedMigrations(): array
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT name FROM migrations');
|
||||
|
||||
return array_map(static fn (array $row): string => (string) $row['name'], $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string> name => absoluter Pfad
|
||||
*/
|
||||
private function availableMigrations(): array
|
||||
{
|
||||
$files = glob($this->migrationsDir . '/*.php') ?: [];
|
||||
sort($files, SORT_STRING);
|
||||
|
||||
$result = [];
|
||||
foreach ($files as $file) {
|
||||
$result[basename($file, '.php')] = $file;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user