first commit
This commit is contained in:
137
src/Shared/Database/Migrator.php
Normal file
137
src/Shared/Database/Migrator.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Database;
|
||||
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Gestionnaire de migrations de base de données.
|
||||
*
|
||||
* Responsabilité unique : exécuter les migrations DDL et synchroniser
|
||||
* l'index FTS5. Le provisionnement des données initiales est délégué
|
||||
* à {@see Seeder}.
|
||||
*
|
||||
* Convention des fichiers de migration :
|
||||
* - Placés dans database/migrations/
|
||||
* - Nommés NNN_description.php (ex: 001_create_users.php)
|
||||
* - Retournent un tableau ['up' => 'SQL...', 'down' => 'SQL...']
|
||||
* - Triés et exécutés par ordre alphanumérique croissant
|
||||
*
|
||||
* run() est idempotent et sûr à appeler à chaque démarrage applicatif :
|
||||
* les migrations déjà appliquées ne sont jamais rejouées.
|
||||
*/
|
||||
final class Migrator
|
||||
{
|
||||
/**
|
||||
* Répertoire contenant les fichiers de migration.
|
||||
*/
|
||||
private const MIGRATIONS_DIR = __DIR__ . '/../../../database/migrations';
|
||||
|
||||
/**
|
||||
* Exécute les migrations en attente puis synchronise l'index FTS5.
|
||||
*
|
||||
* Opération idempotente et sans effets de bord sur les données :
|
||||
* sûre à appeler à chaque démarrage applicatif.
|
||||
*
|
||||
* Séquence :
|
||||
* 1. Crée la table de suivi si absente
|
||||
* 2. Joue les migrations en attente
|
||||
* 3. Indexe dans posts_fts les articles absents de l'index (syncFtsIndex)
|
||||
*
|
||||
* @param PDO $db L'instance de connexion à la base de données
|
||||
*/
|
||||
public static function run(PDO $db): void
|
||||
{
|
||||
self::createMigrationTable($db);
|
||||
self::runPendingMigrations($db);
|
||||
self::syncFtsIndex($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée la table de suivi des migrations si elle n'existe pas.
|
||||
*
|
||||
* Cette table doit exister avant de pouvoir lire les migrations appliquées.
|
||||
*
|
||||
* @param PDO $db L'instance de connexion à la base de données
|
||||
*/
|
||||
private static function createMigrationTable(PDO $db): void
|
||||
{
|
||||
$db->exec('
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
version TEXT NOT NULL UNIQUE,
|
||||
run_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les fichiers de migration, filtre ceux déjà appliqués
|
||||
* et exécute les migrations en attente dans l'ordre.
|
||||
*
|
||||
* @param PDO $db L'instance de connexion à la base de données
|
||||
*/
|
||||
private static function runPendingMigrations(PDO $db): void
|
||||
{
|
||||
// Versions déjà appliquées (indexées pour un accès O(1))
|
||||
$stmt = $db->query('SELECT version FROM migrations');
|
||||
$rows = $stmt ? $stmt->fetchAll(PDO::FETCH_COLUMN) : [];
|
||||
$applied = array_flip($rows);
|
||||
|
||||
// Fichiers de migration triés par nom (ordre alphanumérique = ordre numérique)
|
||||
$files = glob(self::MIGRATIONS_DIR . '/*.php') ?: [];
|
||||
sort($files);
|
||||
|
||||
$insert = $db->prepare('INSERT INTO migrations (version, run_at) VALUES (:version, :run_at)');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$version = basename($file, '.php');
|
||||
|
||||
if (isset($applied[$version])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// require évalue le fichier à chaque appel dans la boucle.
|
||||
// require_once aurait mis en cache le résultat du premier fichier
|
||||
// et l'aurait réutilisé pour tous les suivants — à ne pas utiliser ici.
|
||||
$migration = require $file;
|
||||
|
||||
$db->exec($migration['up']);
|
||||
|
||||
$insert->execute([
|
||||
':version' => $version,
|
||||
':run_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise l'index FTS5 avec les articles présents en base.
|
||||
*
|
||||
* Insère dans posts_fts les articles dont le rowid est absent de l'index.
|
||||
* Idempotent et sans effet si l'index est déjà à jour.
|
||||
*
|
||||
* Nécessaire car les triggers FTS5 ne couvrent que les INSERT/UPDATE/DELETE
|
||||
* effectués APRÈS leur création — les articles existants au moment de la
|
||||
* migration 006 ne sont pas indexés rétroactivement.
|
||||
*
|
||||
* strip_tags() est enregistrée comme fonction SQLite dans container.php via
|
||||
* sqliteCreateFunction() avant l'appel à Migrator::run() — elle est donc
|
||||
* disponible ici.
|
||||
*
|
||||
* @param PDO $db L'instance de connexion à la base de données
|
||||
*/
|
||||
private static function syncFtsIndex(PDO $db): void
|
||||
{
|
||||
$db->exec("
|
||||
INSERT INTO posts_fts(rowid, title, content, author_username)
|
||||
SELECT p.id,
|
||||
p.title,
|
||||
COALESCE(strip_tags(p.content), ''),
|
||||
COALESCE((SELECT username FROM users WHERE id = p.author_id), '')
|
||||
FROM posts p
|
||||
WHERE p.id NOT IN (SELECT rowid FROM posts_fts)
|
||||
");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user