Remove all static model methods. (#52)

Replace all static model methods. Support dependency injection for all models. Clean up things that used the static model methods. Decouple ConfigModel and CssModel.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/52
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 19:15:24 +00:00 committed by greg
parent dc4f60ce2e
commit 832b7b95fa
10 changed files with 73 additions and 75 deletions

View File

@ -72,8 +72,8 @@ global $app;
$app = [ $app = [
'db' => $db, 'db' => $db,
'config' => (new ConfigModel($db))->loadFromDatabase(), 'config' => (new ConfigModel($db))->get(),
'user' => (new UserModel($db))->loadFromDatabase(), 'user' => (new UserModel($db))->get(),
]; ];
// Start a session and generate a CSRF Token // Start a session and generate a CSRF Token

View File

@ -13,6 +13,16 @@ class Controller {
throw new RuntimeException("Template not found: $childTemplatePath"); throw new RuntimeException("Template not found: $childTemplatePath");
} }
// Add custom CSS filename if needed
global $app;
if ($app['config']->cssId) {
$cssModel = new CssModel($app['db']);
$cssFile = $cssModel->getById($app['config']->cssId);
$vars['customCssFilename'] = $cssFile['filename'] ?? null;
} else {
$vars['customCssFilename'] = null;
}
// always check for flash messages and add them if they exist // always check for flash messages and add them if they exist
if (Session::hasFlashMessages()){ if (Session::hasFlashMessages()){
$flashMessages = Session::getFlashMessages(); $flashMessages = Session::getFlashMessages();

View File

@ -3,8 +3,8 @@
class CssController extends Controller { class CssController extends Controller {
public function index() { public function index() {
global $app; global $app;
$cssModel = new CssModel($app['db']);
$customCss = CssModel::load(); $customCss = $cssModel->getAll();
$vars = [ $vars = [
'user' => $app['user'], 'user' => $app['user'],
@ -16,7 +16,8 @@ class CssController extends Controller {
} }
public function serveCustomCss(string $baseFilename){ public function serveCustomCss(string $baseFilename){
$cssModel = new CssModel(); global $app;
$cssModel = new CssModel($app['db']);
$filename = "$baseFilename.css"; $filename = "$baseFilename.css";
$cssRow = $cssModel->getByFilename($filename); $cssRow = $cssModel->getByFilename($filename);
@ -77,7 +78,7 @@ class CssController extends Controller {
// Get the data for the selected CSS file // Get the data for the selected CSS file
$cssId = $_POST['selectCssFile']; $cssId = $_POST['selectCssFile'];
$cssModel = new CssModel(); $cssModel = new CssModel($app['db']);
$cssRow = $cssModel->getById($cssId); $cssRow = $cssModel->getById($cssId);
// exit if the requested file isn't in the database // exit if the requested file isn't in the database
@ -182,7 +183,8 @@ class CssController extends Controller {
} }
// Add upload to database // Add upload to database
$cssModel = new CssModel(); global $app;
$cssModel = new CssModel($app['db']);
$cssModel->save($safeFilename, $description); $cssModel->save($safeFilename, $description);
// Set success flash message // Set success flash message

View File

@ -3,8 +3,9 @@
// Shows the custom emoji management page // Shows the custom emoji management page
public function index(){ public function index(){
global $app; global $app;
$emojiList = EmojiModel::loadAll(); $emojiModel = new EmojiModel($app['db']);
$emojiList = $emojiModel->getAll();
$vars = [ $vars = [
'config' => $app['config'], 'config' => $app['config'],
@ -34,14 +35,14 @@
exit; exit;
} }
public function handleAdd(string $emoji, ?string $description=null): void { private function isValidEmoji(string $emoji){
// Validate 1 visible character in the emoji // Validate 1 visible character in the emoji
if (extension_loaded('mbstring')) { if (extension_loaded('mbstring')) {
// TODO - log a warning if mbstring isn't loaded // TODO - log a warning if mbstring isn't loaded
$charCount = mb_strlen($emoji, 'UTF-8'); $charCount = mb_strlen($emoji, 'UTF-8');
if ($charCount !== 1) { if ($charCount !== 1) {
// TODO - handle error // TODO - handle error
return; return false;
} }
} }
@ -50,25 +51,40 @@
if (!preg_match($emojiPattern, $emoji)) { if (!preg_match($emojiPattern, $emoji)) {
// TODO - handle error // TODO - handle error
return; return false;
} }
// emojis should have more bytes than characters // emojis should have more bytes than characters
$byteCount = strlen($emoji); $byteCount = strlen($emoji);
if ($byteCount <= 1) { if ($byteCount <= 1) {
// TODO - handle error // TODO - handle error
return false;
}
return true;
}
public function handleAdd(string $emoji, ?string $description=null): void {
global $app;
if (!$this->isValidEmoji($emoji)){
// TODO - handle
return; return;
} }
// It looks like an emoji. Let's add it. // It looks like an emoji. Let's add it.
EmojiModel::add($emoji, $description); $emojiModel = new EmojiModel($app['db']);
$emojiList = $emojiModel->add($emoji, $description);
} }
public function handleDelete(): void { public function handleDelete(): void {
global $app;
$ids = $_POST['delete_emoji_ids']; $ids = $_POST['delete_emoji_ids'];
if (!empty($ids)) { if (!empty($ids)) {
EmojiModel::delete($ids); $emojiModel = new EmojiModel($app['db']);
$emojiModel->delete($ids);
} }
} }
} }

View File

@ -2,7 +2,7 @@
class MoodController extends Controller { class MoodController extends Controller {
public function index(){ public function index(){
global $app; global $app;
$view = new MoodView(); $view = new MoodView();
$moodPicker = $view->render_mood_picker(self::getEmojisWithLabels(), $app['user']->mood); $moodPicker = $view->render_mood_picker(self::getEmojisWithLabels(), $app['user']->mood);
@ -17,7 +17,7 @@
public function handlePost(){ public function handlePost(){
global $app; global $app;
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
switch ($_POST['action']){ switch ($_POST['action']){
case 'set': case 'set':
@ -39,7 +39,10 @@
} }
private static function getEmojisWithLabels(): array { private static function getEmojisWithLabels(): array {
$customEmoji = EmojiModel::loadAll(); global $app;
$emojiModel = new EmojiModel($app['db']);
$customEmoji = $emojiModel->getAll();
if (!empty($customEmoji)){ if (!empty($customEmoji)){
$custom = []; $custom = [];

View File

@ -13,15 +13,8 @@ class ConfigModel {
public function __construct(private PDO $db) {} public function __construct(private PDO $db) {}
// load config from sqlite database (backward compatibility)
public static function load(): self {
global $db;
$instance = new self($db);
return $instance->loadFromDatabase();
}
// Instance method that uses injected database // Instance method that uses injected database
public function loadFromDatabase(): self { public function get(): self {
$init = require APP_ROOT . '/config/init.php'; $init = require APP_ROOT . '/config/init.php';
$c = new self($this->db); $c = new self($this->db);
$c->baseUrl = ($c->baseUrl === '') ? $init['base_url'] : $c->baseUrl; $c->baseUrl = ($c->baseUrl === '') ? $init['base_url'] : $c->baseUrl;
@ -53,18 +46,6 @@ class ConfigModel {
return $c; return $c;
} }
public function customCssFilename() {
if (empty($this->cssId)) {
return null;
}
// Fetch filename from css table using cssId
$cssModel = new CssModel();
$cssRecord = $cssModel->getById($this->cssId);
return $cssRecord ? $cssRecord['filename'] : null;
}
public function save(): self { public function save(): self {
$settingsCount = (int) $this->db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); $settingsCount = (int) $this->db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
@ -104,6 +85,6 @@ class ConfigModel {
$this->logLevel $this->logLevel
]); ]);
return $this->loadFromDatabase(); return $this->get();
} }
} }

View File

@ -1,44 +1,41 @@
<?php <?php
class CssModel { class CssModel {
public static function load(): Array { public function __construct(private PDO $db) {}
global $db;
$stmt = $db->prepare("SELECT id, filename, description FROM css ORDER BY filename"); public function getAll(): Array {
$stmt = $this->db->prepare("SELECT id, filename, description FROM css ORDER BY filename");
$stmt->execute(); $stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function getById(int $id): Array{ public function getById(int $id): Array{
global $db; $stmt = $this->db->prepare("SELECT id, filename, description FROM css WHERE id=?");
$stmt = $db->prepare("SELECT id, filename, description FROM css WHERE id=?");
$stmt->execute([$id]); $stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function getByFilename(string $filename): Array{ public function getByFilename(string $filename): Array{
global $db; $stmt = $this->db->prepare("SELECT id, filename, description FROM css WHERE filename=?");
$stmt = $db->prepare("SELECT id, filename, description FROM css WHERE filename=?");
$stmt->execute([$filename]); $stmt->execute([$filename]);
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
} }
public function delete(int $id): bool{ public function delete(int $id): bool{
global $db; $stmt = $this->db->prepare("DELETE FROM css WHERE id=?");
$stmt = $db->prepare("DELETE FROM css WHERE id=?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
public function save(string $filename, ?string $description = null): void { public function save(string $filename, ?string $description = null): void {
global $db; $stmt = $this->db->prepare("SELECT COUNT(id) FROM css WHERE filename = ?");
$stmt = $db->prepare("SELECT COUNT(id) FROM css WHERE filename = ?");
$stmt->execute([$filename]); $stmt->execute([$filename]);
$fileExists = $stmt->fetchColumn(); $fileExists = $stmt->fetchColumn();
if ($fileExists) { if ($fileExists) {
$stmt = $db->prepare("UPDATE css SET description = ? WHERE filename = ?"); $stmt = $this->db->prepare("UPDATE css SET description = ? WHERE filename = ?");
} else { } else {
$stmt = $db->prepare("INSERT INTO css (filename, description) VALUES (?, ?)"); $stmt = $this->db->prepare("INSERT INTO css (filename, description) VALUES (?, ?)");
} }
$stmt->execute([$filename, $description]); $stmt->execute([$filename, $description]);

View File

@ -1,29 +1,25 @@
<?php <?php
// welp this model is overkill // welp this model is overkill
class EmojiModel{ class EmojiModel{
public function __construct(private PDO $db) {}
// This isn't memory-efficient, // This isn't memory-efficient,
// but I think it'll be fine on this app's scales. // but I think it'll be fine on this app's scales.
public static function loadAll(): array { public function getAll(): array {
global $db; $stmt = $this->db->query("SELECT id, emoji, description FROM emoji");
$stmt = $db->query("SELECT id, emoji, description FROM emoji");
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
// I'm not going to support editing emoji. // I'm not going to support editing emoji.
// It'll just be a delete/readd // It'll just be a delete/readd
public static function add(string $emoji, ?string $description): void{ public function add(string $emoji, ?string $description): void{
global $db; $stmt = $this->db->prepare("INSERT INTO emoji (emoji, description) VALUES (?, ?)");
$stmt = $db->prepare("INSERT INTO emoji (emoji, description) VALUES (?, ?)");
$stmt->execute([$emoji, $description]); $stmt->execute([$emoji, $description]);
} }
public static function delete(array $idsToDelete): void{ public function delete(array $idsToDelete): void{
global $db;
$placeholders = rtrim(str_repeat('?,', count($idsToDelete)), ','); $placeholders = rtrim(str_repeat('?,', count($idsToDelete)), ',');
$stmt = $db->prepare("DELETE FROM emoji WHERE id IN ($placeholders)"); $stmt = $this->db->prepare("DELETE FROM emoji WHERE id IN ($placeholders)");
$stmt->execute($idsToDelete); $stmt->execute($idsToDelete);
} }
} }

View File

@ -8,15 +8,7 @@ class UserModel {
public function __construct(private PDO $db) {} public function __construct(private PDO $db) {}
// load user settings from sqlite database (backward compatibility) public function get(): self {
public static function load(): self {
global $db;
$instance = new self($db);
return $instance->loadFromDatabase();
}
// Instance method that uses injected database
public function loadFromDatabase(): self {
// There's only ever one user. I'm just leaning into that. // There's only ever one user. I'm just leaning into that.
$stmt = $this->db->query("SELECT username, display_name, website, mood FROM user WHERE id=1"); $stmt = $this->db->query("SELECT username, display_name, website, mood FROM user WHERE id=1");
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
@ -43,7 +35,7 @@ class UserModel {
$stmt->execute([$this->username, $this->displayName, $this->website, $this->mood]); $stmt->execute([$this->username, $this->displayName, $this->website, $this->mood]);
return $this->loadFromDatabase(); return $this->get();
} }
// Making this a separate function to avoid // Making this a separate function to avoid

View File

@ -2,6 +2,7 @@
<?php /** @var ConfigModel $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var UserModel $user */ ?> <?php /** @var UserModel $user */ ?>
<?php /** @var string $childTemplateFile */ ?> <?php /** @var string $childTemplateFile */ ?>
<?php /** @var string $customCssFilename */ ?>
<?php /** @var srting $flashSection */ ?> <?php /** @var srting $flashSection */ ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -13,7 +14,7 @@
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/default.css')) ?>"> href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/default.css')) ?>">
<?php if (!empty($config->cssId)): ?> <?php if (!empty($config->cssId)): ?>
<link rel="stylesheet" <link rel="stylesheet"
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/custom/' . $config->customCssFilename())) ?>"> href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/custom/' . $customCssFilename)) ?>">
<?php endif; ?> <?php endif; ?>
<link rel="alternate" <link rel="alternate"
type="application/rss+xml" type="application/rss+xml"