Skip to main content

Overview

ForkBB uses a hierarchical structure to organize discussions: Categories contain Forums, which contain Topics, which contain Posts. This flexible system supports complex discussion boards with granular permissions.

Forum Hierarchy

1

Categories

Top-level organizational containers that group related forums together
2

Forums

Discussion areas that contain topics. Forums can have subforums for deeper organization
3

Topics

Individual discussion threads started by users
4

Posts

Individual messages within a topic thread

Categories

Categories provide the highest level of organization, implemented in app/Models/Category/Categories.php:20.

Category Management

app/Models/Category/Categories.php
public function init(): Categories
{
    $query = 'SELECT c.id, c.cat_name, c.disp_position
        FROM ::categories AS c
        ORDER BY c.disp_position';
    
    $this->repository = $this->c->DB->query($query)->fetchAll(PDO::FETCH_UNIQUE);
    
    return $this;
}

public function insert(string $name): int
{
    $pos = 0;
    
    foreach ($this->repository as $cat) {
        if ($cat['disp_position'] > $pos) {
            $pos = $cat['disp_position'];
        }
    }
    
    ++$pos;
    
    $vars = [
        ':name'     => $name,
        ':position' => $pos,
    ];
    $query = 'INSERT INTO ::categories (cat_name, disp_position)
        VALUES (?s:name, ?i:position)';
    
    $this->c->DB->exec($query, $vars);
    
    $cid = (int) $this->c->DB->lastInsertId();
    
    parent::set($cid, ['cat_name' => $name, 'disp_position' => $pos]);
    
    return $cid;
}
Categories are ordered by disp_position and can be reordered without affecting forum IDs.

Forums

Forums are the core discussion containers, implemented in app/Models/Forum/Forum.php:19.

Forum Structure

app/Models/Forum/Forum.php
protected function getparent(): ?Forum
{
    if (null === $this->parent_forum_id) {
        if (0 !== $this->id) {
            throw new RuntimeException('Parent is not defined');
        }
        
        return null;
    } else {
        return $this->manager->get($this->parent_forum_id);
    }
}

protected function getsubforums(): array
{
    $sub  = [];
    $attr = $this->getModelAttr('subforums');
    
    if (\is_array($attr)) {
        foreach ($attr as $id) {
            $sub[$id] = $this->manager->get($id);
        }
    }
    
    return $sub;
}

protected function getdescendants(): array
{
    $all  = [];
    $attr = $this->getModelAttr('descendants');
    
    if (\is_array($attr)) {
        foreach ($attr as $id) {
            $all[$id] = $this->manager->get($id);
        }
    }
    
    return $all;
}

Forum Properties

Name & Description

Each forum has a name and optional description for users

Parent/Child

Forums can have parent forums and multiple subforums

Moderators

Specific users can be assigned as moderators

Statistics

Topics, posts, and last post information tracked
app/Models/Forum/Forum.php
protected function getlink(): string
{
    if (0 === $this->id) {
        return $this->c->Router->link('Index');
    } else {
        return $this->c->Router->link(
            'Forum',
            [
                'id'   => $this->id,
                'name' => $this->friendly,
            ]
        );
    }
}

protected function getlinkCreateTopic(): string
{
    return $this->c->Router->link(
        'NewTopic',
        [
            'id' => $this->id,
        ]
    );
}

Forum Permissions

Forums have granular permission controls:

Topic Creation

app/Models/Forum/Forum.php
protected function getcanCreateTopic(): bool
{
    $user = $this->c->user;
    
    return 1 === $this->post_topics
        || (
            null === $this->post_topics
            && 1 === $user->g_post_topics
        )
        || $user->isAdmin
        || $user->isModerator($this);
}

Subscriptions

app/Models/Forum/Forum.php
protected function getcanSubscription(): bool
{
    return 1 === $this->c->config->b_forum_subscriptions
        && $this->id > 0
        && ! $this->c->user->isGuest
        && ! $this->c->user->isUnverified;
}
Admins and moderators can override forum-level permissions.

Forum Moderators

app/Models/Forum/Forum.php
protected function getmoderators(): array
{
    $attr = $this->getModelAttr('moderators');
    
    if (
        empty($attr)
        || ! \is_array($attr)
    ) {
        return [];
    }
    
    $viewUsers = $this->c->userRules->viewUsers;
    
    foreach ($attr as $id => &$cur) {
        $cur = [
            'name' => $cur,
            'link' => $viewUsers ?
                $this->c->Router->link(
                    'User',
                    [
                        'id'   => $id,
                        'name' => $this->c->Func->friendly($cur),
                    ]
                )
                : null,
        ];
    }
    
    unset($cur);
    
    return $attr;
}

