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 = [
'db' => $db,
'config' => (new ConfigModel($db))->loadFromDatabase(),
'user' => (new UserModel($db))->loadFromDatabase(),
'config' => (new ConfigModel($db))->get(),
'user' => (new UserModel($db))->get(),
];
// Start a session and generate a CSRF Token

View File

@ -13,6 +13,16 @@ class Controller {
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
if (Session::hasFlashMessages()){
$flashMessages = Session::getFlashMessages();

View File

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

View File

@ -4,7 +4,8 @@
public function index(){
global $app;
$emojiList = EmojiModel::loadAll();
$emojiModel = new EmojiModel($app['db']);
$emojiList = $emojiModel->getAll();
$vars = [
'config' => $app['config'],
@ -34,14 +35,14 @@
exit;
}
public function handleAdd(string $emoji, ?string $description=null): void {
private function isValidEmoji(string $emoji){
// Validate 1 visible character in the emoji
if (extension_loaded('mbstring')) {
// TODO - log a warning if mbstring isn't loaded
$charCount = mb_strlen($emoji, 'UTF-8');
if ($charCount !== 1) {
// TODO - handle error
return;
return false;
}
}
@ -50,25 +51,40 @@
if (!preg_match($emojiPattern, $emoji)) {
// TODO - handle error
return;
return false;
}
// emojis should have more bytes than characters
$byteCount = strlen($emoji);
if ($byteCount <= 1) {
// 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;
}
// 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 {
global $app;
$ids = $_POST['delete_emoji_ids'];
if (!empty($ids)) {
EmojiModel::delete($ids);
$emojiModel = new EmojiModel($app['db']);
$emojiModel->delete($ids);
}
}
}

View File

@ -39,7 +39,10 @@
}
private static function getEmojisWithLabels(): array {
$customEmoji = EmojiModel::loadAll();
global $app;
$emojiModel = new EmojiModel($app['db']);
$customEmoji = $emojiModel->getAll();
if (!empty($customEmoji)){
$custom = [];

View File

@ -13,15 +13,8 @@ class ConfigModel {
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
public function loadFromDatabase(): self {
public function get(): self {
$init = require APP_ROOT . '/config/init.php';
$c = new self($this->db);
$c->baseUrl = ($c->baseUrl === '') ? $init['base_url'] : $c->baseUrl;
@ -53,18 +46,6 @@ class ConfigModel {
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 {
$settingsCount = (int) $this->db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
@ -104,6 +85,6 @@ class ConfigModel {
$this->logLevel
]);
return $this->loadFromDatabase();
return $this->get();
}
}

View File

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

View File

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

View File

@ -8,15 +8,7 @@ class UserModel {
public function __construct(private PDO $db) {}
// load user settings 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
public function loadFromDatabase(): self {
public function get(): self {
// 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");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
@ -43,7 +35,7 @@ class UserModel {
$stmt->execute([$this->username, $this->displayName, $this->website, $this->mood]);
return $this->loadFromDatabase();
return $this->get();
}
// Making this a separate function to avoid

View File

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