138 lines
4.7 KiB
PHP
138 lines
4.7 KiB
PHP
<?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_schema.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 002 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)
|
|
");
|
|
}
|
|
}
|