community
All checks were successful
Deploy / deploy (push) Successful in 47s

This commit is contained in:
2026-07-18 01:53:44 +02:00
parent 57e197755e
commit 3ff5767900
9 changed files with 1569 additions and 74 deletions

View File

@@ -5,6 +5,10 @@ namespace App;
final class Community
{
private ?array $forumStructure = null;
private array $tableCache = [];
private array $columnCache = [];
public function __construct(private \PDO $pdo, private array $config)
{
}
@@ -24,11 +28,41 @@ final class Community
public function createThread(int $userId, string $title, string $body): void
{
$this->createThreadInBoard($userId, null, $title, $body);
}
public function createThreadInBoard(int $userId, ?string $boardSlug, string $title, string $body): void
{
$title = trim($title);
$body = trim($body);
if ($title === '' || $body === '') {
throw new \RuntimeException('Titel und Text sind erforderlich.');
}
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards')) {
$boardId = null;
if ($boardSlug !== null && $boardSlug !== '') {
$board = $this->getBoardBySlug($boardSlug);
if (!$board) {
throw new \RuntimeException('Forum-Bereich nicht gefunden.');
}
$boardId = (int)$board['id'];
}
$stmt = $this->pdo->prepare('INSERT INTO forum_threads (board_id, user_id, title, body) VALUES (:boardId, :uid, :title, :body)');
$stmt->bindValue(':boardId', $boardId, $boardId === null ? \PDO::PARAM_NULL : \PDO::PARAM_INT);
$stmt->bindValue(':uid', $userId, \PDO::PARAM_INT);
$stmt->bindValue(':title', $title, \PDO::PARAM_STR);
$stmt->bindValue(':body', $body, \PDO::PARAM_STR);
$stmt->execute();
return;
}
$stmt = $this->pdo->prepare('INSERT INTO forum_threads (user_id, title, body) VALUES (:uid, :title, :body)');
$stmt->execute([
':uid' => $userId,
':title' => trim($title),
':body' => trim($body),
':title' => $title,
':body' => $body,
]);
}
@@ -42,7 +76,57 @@ final class Community
]);
}
public function searchThreads(string $query, int $limit = 50): array
public function updateThread(int $threadId, string $title, string $body): void
{
$stmt = $this->pdo->prepare('UPDATE forum_threads SET title = :title, body = :body, updated_at = NOW() WHERE id = :id');
$stmt->execute([
'id' => $threadId,
'title' => trim($title),
'body' => trim($body),
]);
}
public function deleteThread(int $threadId): void
{
$stmt = $this->pdo->prepare('DELETE FROM forum_threads WHERE id = :id');
$stmt->execute(['id' => $threadId]);
}
public function updatePost(int $postId, string $body): void
{
$stmt = $this->pdo->prepare('UPDATE forum_posts SET body = :body, updated_at = NOW() WHERE id = :id');
$stmt->execute([
'id' => $postId,
'body' => trim($body),
]);
}
public function deletePost(int $postId): void
{
$stmt = $this->pdo->prepare('DELETE FROM forum_posts WHERE id = :id');
$stmt->execute(['id' => $postId]);
}
public function toggleHelpfulHighlight(int $postId, ?int $userId): void
{
if (!$this->hasColumn('forum_posts', 'highlighted_at')) {
return;
}
if ($userId === null) {
$stmt = $this->pdo->prepare('UPDATE forum_posts SET highlighted_by = NULL, highlighted_at = NULL WHERE id = :id');
$stmt->execute(['id' => $postId]);
return;
}
$stmt = $this->pdo->prepare('UPDATE forum_posts SET highlighted_by = :uid, highlighted_at = NOW() WHERE id = :id');
$stmt->execute([
'uid' => $userId,
'id' => $postId,
]);
}
public function searchThreads(string $query, int $limit = 50, ?string $boardSlug = null): array
{
$conditions = [];
$params = [];
@@ -56,11 +140,24 @@ final class Community
$params[$ph2] = '%' . $tok . '%';
$i++;
}
$boardJoin = '';
$boardSelect = "NULL AS board_slug, NULL AS board_title, NULL AS category_slug, NULL AS category_title";
$boardWhere = '';
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards') && $this->hasTable('forum_categories')) {
$boardJoin = ' LEFT JOIN forum_boards fb ON fb.id = ft.board_id LEFT JOIN forum_categories fc ON fc.id = fb.category_id ';
$boardSelect = 'fb.slug AS board_slug, fb.title AS board_title, fc.slug AS category_slug, fc.title AS category_title';
if ($boardSlug !== null && $boardSlug !== '') {
$boardWhere = ' AND fb.slug = :boardSlug';
$params[':boardSlug'] = $boardSlug;
}
}
$where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : '';
$sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, ft.updated_at,
u.id as uid, u.created_at as user_created,
p.display_name,
$boardSelect,
(SELECT COUNT(*) FROM forum_posts fp WHERE fp.thread_id = ft.id) AS answers,
COALESCE(
(SELECT MAX(fp.created_at) FROM forum_posts fp WHERE fp.thread_id = ft.id),
@@ -84,7 +181,8 @@ final class Community
FROM forum_threads ft
JOIN users u ON u.id = ft.user_id
LEFT JOIN user_profiles p ON p.user_id = u.id
WHERE 1=1 $where
$boardJoin
WHERE 1=1 $where $boardWhere
ORDER BY ft.created_at DESC
LIMIT :lim";
$stmt = $this->pdo->prepare($sql);
@@ -96,14 +194,20 @@ final class Community
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
}
public function listThreads(int $limit = 50): array
public function listThreads(int $limit = 50, ?string $boardSlug = null): array
{
return $this->searchThreads('', $limit);
return $this->searchThreads('', $limit, $boardSlug);
}
public function getThread(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT ft.*, p.display_name FROM forum_threads ft LEFT JOIN user_profiles p ON p.user_id = ft.user_id WHERE ft.id = :id');
$select = 'ft.*, p.display_name';
$join = '';
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards') && $this->hasTable('forum_categories')) {
$select .= ', fb.slug AS board_slug, fb.title AS board_title, fc.slug AS category_slug, fc.title AS category_title';
$join = ' LEFT JOIN forum_boards fb ON fb.id = ft.board_id LEFT JOIN forum_categories fc ON fc.id = fb.category_id ';
}
$stmt = $this->pdo->prepare("SELECT $select FROM forum_threads ft LEFT JOIN user_profiles p ON p.user_id = ft.user_id $join WHERE ft.id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
return $row ?: null;
@@ -111,11 +215,97 @@ final class Community
public function listPosts(int $threadId): array
{
$stmt = $this->pdo->prepare('SELECT fp.*, p.display_name FROM forum_posts fp LEFT JOIN user_profiles p ON p.user_id = fp.user_id WHERE fp.thread_id = :id ORDER BY fp.created_at ASC');
$select = 'fp.*, p.display_name';
if ($this->hasColumn('forum_posts', 'highlighted_at')) {
$select .= ', hp.display_name AS highlighted_by_name';
} else {
$select .= ', NULL AS highlighted_by_name';
}
$join = ' LEFT JOIN user_profiles p ON p.user_id = fp.user_id ';
if ($this->hasColumn('forum_posts', 'highlighted_at')) {
$join .= ' LEFT JOIN user_profiles hp ON hp.user_id = fp.highlighted_by ';
}
$stmt = $this->pdo->prepare("SELECT $select FROM forum_posts fp $join WHERE fp.thread_id = :id ORDER BY fp.created_at ASC");
$stmt->execute([':id' => $threadId]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
}
public function getPost(int $postId): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM forum_posts WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $postId]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
return $row ?: null;
}
public function listForumCategories(): array
{
$structure = $this->forumStructure();
$boards = [];
foreach ($structure as $category) {
foreach ($category['boards'] as $board) {
$boards[$board['slug']] = $board;
}
}
$threadsByBoard = [];
foreach ($this->listThreads(200) as $thread) {
$slug = (string)($thread['board_slug'] ?? '');
if ($slug === '' || !isset($boards[$slug])) {
continue;
}
$threadsByBoard[$slug][] = $thread;
}
foreach ($structure as &$category) {
foreach ($category['boards'] as &$board) {
$items = $threadsByBoard[$board['slug']] ?? [];
$board['thread_count'] = count($items);
$replyCount = 0;
foreach ($items as $item) {
$replyCount += (int)($item['answers'] ?? 0);
}
$board['post_count'] = $replyCount;
$board['latest_thread'] = $items[0] ?? null;
}
unset($board);
}
unset($category);
return $structure;
}
public function getBoardBySlug(string $slug): ?array
{
foreach ($this->forumStructure() as $category) {
foreach ($category['boards'] as $board) {
if ($board['slug'] === $slug) {
if ($this->hasTable('forum_boards')) {
$stmt = $this->pdo->prepare('
SELECT fb.id, fb.slug, fb.title, fb.description, fb.icon, fc.slug AS category_slug, fc.title AS category_title
FROM forum_boards fb
JOIN forum_categories fc ON fc.id = fb.category_id
WHERE fb.slug = :slug
LIMIT 1
');
$stmt->execute(['slug' => $slug]);
$dbBoard = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($dbBoard) {
return array_merge($board, $dbBoard);
}
}
return array_merge($board, [
'category_slug' => $category['slug'],
'category_title' => $category['title'],
]);
}
}
}
return null;
}
public function computePoints(int $userId): float
{
// Primär: aggregierte Werte aus user_points_totals, Fallback: Summe aus user_points
@@ -223,4 +413,92 @@ final class Community
$fallback = $levels ? $levels[count($levels)-1] : ['label' => 'New Daddy','icon' => ''];
return ['label' => $fallback['label'], 'icon' => $fallback['icon'] ?? ''];
}
private function forumStructure(): array
{
if ($this->forumStructure !== null) {
return $this->forumStructure;
}
$file = dirname(__DIR__, 2) . '/config/community_forums.php';
$this->forumStructure = file_exists($file) ? (array) require $file : [];
$this->syncForumStructure();
return $this->forumStructure;
}
private function syncForumStructure(): void
{
if (!$this->hasTable('forum_categories') || !$this->hasTable('forum_boards')) {
return;
}
foreach ($this->forumStructure as $categoryIndex => $category) {
$stmt = $this->pdo->prepare('INSERT INTO forum_categories (slug, title, sort_order) VALUES (:slug, :title, :sortOrder) ON DUPLICATE KEY UPDATE title = VALUES(title), sort_order = VALUES(sort_order)');
$stmt->execute([
'slug' => $category['slug'],
'title' => $category['title'],
'sortOrder' => $categoryIndex,
]);
$catIdStmt = $this->pdo->prepare('SELECT id FROM forum_categories WHERE slug = :slug LIMIT 1');
$catIdStmt->execute(['slug' => $category['slug']]);
$categoryId = (int)$catIdStmt->fetchColumn();
foreach ($category['boards'] as $boardIndex => $board) {
$boardStmt = $this->pdo->prepare('
INSERT INTO forum_boards (category_id, slug, title, description, icon, sort_order)
VALUES (:categoryId, :slug, :title, :description, :icon, :sortOrder)
ON DUPLICATE KEY UPDATE
category_id = VALUES(category_id),
title = VALUES(title),
description = VALUES(description),
icon = VALUES(icon),
sort_order = VALUES(sort_order)
');
$boardStmt->execute([
'categoryId' => $categoryId,
'slug' => $board['slug'],
'title' => $board['title'],
'description' => $board['description'],
'icon' => $board['icon'] ?? null,
'sortOrder' => $boardIndex,
]);
}
}
}
private function hasTable(string $table): bool
{
if (array_key_exists($table, $this->tableCache)) {
return $this->tableCache[$table];
}
try {
$stmt = $this->pdo->prepare('SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table LIMIT 1');
$stmt->execute(['table' => $table]);
return $this->tableCache[$table] = (bool)$stmt->fetchColumn();
} catch (\Throwable) {
return $this->tableCache[$table] = false;
}
}
private function hasColumn(string $table, string $column): bool
{
$key = $table . '.' . $column;
if (array_key_exists($key, $this->columnCache)) {
return $this->columnCache[$key];
}
try {
$stmt = $this->pdo->prepare('SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = :table AND column_name = :column LIMIT 1');
$stmt->execute([
'table' => $table,
'column' => $column,
]);
return $this->columnCache[$key] = (bool)$stmt->fetchColumn();
} catch (\Throwable) {
return $this->columnCache[$key] = false;
}
}
}