tkr/src/Framework/Database/Database.php
Greg Sarjeant 6337fa2dfb Add Prerequisite class and cli script to confirm prereqs. (#30)
Add a Prerequisite class to manage all initilaization prerequisites. Invoke before each request. Provide a CLI script to allow checking Prereqs from command line. Add CI  workflow to confirm prereq validation behavior.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/30
Co-authored-by: Greg Sarjeant <greg@subcultureofone.org>
Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
2025-07-27 16:43:09 +00:00

138 lines
4.2 KiB
PHP

<?php
class Database{
// TODO = Make this not static
public static function get(): PDO {
try {
// SQLite will just create this if it doesn't exist.
$db = new PDO("sqlite:" . DB_FILE);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
throw new SetupException(
"Database connection failed: " . $e->getMessage(),
'database_connection',
0,
$e
);
}
return $db;
}
public function validate(): void{
$this->validateTableContents();
}
// The database version will just be an int
// stored as PRAGMA user_version. It will
// correspond to the most recent migration file applied to the db.
private function getVersion(): int {
$db = self::get();
return $db->query("PRAGMA user_version")->fetchColumn() ?? 0;
}
private function migrationNumberFromFile(string $filename): int {
$basename = basename($filename, '.sql');
$parts = explode('_', $basename);
return (int) $parts[0];
}
private function setVersion(int $newVersion): void {
$currentVersion = $this->getVersion();
if ($newVersion <= $currentVersion){
throw new SetupException(
"New version ($newVersion) must be greater than current version ($currentVersion)",
'db_migration'
);
}
$db = self::get();
$db->exec("PRAGMA user_version = $newVersion");
}
private function getPendingMigrations(): array {
$currentVersion = $this->getVersion();
$files = glob(CONFIG_DIR . '/migrations/*.sql');
$pending = [];
foreach ($files as $file) {
$version = $this->migrationNumberFromFile($file);
if ($version > $currentVersion) {
$pending[$version] = $file;
}
}
ksort($pending);
return $pending;
}
public function migrate(): void {
$migrations = $this->getPendingMigrations();
if (empty($migrations)) {
# TODO: log
return;
}
$db = self::get();
$db->beginTransaction();
try {
foreach ($migrations as $version => $file) {
$filename = basename($file);
// TODO: log properly
$sql = file_get_contents($file);
if ($sql === false) {
throw new Exception("Could not read migration file: $file");
}
// Remove comments and split by semicolon
$sql = preg_replace('/--.*$/m', '', $sql);
$statements = preg_split('/;\s*$/m', $sql, -1, PREG_SPLIT_NO_EMPTY);
// Execute each statement
foreach ($statements as $statement){
if (!empty($statement)){
$db->exec($statement);
}
}
}
// Update db version
$db->commit();
$this->setVersion($version);
//TODO: log properly
//echo "All migrations completed successfully.\n";
} catch (Exception $e) {
$db->rollBack();
throw new SetupException(
"Migration failed: $filename",
'db_migration',
0,
$e
);
}
}
// make sure tables that need to be seeded have been
public function confirmSetup(): void {
$db = self::get();
// make sure required tables (user, settings) are populated
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
$settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
// If either required table has no records, throw an exception.
// This will be caught and redirect to setup.
if ($user_count === 0 || $settings_count === 0){
throw new SetupException(
"Required tables aren't populated. Please complete setup",
'table_contents',
);
};
}
}