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:
		
							parent
							
								
									2e82f946ae
								
							
						
					
					
						commit
						dc4f60ce2e
					
				| @ -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 { | ||||
|  | ||||
| @ -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; | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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', | ||||
|             ); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user