refactor-app-initialization (#51)

Separate database migrations from other database initialization functions.
Move some initialization directly into index to keep classes targeted.
Simplify setup validation and redirection logic.
Clean up comments.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/51
Co-authored-by: Greg Sarjeant <greg@subcultureofone.org>
Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
This commit is contained in:
Greg Sarjeant 2025-08-03 16:04:06 +00:00 committed by greg
parent 2e82f946ae
commit dc4f60ce2e
3 changed files with 58 additions and 88 deletions

View File

@ -1,4 +1,8 @@
<?php
/*
* Initialize fundamental configuration
*/
// Store and validate request data
$method = $_SERVER['REQUEST_METHOD'];
$request = $_SERVER['REQUEST_URI'];
@ -14,7 +18,11 @@ if (preg_match('/\.php$/', $path)) {
// Define base paths and load classes
include_once(dirname(dirname(__FILE__)) . "/config/bootstrap.php");
// Check prerequisites.
/*
* Validate application state before processing request
*/
// Check prerequisites
$prerequisites = new Prerequisites();
$results = $prerequisites->validate();
if (count($prerequisites->getErrors()) > 0) {
@ -22,28 +30,46 @@ if (count($prerequisites->getErrors()) > 0) {
exit;
}
// Do any necessary database migrations
$dbMgr = new Database();
$dbMgr->migrate();
// Make sure the initial setup is complete
// unless we're already heading to setup
//
// TODO: Consider simplifying this.
// Might not need the custom exception now that the prereq checker is more robust.
if (!(preg_match('/setup$/', $path))) {
try {
// Make sure setup has been completed
$dbMgr->confirmSetup();
} catch (SetupException $e) {
$e->handle();
exit;
}
// Connect to the database
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
);
}
// Do any necessary database migrations
$migrator = new Migrator($db);
$migrator->migrate();
// Make sure the initial setup is complete unless we're already heading to setup
if (!(preg_match('/setup$/', $path))) {
// 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, redirect to setup.
if ($user_count === 0 || $settings_count === 0){
$init = require APP_ROOT . '/config/init.php';
header('Location: ' . $init['base_path'] . 'setup');
exit;
};
}
/*
* Begin processing request
*/
// Initialize application context with all dependencies
global $app;
$db = Database::get();
$app = [
'db' => $db,
'config' => (new ConfigModel($db))->loadFromDatabase(),
@ -51,7 +77,6 @@ $app = [
];
// Start a session and generate a CSRF Token
// if there isn't already an active session
Session::start();
Session::generateCsrfToken();
@ -75,7 +100,7 @@ if ($method === 'POST' && $path != 'setup') {
if (!Session::isValid($_POST['csrf_token'])) {
// Invalid session - redirect to /login
Log::info('Attempt to POST with invalid session. Redirecting to login.');
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login'));
header('Location: ' . Util::buildRelativeUrl($app->config->basePath, 'login'));
exit;
}
} else {

View File

@ -20,6 +20,8 @@ class SetupException extends Exception {
// We don't want to short-circuit this if there's a problem logging
}
// TODO: This doesn't need to be a switch anymore
// May not need to exist at all
switch ($this->setupIssue){
case 'database_connection':
case 'db_migration':
@ -29,19 +31,6 @@ class SetupException extends Exception {
echo "<h1>Configuration Error</h1>";
echo "<p>" . Util::escape_html($this->setupIssue) . '-' . Util::escape_html($this->getMessage()) . "</p>";
exit;
case 'table_contents':
// Recoverable error.
// Redirect to setup if we aren't already headed there.
// NOTE: Just read directly from init.php instead of
// trying to use the config object. This is the initial
// setup. It shouldn't assume any data can be loaded.
$init = require APP_ROOT . '/config/init.php';
$currentPath = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
if (strpos($currentPath, 'setup') === false) {
header('Location: ' . $init['base_path'] . 'setup');
exit;
}
}
}

View File

@ -1,35 +1,11 @@
<?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
);
}
class Migrator{
public function __construct(private PDO $db) {}
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.
// The database version is an int stored as PRAGMA user_version.
// It corresponds 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;
return $this->db->query("PRAGMA user_version")->fetchColumn() ?? 0;
}
private function migrationNumberFromFile(string $filename): int {
@ -48,8 +24,7 @@ class Database{
);
}
$db = self::get();
$db->exec("PRAGMA user_version = $newVersion");
$this->db->exec("PRAGMA user_version = $newVersion");
}
private function getPendingMigrations(): array {
@ -79,8 +54,7 @@ class Database{
Log::info("Found " . count($migrations) . " pending migrations.");
Log::info("Updating database. Current Version: " . $this->getVersion());
$db = self::get();
$db->beginTransaction();
$this->db->beginTransaction();
try {
foreach ($migrations as $version => $file) {
@ -100,7 +74,7 @@ class Database{
foreach ($statements as $statement){
if (!empty($statement)){
Log::debug("Migration statement: {$statement}");
$db->exec($statement);
$this->db->exec($statement);
}
}
@ -108,13 +82,13 @@ class Database{
}
// Update db version
$db->commit();
$this->db->commit();
$this->setVersion($version);
Log::info("Applied " . count($migrations) . " migrations.");
Log::info("Updated database version to " . $this->getVersion());
} catch (Exception $e) {
$db->rollBack();
$this->db->rollBack();
throw new SetupException(
"Migration failed: $filename",
'db_migration',
@ -123,22 +97,4 @@ class Database{
);
}
}
// 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',
);
};
}
}