public function modAdd(User ...$users): void
{
    $attr = $this->getModelAttr('moderators');
    
    if (
        empty($attr)
        || ! \is_array($attr)
    ) {
        $attr = [];
    }
    
    foreach ($users as $user) {
        if (! $user instanceof User) {
            throw new InvalidArgumentException('Expected User');
        }
        
        $attr[$user->id] = $user->username;
    }
    
    $this->moderators = $attr;
}

Topics

Topics are individual discussion threads, implemented in app/Models/Topic/Topic.php:21.

Topic Properties

app/Models/Topic/Topic.php
protected function getparent(): ?Forum
{
    if ($this->forum_id < 1) {
        throw new RuntimeException('Parent is not defined');
    }
    
    $forum = $this->c->forums->get($this->forum_id);
    
    if (
        ! $forum instanceof Forum
        || $forum->redirect_url
    ) {
        return null;
    } else {
        return $forum;
    }
}

protected function getname(): ?string
{
    return $this->censorSubject;
}

protected function getfriendly(): ?string
{
    return $this->c->Func->friendly($this->name);
}

Topic Status

Open/Closed

Topics can be closed to prevent new replies

Sticky

Important topics can be pinned to the top

Moved

Topics can be moved between forums

Deleted

Topics can be soft or hard deleted

Reply Permissions

app/Models/Topic/Topic.php
protected function getcanReply(): bool
{
    if ($this->moved_to) {
        return false;
    } elseif ($this->c->user->isAdmin) {
        return true;
    } elseif (
        $this->closed
        || $this->c->user->isBot
    ) {
        return false;
    } elseif (
        1 === $this->parent->post_replies
        || (
            null === $this->parent->post_replies
            && 1 === $this->c->user->g_post_replies
        )
        || $this->c->user->isModerator($this)
    ) {
        return true;
    } else {
        return false;
    }
}
app/Models/Topic/Topic.php
protected function getlink(): string
{
    return $this->c->Router->link(
        'Topic',
        [
            'id'   => $this->moved_to ?: $this->id,
            'name' => $this->friendly,
        ]
    );
}

protected function getlinkReply(): string
{
    return $this->c->Router->link(
        'NewReply',
        [
            'id' => $this->id,
        ]
    );
}

protected function getlinkLast(): string
{
    if (
        $this->moved_to
        || $this->last_post_id < 1
    ) {
        return '';
    } else {
        return $this->c->Router->link(
            'Topic',
            [
                'id'   => $this->moved_to ?: $this->id,
                'name' => $this->friendly,
                'page' => $this->numPages,
                '#'    => 'p' . $this->last_post_id,
            ]
        );
    }
}

Read Tracking

ForkBB tracks which posts users have read:

New Messages

app/Models/Topic/Topic.php
protected function gethasNew(): int|false
{
    if (
        $this->c->user->isGuest
        || $this->moved_to
    ) {
        return false;
    }
    
    $time = \max(
        (int) $this->c->user->u_mark_all_read,
        (int) $this->parent->mf_mark_all_read,
        (int) $this->c->user->last_visit,
        (int) $this->mt_last_visit
    );
    
    return $this->last_post > $time ? $time : false;
}

protected function getfirstNew(): int
{
    if (false === $this->hasNew) {
        return 0;
    } elseif ($this->posted > $this->hasNew) {
        return $this->first_post_id;
    }
    
    $vars = [
        ':tid'   => $this->id,
        ':visit' => $this->hasNew,
    ];
    $query = 'SELECT MIN(p.id)
        FROM ::posts AS p
        WHERE p.topic_id=?i:tid AND p.posted>?i:visit';
    
    return (int) $this->c->DB->query($query, $vars)->fetchColumn();
}

Visit Tracking

app/Models/Topic/Topic.php
public function updateVisits(): void
{
    if ($this->c->user->isGuest) {
        return;
    }
    
    $vars = [
        ':uid'   => $this->c->user->id,
        ':tid'   => $this->id,
        ':read'  => (int) $this->mt_last_read,
        ':visit' => (int) $this->mt_last_visit,
    ];
    $flag = false;
    
    if (false !== $this->hasNew) {
        $flag           = true;
        $vars[':visit'] = $this->last_post;
    }
    
    if (
        false !== $this->hasUnread
        && $this->timeMax > $this->hasUnread
    ) {
        $flag           = true;
        $vars[':read']  = $this->timeMax;
        $vars[':visit'] = $this->last_post;
    }
    
    if ($flag) {
        // Insert or update mark_of_topic record
        // ...
    }
}
Visit tracking allows users to see which topics have new posts since their last visit.

Pagination

Forum Pagination

