cache = new DbStatementCache($dbConn); } public static function convertCategoryListToTree( iterable $catInfos, ForumCategoryInfo|string|null $parentInfo = null, ?Colour $colour = null ): array { if(!is_array($catInfos)) $catInfos = iterator_to_array($catInfos); $colour ??= Colour::none(); $tree = []; $predicate = $parentInfo ? fn($catInfo) => $catInfo->isDirectChildOf($parentInfo) : fn($catInfo) => !$catInfo->hasParent(); foreach($catInfos as $catInfo) { if(!$predicate($catInfo)) continue; $tree[$catInfo->getId()] = $item = new stdClass; $item->info = $catInfo; $item->colour = $catInfo->hasColour() ? $catInfo->getColour() : $colour; $item->children = self::convertCategoryListToTree($catInfos, $catInfo, $item->colour); $item->childIds = []; foreach($item->children as $child) { $item->childIds[] = $child->info->getId(); $item->childIds += $child->childIds; } } return $tree; } public function countCategories( ForumCategoryInfo|string|null|false $parentInfo = false, string|int|null $type = null, ?bool $hidden = null ): int { if($parentInfo instanceof ForumCategoryInfo) $parentInfo = $parentInfo->getId(); $isRootParent = false; $hasParentInfo = $parentInfo !== false; $hasType = $type !== null; $hasHidden = $hidden !== null; $args = 0; $query = 'SELECT COUNT(*) FROM msz_forum_categories'; if($hasParentInfo) { ++$args; $isRootParent = $parentInfo === null; if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL $query .= 'WHERE (forum_parent IS NULL OR forum_parent = 0)'; } else { $query .= 'WHERE forum_parent = ?'; } } if($hasType) { if(is_string($type)) { if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES)) throw new InvalidArgumentException('$type is not a valid alias.'); $type = ForumCategoryInfo::TYPE_ALIASES[$type]; } $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE'); } if($hasHidden) $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); $args = 0; $stmt = $this->cache->get($query); if($hasParentInfo && !$isRootParent) $stmt->addParameter(++$args, $parentInfo); if($hasType) $stmt->addParameter(++$args, $type); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? $result->getInteger(0) : 0; } public function getCategories( ForumCategoryInfo|string|null|false $parentInfo = false, string|int|null $type = null, ?bool $hidden = null, bool $asTree = false, ?Pagination $pagination = null ): iterable { $isRootParent = false; $hasParentInfo = $parentInfo !== false; $hasType = $type !== null; $hasHidden = $hidden !== null; $hasPagination = $pagination !== null; if($hasParentInfo && $asTree) throw new InvalidArgumentException('$asTree can only be used with $parentInfo set to false.'); $args = 0; $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories'; if($hasParentInfo) { ++$args; $isRootParent = $parentInfo === null; if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL $query .= ' WHERE (forum_parent IS NULL OR forum_parent = 0)'; } else { $query .= ' WHERE forum_parent = ?'; } } if($hasType) { if(is_string($type)) { if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES)) throw new InvalidArgumentException('$type is not a valid alias.'); $type = ForumCategoryInfo::TYPE_ALIASES[$type]; } $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE'); } if($hasHidden) $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); $query .= ' ORDER BY forum_parent, forum_type <> 1, forum_order'; if($hasPagination) $query .= ' LIMIT ? OFFSET ?'; $args = 0; $stmt = $this->cache->get($query); if($hasParentInfo && !$isRootParent) { if($parentInfo instanceof ForumCategoryInfo) $stmt->addParameter(++$args, $parentInfo->getId()); else $stmt->addParameter(++$args, $parentInfo); } if($hasType) $stmt->addParameter(++$args, $type); if($hasPagination) { $stmt->addParameter(++$args, $pagination->getRange()); $stmt->addParameter(++$args, $pagination->getOffset()); } $stmt->execute(); $cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...)); if($asTree) $cats = self::convertCategoryListToTree($cats); return $cats; } public function getCategory( ?string $categoryId = null, ForumTopicInfo|string|null $topicInfo = null, ForumPostInfo|string|null $postInfo = null ): ForumCategoryInfo { $hasCategoryId = $categoryId !== null; $hasTopicInfo = $topicInfo !== null; $hasPostInfo = $postInfo !== null; if(!$hasCategoryId && !$hasTopicInfo && !$hasPostInfo) throw new InvalidArgumentException('You must specify an argument.'); if(($hasCategoryId && ($hasTopicInfo || $hasPostInfo)) || ($hasTopicInfo && ($hasCategoryId || $hasPostInfo)) || ($hasPostInfo && ($hasCategoryId || $hasTopicInfo))) throw new InvalidArgumentException('Only one argument may be specified.'); $value = null; $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories'; if($hasCategoryId) { $query .= ' WHERE forum_id = ?'; $value = $categoryId; } if($hasTopicInfo) { if($topicInfo instanceof ForumTopicInfo) { $query .= ' WHERE forum_id = ?'; $value = $topicInfo->getCategoryId(); } else { $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_topics WHERE topic_id = ?)'; $value = $topicInfo; } } if($hasPostInfo) { if($postInfo instanceof ForumPostInfo) { $query .= ' WHERE forum_id = ?'; $value = $postInfo->getCategoryId(); } else { $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_posts WHERE post_id = ?)'; $value = $postInfo; } } $stmt = $this->cache->get($query); $stmt->addParameter(1, $value); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Forum category info not found.'); return ForumCategoryInfo::fromResult($result); } public function updateCategory( ForumCategoryInfo|string $categoryInfo ): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); } public function deleteCategory(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); } public function incrementCategoryClicks(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); // previous implementation also WHERE'd for forum_type = link but i don't think there's any other way to get here anyhow $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_link_clicks = forum_link_clicks + 1 WHERE forum_id = ? AND forum_link_clicks IS NOT NULL'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); } public function incrementCategoryTopics(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = forum_count_topics + 1 WHERE forum_id = ?'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); } public function incrementCategoryPosts(ForumCategoryInfo|string $categoryInfo): void { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_posts = forum_count_posts + 1 WHERE forum_id = ?'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); } public function getCategoryAncestry( ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo ): iterable { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo) $categoryInfo = $categoryInfo->getCategoryId(); $query = 'WITH RECURSIVE msz_cte_ancestry AS (' . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?' . ' UNION ALL' . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_ancestry AS ca ON fc.forum_id = ca.forum_parent' . ') SELECT * FROM msz_cte_ancestry'; $stmt = $this->cache->get($query); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); return $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...)); } public function getCategoryChildren( ForumCategoryInfo|string $parentInfo, bool $includeSelf = false, ?bool $hidden = null, bool $asTree = false ): iterable { if($parentInfo instanceof ForumCategoryInfo) $parentInfo = $parentInfo->getId(); $hasHidden = $hidden !== null; $query = 'WITH RECURSIVE msz_cte_children AS (' . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?' . ' UNION ALL' . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_children AS cc ON fc.forum_parent = cc.forum_id' . ') SELECT * FROM msz_cte_children'; $args = 0; if(!$includeSelf) { ++$args; $query .= ' WHERE forum_id <> ?'; } if($hasHidden) $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '='); $query .= ' ORDER BY forum_parent, forum_order'; $args = 0; $stmt = $this->cache->get($query); $stmt->addParameter(++$args, $parentInfo); if(!$includeSelf) $stmt->addParameter(++$args, $parentInfo); $stmt->execute(); $cats = $stmt->getResult()->getIterator(ForumCategoryInfo::fromResult(...)); if($asTree) $cats = self::convertCategoryListToTree($cats, $parentInfo); return $cats; } public function checkCategoryUnread( ForumCategoryInfo|string|array $categoryInfos, UserInfo|string|null $userInfo ): bool { if($userInfo === null) return false; if(!is_array($categoryInfos)) $categoryInfos = [$categoryInfos]; if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $args = 0; $stmt = $this->cache->get(sprintf( 'SELECT COUNT(*) FROM msz_forum_topics AS ft LEFT JOIN msz_forum_topics_track AS ftt ON ftt.topic_id = ft.topic_id AND ftt.user_id = ? WHERE ft.forum_id IN (%s) AND ft.topic_deleted IS NULL AND ft.topic_bumped >= NOW() - INTERVAL 1 MONTH AND (ftt.track_last_read IS NULL OR ftt.track_last_read < ft.topic_bumped)', DbTools::prepareListString($categoryInfos) )); $stmt->addParameter(++$args, $userInfo); foreach($categoryInfos as $categoryInfo) { if($categoryInfo instanceof ForumCategoryInfo) $stmt->addParameter(++$args, $categoryInfo->getId()); elseif(is_string($categoryInfo) || is_int($categoryInfo)) $stmt->addParameter(++$args, $categoryInfo); else throw new InvalidArgumentException('Invalid item in $categoryInfos.'); } $stmt->execute(); $result = $stmt->getResult(); return $result->next() && $result->getInteger(0) > 0; } public function updateUserReadCategory( UserInfo|string|null $userInfo, ForumCategoryInfo|string $categoryInfo ): void { if($userInfo === null) return; if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($categoryInfo instanceof $categoryInfo) $categoryInfo = $categoryInfo->getId(); $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) SELECT ?, topic_id, forum_id, NOW() FROM msz_forum_topics WHERE forum_id = ? AND topic_bumped >= NOW() - INTERVAL 1 MONTH'); $stmt->addParameter(1, $userInfo); $stmt->addParameter(2, $categoryInfo); $stmt->execute(); } public function getCategoryColour( ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo ): Colour { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo) $categoryInfo = $categoryInfo->getCategoryId(); $query = 'WITH RECURSIVE msz_cte_colours AS (' . 'SELECT forum_id, forum_parent, forum_colour FROM msz_forum_categories WHERE forum_id = ?' . ' UNION ALL' . ' SELECT fc.forum_id, fc.forum_parent, fc.forum_colour FROM msz_forum_categories AS fc JOIN msz_cte_colours AS cc ON fc.forum_id = cc.forum_parent' . ') SELECT forum_colour FROM msz_cte_colours WHERE forum_colour IS NOT NULL'; $stmt = $this->cache->get($query); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); $result = $stmt->getResult(); return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none(); } public function getMostActiveCategoryInfo( UserInfo|string $userInfo, array $exceptCategoryInfos = [], array $exceptTopicInfos = [], ?bool $deleted = null ): object { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); $hasExceptCategoryInfos = !empty($exceptCategoryInfos); $hasExceptTopicInfos = !empty($exceptTopicInfos); $hasDeleted = $deleted !== null; $query = 'SELECT forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = ?'; if($hasDeleted) $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); if($hasExceptCategoryInfos) $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos)); if($hasExceptTopicInfos) $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos)); $query .= ' GROUP BY forum_id ORDER BY post_count DESC LIMIT 1'; $args = 0; $stmt = $this->cache->get($query); $stmt->addParameter(++$args, $userInfo); foreach($exceptCategoryInfos as $categoryInfo) { if($categoryInfo instanceof ForumCategoryInfo) $stmt->addParameter(++$args, $categoryInfo->getId()); elseif(is_string($categoryInfo) || is_int($categoryInfo)) $stmt->addParameter(++$args, (string)$categoryInfo); else throw new InvalidArgumentException('$exceptCategoryInfos may only contain string ids or instances of ForumCategoryInfo.'); } foreach($exceptTopicInfos as $topicInfo) { if($topicInfo instanceof ForumTopicInfo) $stmt->addParameter(++$args, $topicInfo->getId()); elseif(is_string($topicInfo) || is_int($topicInfo)) $stmt->addParameter(++$args, (string)$topicInfo); else throw new InvalidArgumentException('$exceptTopicInfos may only contain string ids or instances of ForumTopicInfo.'); } $stmt->execute(); $result = $stmt->getResult(); $info = new stdClass; $info->success = $result->next(); if($info->success) { $info->categoryId = $result->getString(0); $info->postCount = $result->getInteger(1); } return $info; } public function syncForumCounters( ForumCategoryInfo|string|null $categoryInfo = null, bool $updateCounters = true ): object { if($categoryInfo instanceof ForumCategoryInfo) $categoryInfo = $categoryInfo->getId(); elseif($categoryInfo === null) $categoryInfo = '0'; $counters = new stdClass; $stmt = $this->cache->get('SELECT ? AS target_category_id, (SELECT COUNT(*) FROM msz_forum_topics WHERE forum_id = target_category_id AND topic_deleted IS NULL) AS count_topics, (SELECT COUNT(*) FROM msz_forum_posts WHERE forum_id = target_category_id AND post_deleted IS NULL) AS count_posts'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('Failed to fetch forum category counters.'); $counters->topics = $result->getInteger(1); $counters->posts = $result->getInteger(2); $stmt = $this->cache->get('SELECT forum_id FROM msz_forum_categories WHERE forum_parent = ?'); $stmt->addParameter(1, $categoryInfo); $stmt->execute(); $children = []; $result = $stmt->getResult(); while($result->next()) $children[] = $result->getString(0); foreach($children as $childId) { $childCounters = $this->syncForumCounters($childId, $updateCounters); $counters->topics += $childCounters->topics; $counters->posts += $childCounters->posts; } if($updateCounters && $categoryInfo !== '0') { $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = ?, forum_count_posts = ? WHERE forum_id = ?'); $stmt->addParameter(1, $counters->topics); $stmt->addParameter(2, $counters->posts); $stmt->addParameter(3, $categoryInfo); $stmt->execute(); } return $counters; } }