app/Models/Forum/Forum.php
protected function getnumPages(): int
{
    if (! \is_int($this->num_topics)) {
        throw new RuntimeException('The model does not have the required data');
    }
    
    return (int) \ceil(($this->num_topics ?: 1) / $this->c->user->disp_topics);
}

protected function getpagination(): array
{
    return $this->c->Func->paginate(
        $this->numPages,
        $this->page,
        'Forum',
        [
            'id'   => $this->id,
            'name' => $this->friendly,
        ]
    );
}

public function pageData(): array
{
    if (! $this->hasPage()) {
        throw new InvalidArgumentException('Bad number of displayed page');
    }
    
    if (empty($this->num_topics)) {
        return [];
    }
    
    $this->createIdsList(
        $this->c->user->disp_topics,
        ($this->page - 1) * $this->c->user->disp_topics
    );
    
    return empty($this->idsList) ? [] : $this->c->topics->view($this);
}

Topic Pagination

app/Models/Topic/Topic.php
protected function getnumPages(): int
{
    if (null === $this->num_replies) {
        throw new RuntimeException('The model does not have the required data');
    }
    
    return (int) \ceil(($this->num_replies + 1) / $this->c->user->disp_posts);
}

public function pageData(): array
{
    if (! $this->hasPage()) {
        throw new InvalidArgumentException('Bad number of displayed page');
    }
    
    $vars = [
        ':tid'    => $this->id,
        ':offset' => ($this->page - 1) * $this->c->user->disp_posts,
        ':rows'   => $this->c->user->disp_posts,
    ];
    $query = 'SELECT p.id
        FROM ::posts AS p
        WHERE p.topic_id=?i:tid
        ORDER BY p.id
        LIMIT ?i:rows OFFSET ?i:offset';
    
    $list = $this->c->DB->query($query, $vars)->fetchAll(PDO::FETCH_COLUMN);
    
    $this->idsList = $list;
    
    return empty($this->idsList) ? [] : $this->c->posts->view($this);
}

Topic Views

View counting can be enabled for topics:
app/Models/Topic/Topic.php
protected function getshowViews(): bool
{
    return 1 === $this->c->config->b_topic_views;
}

public function incViews(): void
{
    $vars = [
        ':tid' => $this->id,
    ];
    $query = 'UPDATE ::topics
        SET num_views=num_views+1
        WHERE id=?i:tid';
    
    $this->c->DB->exec($query, $vars);
}

Topic Solutions

Forums can enable solution marking for Q&A style discussions:
app/Models/Topic/Topic.php
protected function getcanChSolution(): bool
{
    return 1 === $this->parent->use_solution
        && (
            $this->c->user->isAdmin
            || (
                $this->c->user->id === $this->poster_id
                && ! $this->c->user->isGuest
            )
        );
}

Table of Contents

Topics can automatically generate a table of contents from headers:
app/Models/Topic/Topic.php
protected array $idsLevel = [
    'h1' => 1,
    'h2' => 2,
    'h'  => 3,
    'h3' => 3,
    'h4' => 4,
    'h5' => 5,
    'h6' => 6,
];

public function addPostToToc(Post $post, bool $merge = false): Topic
{
    if (
        $post->poster_id < 1
        || $this->poster_id !== $post->poster_id
        || (
            empty($this->toc)
            && $this->first_post_id !== $post->id
        )
    ) {
        return $this;
    }
    
    $ids = $this->c->Parser->getIds(...(\array_keys($this->idsLevel)));
    
    if (empty($ids)) {
        if ($merge) {
            return $this;
        } elseif ($this->first_post_id === $post->id) {
            $this->toc = null;
            return $this;
        }
    }
    
    $r = [];
    
    foreach ($ids as $id => $tag) {
        $text  = \preg_replace('%^\s+|\s+$%uD', '', $this->c->Parser->getText($id));
        $ident = $this->c->Parser->createIdentifier($text);
        $r[]   = [$this->idsLevel[$tag], $ident, $text];
    }
    
    $toc = $this->toc ? \json_decode($this->toc, true, 512, \JSON_THROW_ON_ERROR) : [];
    
    if (
        $merge
        && ! empty($toc[$post->id])
    ) {
        $toc[$post->id] = \array_merge($toc[$post->id], $r);
    } else {
        $toc[$post->id] = $r;
    }
    
    \ksort($toc, \SORT_NUMERIC);
    
    $this->toc = \json_encode($toc, FORK_JSON_ENCODE);
    
    return $this;
}

BBCode

Learn about post formatting with BBCode

Search

Search topics and posts across forums

Moderation

Moderate content and manage forums

Best Practices

Use categories to logically group related forums. Keep the hierarchy shallow (2-3 levels max) for better usability.
Set permissions at the forum level when possible. Use moderators for specific forums rather than global moderation.
Cache forum trees and statistics. Use pagination to limit database queries for large forums.