Read from the package's own PHP files at the commit f074f7706ccb: its namespaces, classes, interfaces and enums, their public constants, properties and methods with their PHPDoc, and the options its factories take. Nothing of it is run.
DemoControllerclassThe demonstrator's one page: it seeds a small forum, signs a visitor in, and reports on the wiring - then hands over to the forum itself.
2 methods
UserclassThe demo's member. base-bundle's User identifies people by email and has no username; the forum and the mailbox address members by username (an @mention, a recipient typed by hand), so the application's User carries one - as every base-bundle application's User does.
3 methods
UserRepositoryclass
ForumBundleclassA discussion forum on top of base-bundle's Thread.
3 methods
OrderControllerclassThe BBS's display order on one screen, the way a settings page reads: the groups with their boards (a board can change group here too), and the tags of the tag cloud. CategoryCrudController edits one board at a time - ordering twenty of them by typing positions is what this spares.
2 methods · 1 constant
Base\Forum\Controller\Admin\Crud
CategoryCrudControllerclassAdmin CRUD for the boards.
4 methods
TopicCrudControllerclassAdmin CRUD for the topics. Posts are moderated on the site itself.
4 methods
ForumControllerclassThe reading side of the forum: the board index, a board, a tag, the search. Writing lives in TopicController.
7 methods · 5 constants
TopicControllerclassA topic: reading it, opening one, replying, editing, and the moderation switches (pin, lock, delete).
10 methods · 3 constants
Base\Forum\DependencyInjection
ForumConfigurationclass1 method
ForumExtensionclass2 methods
CategoryclassA board of the forum: "Agora", "Helpz", "Games"... Categories form a two-level tree, the way phpBB's categories held forums: a root category is a group (the old `phpbb_categories`), its children are the boards topics are posted in (the old `phpbb_forums`). Topics may only be posted in a leaf.
36 methods
PollclassA topic's poll - phpBB's "sondage", which the 2004 BBS had above a topic's messages: one question, a few answers, and each member picks one of them - or, when the poll allows it (maxChoices), up to that many.
15 methods · 2 constants
PollVoteclassOne answer a member picked in a poll: one row per answer, so a poll that allows several (Poll::$maxChoices) holds a member's picks as several rows.
6 methods
PostclassOne message in a topic. Written in Markdown (a "modern forum" writes Markdown, the way Discourse and the CERN ROOT forum do) and rendered through Base\Forum\Service\MarkdownRenderer, which escapes raw HTML.
24 methods
TopicclassA forum topic is a Thread: title, slug, tags, owner(s), followers, likes, mentions, publish state and soft delete come from base-bundle. The body of the topic is its first Post, so a topic and its replies are read, quoted, edited and moderated the same way.
28 methods
SitemapListenerclassThe forum's own pages in /sitemap.xml: base-bundle lists the routes it can generate by itself, and a board or a topic is behind a slug it cannot know.
2 methods · 1 constant
CategoryModelclassWhat the "new board" form collects (ForumController::CategoryNew): the name, the group it goes in - none makes it a group of its own - and what it is for.
5 properties
PostModelclassWhat a reply (or an edit of one) collects. A DTO rather than the Post itself: base-bundle's form factory refuses an entity as form data, to keep a half-submitted form from ever reaching a flush.
1 method · 2 properties
TopicModelclassWhat the "new topic" form collects: the topic's title, board and tags, and the opening post's content - two entities, one form - and, when it is given a question, the topic's poll.
3 methods · 9 properties
CategoryTypeclassA board opened from the BBS itself (ForumController::CategoryNew): its name, the group it goes in, a line to say what it is for. Left without a group, it is a new group. The back office's CRUD has the rest - icon, colour, the rank a board is reserved to, the lock.
2 methods
PostTypeclassA reply, or the edit of one - just the Markdown body (see PostModel).
3 methods
TopicTypeclass3 methods
CategoryRepositoryclass3 methods
PostRepositoryclass5 methods
TopicRepositoryclassExtends ThreadRepository (not ServiceEntityRepository) on purpose: Thread is #[Hierarchify], which requires HierarchifyTrait on the repository of every subclass, and that trait comes with the parent.
15 methods
ForumVoterclassWho may do what on the forum.
2 methods · 7 constants
MarkdownRendererclassPosts are Markdown, rendered here with GitHub flavouring (tables, task lists, strikethrough, autolinks) and NO raw HTML: anything a member types as a tag is escaped, links are never allowed to be javascript:, and nesting is capped so a pathological post cannot stall the renderer.
4 methods
ForumTwigExtensionclass8 methods
App\Controller
DemoController class
class DemoController extends AbstractController DemoController.php:26
The demonstrator's one page: it seeds a small forum, signs a visitor in, and reports on the wiring - then hands over to the forum itself.
Everything here is demo scaffolding. A real application seeds boards from a fixture or the admin, and signs members in through base-bundle's security controllers; nothing in this file is a pattern to copy.
DemoController::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly MarkdownRenderer $markdown, readonly Security $security)DemoController::index()
public function index(): ResponseApp\Entity
User class
class User extends User User.php:15
The demo's member. base-bundle's User identifies people by email and has no username; the forum and the mailbox address members by username (an @mention, a recipient typed by hand), so the application's User carries one - as every base-bundle application's User does.
User::getUsername()
public function getUsername(): ?stringUser::setUsername()
public function setUsername(string $username): selfUser::__toString()
public function __toString(): stringApp\Repository
UserRepository class
class UserRepository extends UserRepository UserRepository.php:5
Base\Forum
ForumBundle class
class ForumBundle extends AbstractBaseBundle ForumBundle.php:17
A discussion forum on top of base-bundle's Thread.
A Topic IS a Thread (JOINED subclass), so it inherits what base-bundle already gives every thread - tags, owners, followers, likes, mentions, a slug, publish states and soft delete - and the forum only adds what a forum needs on top: categories, posts, pins, locks and view counts.
ForumBundle::__construct()
public function __construct()ForumBundle::getPath()
public function getPath(): stringModern bundle layout: the class lives in src/, the bundle root is the package root - so TwigBundle picks up ./templates as @Forum and Symfony picks up ./translations. Doctrine's auto_mapping still finds the entities: for attribute mappings it looks next to the bundle CLASS, i.e. src/Entity, with the prefix Base\Forum\Entity.
ForumBundle::build()
public function build(ContainerBuilder $container): voidBase\Forum\Controller\Admin
OrderController class
class OrderController extends AbstractController OrderController.php:30
The BBS's display order on one screen, the way a settings page reads: the groups with their boards (a board can change group here too), and the tags of the tag cloud. CategoryCrudController edits one board at a time - ordering twenty of them by typing positions is what this spares.
Categories are written 10, 20, 30... so a board added later through the CRUD can still slot in between two without renumbering the rest. Tags go the other way, highest first, because Tag::$priority is a priority: the tag on top gets the largest value (TopicRepository::findTagUsage).
const CSRF_TOKEN_ID = 'forum_admin_order'
OrderController::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly CategoryRepository $categories, readonly TranslatorInterface $translator, readonly AdminContext $adminContext, readonly MenuBuilder $menuBuilder)OrderController::Order()
public function Order(Request $request): ResponseBase\Forum\Controller\Admin\Crud
CategoryCrudController class
class CategoryCrudController extends AbstractCrudController CategoryCrudController.php:22
Admin CRUD for the boards.
CategoryCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringCategoryCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringCategoryCrudController::configureFilters()
public function configureFilters(Filters $filters): FiltersCategoryCrudController::configureFields()
public function configureFields(string $pageName): iterableTopicCrudController class
class TopicCrudController extends AbstractCrudController TopicCrudController.php:17
Admin CRUD for the topics. Posts are moderated on the site itself.
TopicCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringTopicCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringTopicCrudController::configureFilters()
public function configureFilters(Filters $filters): FiltersTopicCrudController::configureFields()
public function configureFields(string $pageName): iterableBase\Forum\Controller\Client
ForumController class
class ForumController extends AbstractController ForumController.php:29
The reading side of the forum: the board index, a board, a tag, the search. Writing lives in TopicController.
Route names are forum_*; paths stay under /bbs, the name the site has
always given its forum ("BBS (forums)" in the menu).
const VIEWS = ['classic', 'feed']The index's views. "classic" is the phpBB board list, grouped; "feed" is the flat, Discourse-like feed with the announcements and the pinned topics as post-its on top. A view no longer offered - a cookie from before (the French names these once had, the "arbre" the BBS was once grown as) - falls back to the first.
const SORTS = ['recent', 'popular']The feed's two orders: the latest activity, or what is busiest lately.
const DISPLAY = ['view' => [self::VIEW_COOKIE, self::VIEWS], 'sort' => [self::SORT_COOKIE, self::SORTS], 'style' => [TopicController::STYLE_COOKIE, TopicController::STYLES]]How a reader likes the BBS drawn, each choice remembered per browser for a year - never carried in the address: the index's view, the feed's order, a topic's style. What the buttons send (display()), name => [cookie, allowed values].
ForumController::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly PaginatorInterface $paginator, readonly TranslatorInterface $translator, readonly int $topicsPerPage = 20, readonly int $hotThreshold = 25)ForumController::Display()
public function Display(Request $request): ResponseA display choice (DISPLAY) remembered, then back to the page it was made on. A small POST form per
button rather than a query parameter: the address stays the page's own, and reloading it,
sharing it or coming back to it later shows what the reader last chose. Any other name or value is
ignored; back is only ever a path on this site.
ForumController::Index()
public function Index(Request $request): ResponseForumController::Category()
public function Category(Request $request, string $slug): ResponseForumController::CategoryNew()
public function CategoryNew(Request $request): Response"Nouveau forum": a board opened from the BBS itself, for FORUM_ADMIN (forum.admin_role). ?dans=<group slug> chooses its group; left without one, it is a new group. It lands last in its group - the order page (/admin/bbs/ordre) moves it, and the back office's CRUD has the rest.
Declared with a priority: "/bbs/{slug}" would take "nouveau-forum" for a topic.
ForumController::Tag()
public function Tag(Request $request, string $slug): ResponseForumController::Search()
public function Search(Request $request): ResponseTopicController class
class TopicController extends AbstractController TopicController.php:38
A topic: reading it, opening one, replying, editing, and the moderation switches (pin, lock, delete).
const STYLES = ['retro', 'cards']A topic's two looks: the messages as separate cards, or the 2005 board's one box with its rows (topic/show.html.twig). Chosen with the reader's choice (ForumController::Display()), remembered per browser in STYLE_COOKIE.
const STREAM_HEADER = 'X-Forum-Stream'Asked by forum-thread.js for one more page of a topic being read as a single stream (Discourse's way): the page's posts alone, and the reading is not counted again.
TopicController::__construct()
public function __construct(readonly EntityManagerInterface $entityManager, readonly PaginatorInterface $paginator, readonly MarkdownRenderer $markdown, readonly TranslatorInterface $translator, readonly int $postsPerPage = 10, readonly int $floodInterval = 15)TopicController::Create()
public function Create(Request $request, ?string $category = null): ResponseDeclared before Show(): "/bbs/{slug}" would otherwise swallow "nouveau" as a topic slug.
TopicController::Show()
public function Show(Request $request, string $slug): ResponseTopicController::Reply()
public function Reply(Request $request, string $slug): ResponseTopicController::Vote()
public function Vote(Request $request, string $slug): ResponseA vote on the topic's poll: one answer, or on a poll that allows several (Poll::$maxChoices) up to that many, sent together. Once per member and final once cast, as the 2004 BBS had it; none on a locked topic.
TopicController::Edit()
public function Edit(Request $request, string $slug): ResponseTopicController::Moderate()
public function Moderate(Request $request, string $slug, string $action): ResponsePin, lock, delete: one POST endpoint, one switch. CSRF-protected the plain way (a token in the form), no JavaScript needed.
TopicController::ShowPost()
public function ShowPost(int $id): ResponseA message's own address: its place in its topic, on whichever page it is.
TopicController::EditPost()
public function EditPost(Request $request, int $id): ResponseTopicController::DeletePost()
public function DeletePost(Request $request, int $id): ResponseBase\Forum\DependencyInjection
ForumConfiguration class
class ForumConfiguration extends AbstractBaseConfiguration ForumConfiguration.php:8
ForumConfiguration::getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilderForumExtension class
class ForumExtension extends AbstractBaseExtension ForumExtension.php:11
ForumExtension::getConfiguration()
public function getConfiguration(array $config, ContainerBuilder $container): ForumConfigurationForumExtension::load()
public function load(array $configs, ContainerBuilder $container): voidBase\Forum\Entity
Category class
class Category implements LinkableInterface, IconizeInterface Category.php:25
A board of the forum: "Agora", "Helpz", "Games"...
Categories form a two-level tree, the way phpBB's categories held forums:
a root category is a group (the old phpbb_categories), its children are
the boards topics are posted in (the old phpbb_forums). Topics may only
be posted in a leaf.
Category::__construct()
public function __construct(?string $title = null, ?Category $parent = null)Category::__toString()
public function __toString(): stringCategory::__iconize()
public function __iconize(): ?arrayCategory::__iconizeStatic()
public static function __iconizeStatic(): ?arrayCategory::__toLink()
public function __toLink(array $routeParameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): ?stringCategory::getId()
public function getId(): ?intCategory::getTitle()
public function getTitle(): ?stringCategory::setTitle()
public function setTitle(?string $title): selfCategory::getSlug()
public function getSlug(): ?stringCategory::setSlug()
public function setSlug(?string $slug): selfCategory::getDescription()
public function getDescription(): ?stringCategory::setDescription()
public function setDescription(?string $description): selfCategory::getIcon()
public function getIcon(): ?stringCategory::setIcon()
public function setIcon(?string $icon): selfCategory::getColor()
public function getColor(): ?stringCategory::setColor()
public function setColor(?string $color): selfCategory::getPosition()
public function getPosition(): intCategory::setPosition()
public function setPosition(int $position): selfCategory::isLocked()
public function isLocked(): boolCategory::setLocked()
public function setLocked(bool $locked): selfCategory::isAnnouncement()
public function isAnnouncement(): boolCategory::setAnnouncement()
public function setAnnouncement(bool $announcement): selfCategory::allowsPolls()
public function allowsPolls(): boolCategory::isPolls()
public function isPolls(): boolCategory::setPolls()
public function setPolls(bool $polls): selfCategory::getRequiredRole()
public function getRequiredRole(): ?stringCategory::setRequiredRole()
public function setRequiredRole(?string $requiredRole): selfCategory::getParent()
public function getParent(): ?CategoryCategory::setParent()
public function setParent(?Category $parent): selfCategory::getChildren()
public function getChildren(): CollectionCategory::addChild()
public function addChild(Category $child): selfCategory::isBoard()
public function isBoard(): boolA board (leaf) takes topics; a group only holds boards.
Category::isGroup()
public function isGroup(): boolCategory::getTopics()
public function getTopics(): CollectionCategory::getCreatedAt()
public function getCreatedAt(): ?\DateTimeInterfaceCategory::getUpdatedAt()
public function getUpdatedAt(): ?\DateTimeInterfacePoll class
class Poll Poll.php:20
A topic's poll - phpBB's "sondage", which the 2004 BBS had above a topic's messages: one question, a few answers, and each member picks one of them - or, when the poll allows it (maxChoices), up to that many.
The answers are a plain list kept on the poll (their order is their identity: a vote records the index it chose), because a poll's answers are written once, with the topic, and never edited - editing an answer after votes were cast would change what those votes meant.
const MIN_OPTIONS = 2const MAX_OPTIONS = 10
Poll::__construct()
public function __construct(Topic $topic, string $question, array $options, int $maxChoices = 1)Poll::getId()
public function getId(): ?intPoll::getTopic()
public function getTopic(): ?TopicPoll::getQuestion()
public function getQuestion(): stringPoll::getOptions()
public function getOptions(): arrayPoll::getMaxChoices()
public function getMaxChoices(): intPoll::isMultiple()
public function isMultiple(): boolPoll::getCreatedAt()
public function getCreatedAt(): \DateTimeInterfacePoll::getVotes()
public function getVotes(): CollectionPoll::addVote()
public function addVote(PollVote $vote): selfPoll::isOpen()
public function isOpen(): boolA poll closes with its topic: a locked topic takes no reply, and no vote either.
Poll::getCounts()
public function getCounts(): array- Returns
votes per answer, by the answer's index
Poll::getTotal()
public function getTotal(): intHow many members voted - not how many answers were picked: on a poll that allows several, one member's three answers are one voter, and an answer's share is the share of VOTERS who picked it (so the shares may add up past 100%, as they do on any "pick several" poll).
Poll::getChoicesOf()
public function getChoicesOf(?User $user): array- Returns
the answers a member picked, by index; empty when they have not voted
Poll::hasOption()
public function hasOption(int $choice): boolPollVote class
class PollVote PollVote.php:15
One answer a member picked in a poll: one row per answer, so a poll that allows several (Poll::$maxChoices) holds a member's picks as several rows.
The same answer twice is what the unique index refuses - not a check in the controller - so two clicks racing each other cannot count one pick twice; how many answers a member may pick is the vote action's to hold.
PollVote::__construct()
public function __construct(Poll $poll, User $user, int $choice)PollVote::getId()
public function getId(): ?intPollVote::getPoll()
public function getPoll(): ?PollPollVote::getUser()
public function getUser(): ?UserPollVote::getChoice()
public function getChoice(): intPollVote::getCreatedAt()
public function getCreatedAt(): \DateTimeInterfacePost class
class Post Post.php:20
One message in a topic. Written in Markdown (a "modern forum" writes Markdown, the way Discourse and the CERN ROOT forum do) and rendered through Base\Forum\Service\MarkdownRenderer, which escapes raw HTML.
Deleted posts are kept (deletedAt) so a topic never loses its numbering and moderators can see what was removed.
Post::__construct()
public function __construct(?User $author = null, ?string $content = null)Post::__toString()
public function __toString(): stringPost::getId()
public function getId(): ?intPost::getTopic()
public function getTopic(): ?TopicPost::setTopic()
public function setTopic(?Topic $topic): selfPost::getAuthor()
public function getAuthor(): ?UserPost::setAuthor()
public function setAuthor(?User $author): selfPost::getContent()
public function getContent(): ?stringPost::setContent()
public function setContent(?string $content): selfPost::getCreatedAt()
public function getCreatedAt(): ?\DateTimeInterfacePost::getUpdatedAt()
public function getUpdatedAt(): ?\DateTimeInterfacePost::getShownAt()
public function getShownAt(): ?\DateTimeInterfaceThe date the post is shown with: when it was written - except a scheduled topic's opening post, which is dated the hour its topic came out (Topic::schedule()). createdAt itself cannot carry that hour: it is a #[Timestamp] and gets overwritten on persist.
Post::getEditedAt()
public function getEditedAt(): ?\DateTimeInterfacePost::markEdited()
public function markEdited(): selfPost::getEditCount()
public function getEditCount(): intPost::isDeleted()
public function isDeleted(): boolPost::getDeletedAt()
public function getDeletedAt(): ?\DateTimeInterfacePost::getDeletedBy()
public function getDeletedBy(): ?UserPost::delete()
public function delete(?User $by = null): selfPost::restore()
public function restore(): selfPost::getReplyTo()
public function getReplyTo(): ?PostPost::setReplyTo()
public function setReplyTo(?Post $replyTo): selfPost::isFirst()
public function isFirst(): boolIs this the topic's opening post?
Post::getNumber()
public function getNumber(): int1-based position in the topic, for "#12" anchors and permalinks.
Topic class
class Topic extends Thread implements LinkableInterface Topic.php:27
A forum topic is a Thread: title, slug, tags, owner(s), followers, likes, mentions, publish state and soft delete come from base-bundle. The body of the topic is its first Post, so a topic and its replies are read, quoted, edited and moderated the same way.
A topic may be written ahead of its hour (schedule()): until publishedAt it is out of every list and closed to replies, and only its authors and the moderators of its board can open it (ForumVoter). The date alone decides - nothing has to run at that hour for it to appear.
Topic::__construct()
public function __construct(?User $owner = null, ?Category $category = null, ?string $title = null)Topic::schedule()
public function schedule(?\DateTimeInterface $at): selfOut at $at when that is still to come; otherwise now.
The state stays PUBLISH either way. Base-bundle's FUTURE waits for its thread:publishable command to be run to become PUBLISH, and nothing runs it here: the lists go by the date (TopicRepository::whereOut()), so a scheduled topic comes out at its hour on its own.
A topic nobody has answered yet moves its last activity to that hour too: the lists sort by it, and a topic out at 18:00 must come in at 18:00, not at the hour it was written.
Topic::isUpcoming()
public function isUpcoming(): boolWritten, but not out yet (schedule()).
Topic::setAuthor()
public function setAuthor(User $author): selfWho the topic is signed by: its opening post and its owner, together.
An admin may publish in someone else's name (TopicController).
Topic::__toString()
public function __toString(): stringTopic::__toLink()
public function __toLink(array $routeParameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): ?stringTopic::getCategory()
public function getCategory(): ?CategoryTopic::setCategory()
public function setCategory(?Category $category): selfTopic::getPosts()
public function getPosts(): CollectionTopic::addPost()
public function addPost(Post $post): selfTopic::removePost()
public function removePost(Post $post): selfTopic::getFirstPost()
public function getFirstPost(): ?PostThe opening post: the topic's own body.
Topic::getAuthor()
public function getAuthor(): ?UserTopic::isPinned()
public function isPinned(): boolTopic::setPinned()
public function setPinned(bool $pinned): selfTopic::isLocked()
public function isLocked(): boolTopic::setLocked()
public function setLocked(bool $locked): selfTopic::getViews()
public function getViews(): intTopic::setViews()
public function setViews(int $views): selfTopic::getReplies()
public function getReplies(): intTopic::setReplies()
public function setReplies(int $replies): selfTopic::getLastPostAt()
public function getLastPostAt(): ?\DateTimeInterfaceTopic::setLastPostAt()
public function setLastPostAt(?\DateTimeInterface $lastPostAt): selfTopic::getPoll()
public function getPoll(): ?PollTopic::setPoll()
public function setPoll(?Poll $poll): selfTopic::getLastPoster()
public function getLastPoster(): ?UserTopic::setLastPoster()
public function setLastPoster(?User $lastPoster): selfTopic::getActivityAt()
public function getActivityAt(): ?\DateTimeInterfaceLast activity, for sorting: the last reply, else the topic itself.
Base\Forum\EventListener
SitemapListener class
final class SitemapListener SitemapListener.php:19
The forum's own pages in /sitemap.xml: base-bundle lists the routes it can generate by itself, and a board or a topic is behind a slug it cannot know.
Only what a visitor may read goes in: a board reserved to a role is left out, and so is everything written in it. A topic carries the date of its last message, so a crawler comes back to the ones that move.
const MAX_TOPICS = 5000Enough for a busy forum, short of a sitemap's 50 000 URL limit.
SitemapListener::__construct()
public function __construct(readonly CategoryRepository $categories, readonly TopicRepository $topics)SitemapListener::__invoke()
public function __invoke(SitemapEvent $event): voidBase\Forum\Form\Model
CategoryModel class
class CategoryModel CategoryModel.php:12
What the "new board" form collects (ForumController::CategoryNew): the name, the group it goes in - none makes it a group of its own - and what it is for.
public ?string $title = nullpublic ?Category $parent = nullpublic ?string $description = nullpublic bool $announcement = falseA board of announcements: it and its topics wear the megaphone badge.
public bool $polls = trueWhether topics opened in it may carry a poll.
PostModel class
class PostModel PostModel.php:13
What a reply (or an edit of one) collects. A DTO rather than the Post itself: base-bundle's form factory refuses an entity as form data, to keep a half-submitted form from ever reaching a flush.
public ?string $content = nullpublic ?int $replyTo = null
PostModel::fromPost()
public static function fromPost(Post $post): selfTopicModel class
class TopicModel TopicModel.php:15
What the "new topic" form collects: the topic's title, board and tags, and the opening post's content - two entities, one form - and, when it is given a question, the topic's poll.
public ?string $title = nullpublic ?Category $category = nullpublic ?string $content = nullpublic ?string $pollQuestion = nullA poll is optional: no question, no poll.
public ?string $pollOptions = nullIts answers, one per line; blank lines are dropped.
public ?int $pollMax = 1How many answers a member may pick: 1, a single one; more, up to that many (Poll::$maxChoices).
public ?\DateTimeInterface $publishedAt = nullWhen it comes out (Topic::schedule()): empty, at once. Only asked of those who may schedule (ForumVoter::SCHEDULE).
The member it is published under: the forum admin may sign a topic with another member's name (ForumVoter::ADMIN), picked from a search.
Empty, their own.
TopicModel::pollOptionList()
public function pollOptionList(): arrayTopicModel::hasPoll()
public function hasPoll(): boolTopicModel::validatePoll()
public function validatePoll(ExecutionContextInterface $context): voidBase\Forum\Form\Type
CategoryType class
class CategoryType extends AbstractType CategoryType.php:22
A board opened from the BBS itself (ForumController::CategoryNew): its name, the group it goes in, a line to say what it is for. Left without a group, it is a new group. The back office's CRUD has the rest - icon, colour, the rank a board is reserved to, the lock.
CategoryType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidCategoryType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidPostType class
class PostType extends AbstractType PostType.php:14
A reply, or the edit of one - just the Markdown body (see PostModel).
PostType::__construct()
public function __construct(readonly int $contentMaxLength = 20000)PostType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidPostType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidTopicType class
class TopicType extends AbstractType TopicType.php:21
TopicType::__construct()
public function __construct(readonly int $titleMaxLength = 120, readonly int $contentMaxLength = 20000)TopicType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidTopicType::buildForm()
public function buildForm(FormBuilderInterface $builder, array $options): voidBase\Forum\Repository
CategoryRepository class
class CategoryRepository extends ServiceEntityRepository CategoryRepository.php:14
CategoryRepository::findTree()
public function findTree(): arrayThe whole tree in display order: groups first, each followed by its boards - one query, then assembled in memory.
- Returns
root groups, with their children loaded
CategoryRepository::findBoards()
public function findBoards(): array- Returns
the boards (leaves) topics can be posted in
CategoryRepository::findOneBySlug()
public function findOneBySlug(string $slug): ?CategoryPostRepository class
class PostRepository extends ServiceEntityRepository PostRepository.php:17
PostRepository::createTopicQuery()
public function createTopicQuery(Topic $topic): QueryPostRepository::findOutline()
public function findOutline(Topic $topic): arrayThe whole topic, light: one scalar row per post, in reading order, for the timeline and the posts' own hierarchy (TopicController::outline()).
No content, no entities - a topic of a thousand posts stays cheap.
PostRepository::findLastByAuthor()
public function findLastByAuthor(User $author): ?PostThe member's most recent post, for the flood check.
PostRepository::findPageOf()
public function findPageOf(Post $post, int $perPage): intWhich page of a topic a post lands on, for permalinks.
PostRepository::countByAuthor()
public function countByAuthor(User $author): intHow many posts a member wrote - the profile's "Messages sur le forum".
TopicRepository class
class TopicRepository extends ThreadRepository TopicRepository.php:22
Extends ThreadRepository (not ServiceEntityRepository) on purpose: Thread is #[Hierarchify], which requires HierarchifyTrait on the repository of every subclass, and that trait comes with the parent.
TopicRepository::whereOut()
public function whereOut(QueryBuilder $qb, string $alias = 't'): QueryBuilderOnly the topics that are out: a scheduled one (Topic::schedule()) stays out of every list until its hour. Every query below that lists topics goes through here - a list that forgot it would give the topic away before its time, on the home page as much as on the BBS.
TopicRepository::createListQueryBuilder()
public function createListQueryBuilder(bool $withUpcoming = false): QueryBuilderA listing query: pinned first, then by last activity. Soft-deleted topics are filtered by base-bundle's Trasheable filter, so nothing to do here about them. Scheduled topics are left out, unless asked for ($withUpcoming: a board's own list, read by those who may see them).
TopicRepository::createCategoryQuery()
public function createCategoryQuery(Category $category, bool $withUpcoming = false): QueryA board's topics. $withUpcoming for the moderators of that board: they see what is scheduled there, on top (its last activity is its hour to come) and marked as such (_topic_row.html.twig).
TopicRepository::createTagQuery()
public function createTagQuery(Tag $tag, ?array $readableCategoryIds = null): QueryA tag's topics, of the readable boards only (the caller passes them, as for createLatestQuery(); null: every board's).
TopicRepository::createLatestQuery()
public function createLatestQuery(?array $readableCategoryIds = null): QueryLatest activity across the whole forum, for the front page and the "unread" style feeds - never pinned first here, that only makes sense inside a board.
TopicRepository::createTrendingQuery()
public function createTrendingQuery(?array $readableCategoryIds = null, int $days = 30): QueryWhat people are talking about: among the topics that have moved in the
last days, the busiest first - replies, then readers, then the most
recently active. The feed's second order ("popular"). Readable
boards only, the caller passing them as for createLatestQuery().
TopicRepository::findPinned()
public function findPinned(array $boardIds, int $limit = 6): arrayThe pinned topics of the given boards, most recently active first: the post-its of the flat view. Readable boards only - the caller passes them, as for createLatestQuery().
TopicRepository::findAnnouncements()
public function findAnnouncements(array $boardIds, int $limit = 2): arrayThe announcements of the flat view: the latest topics of the announcement boards (Category::$announcement - "Annonce", "Messages Officiels"), in the order they came out - a scheduled one takes its place at its hour, not at the hour it was written.
$boardIdsthe readable announcement boards
TopicRepository::createSearchQuery()
public function createSearchQuery(string $term, ?array $readableCategoryIds = null): QueryPlain title/content search: a LIKE over the topic title and its posts.
Small forums do not need an index; when the site wants Typesense, the Topic entity is where a mapping goes. Readable boards only, as createTagQuery().
TopicRepository::findOneBySlug()
public function findOneBySlug(string $slug): ?TopicTopicRepository::incrementViews()
public function incrementViews(Topic $topic): voidA view is not an edit: bump the counter with a direct UPDATE so the Thread's updatedAt (Timestamp on update) is not touched by readers.
TopicRepository::countPerCategory()
public function countPerCategory(): arrayPer-board counters for the index page: topics and replies, in one query. Keyed by category id.
TopicRepository::findHotCategoryIds()
public function findHotCategoryIds(int $threshold): arrayThe boards holding at least one popular topic, for the folder badge of a board row. One grouped query for the whole index rather than a count per row, and the very threshold a topic row uses, so "populaire" means the same thing at both levels of the hierarchy.
- Returns
keyed by category id, to be read with hot[board.id]|default(false) in Twig
TopicRepository::findLastPerCategory()
public function findLastPerCategory(): arrayThe most recently active topic of each board, for the classic index's "last message" column (who wrote last, when, in which topic), as phpBB's forum list had it. One query: a topic whose last activity is its board's latest. Keyed by category id.
TopicRepository::findTagUsage()
public function findTagUsage(int $limit = 30): arrayTags in use on the forum - the tag cloud. Rows of ['tag' => Tag, 'nb' => int]. Queried from the Tag side: DQL will not select a joined entity without its root alias, and the count is what the join is for.
Highest Tag::$priority first, as the admin set it on /admin/bbs/ordre; usage only decides between tags of equal priority - which, until an admin orders them, is all of them (priority 0).
Base\Forum\Security
ForumVoter class
final class ForumVoter extends Voter ForumVoter.php:39
Who may do what on the forum.
FORUM_READ a category (its required role, if any) FORUM_POST a category: signed in, board not locked, readable; an announcement board takes topics from the admin role only FORUM_REPLY a topic: signed in, topic and board not locked FORUM_EDIT a topic or post: its author, or its board's moderator FORUM_MODERATE a category, topic or post: whoever moderates its board (see moderates()); with no subject, whoever moderates any board at all FORUM_SCHEDULE a category: whoever may post there and moderates it - writing a topic to come out at a later hour (Topic::schedule()); with no subject, whoever moderates any board, to know whether to offer it at all
A scheduled topic that is not out yet (Topic::isUpcoming()) is its authors' and its board's moderators' alone: everyone else is refused everything on it and on its posts, READ included, and nobody replies to it before its hour. FORUM_ADMIN opening a board or a group from the site: the configured admin role (forum.admin_role)
const READ = 'FORUM_READ'const POST = 'FORUM_POST'const REPLY = 'FORUM_REPLY'const EDIT = 'FORUM_EDIT'const MODERATE = 'FORUM_MODERATE'const ADMIN = 'FORUM_ADMIN'const SCHEDULE = 'FORUM_SCHEDULE'
ForumVoter::__construct()
public function __construct(readonly Security $security, readonly RoleHierarchyInterface $roleHierarchy, readonly string $moderatorRole = 'ROLE_ADMIN', readonly string $adminRole = 'ROLE_SUPER_ADMIN')ForumVoter::moderates()
public function moderates(array $roles, ?Category $category): boolWhether holders of these roles moderate this board - or, with no board, any board at all.
The admin role moderates everything. Below it, the moderator role moderates the open boards and the boards kept for a role it stands ABOVE, never one kept for its own rank: the moderators read their own board but the admins keep order there, while they do keep it on the animators' and helpers'. A board kept for a role outside their branch they cannot even read. Announcement boards are the admins' alone.
Base\Forum\Service
MarkdownRenderer class
class MarkdownRenderer MarkdownRenderer.php:25
Posts are Markdown, rendered here with GitHub flavouring (tables, task lists, strikethrough, autolinks) and NO raw HTML: anything a member types as a tag is escaped, links are never allowed to be javascript:, and nesting is capped so a pathological post cannot stall the renderer.
Two forum habits are added on top of CommonMark:
MarkdownRenderer::__construct()
public function __construct(readonly ?RouterInterface $router = null, readonly ?TranslatorInterface $translator = null)MarkdownRenderer::render()
public function render(?string $markdown): stringMarkdownRenderer::excerpt()
public function excerpt(?string $markdown, int $length = 160): stringA one-line, tag-free excerpt for lists and previews.
MarkdownRenderer::quote()
public function quote(?string $markdown, ?string $author = null): stringQuote a post the way a reply box pre-fills it.
Base\Forum\Twig
ForumTwigExtension class
final class ForumTwigExtension extends AbstractExtension ForumTwigExtension.php:13
ForumTwigExtension::__construct()
public function __construct(readonly MarkdownRenderer $markdown, readonly int $hotThreshold = 25, readonly int $activeHours = 72)ForumTwigExtension::getFilters()
public function getFilters(): arrayForumTwigExtension::getFunctions()
public function getFunctions(): arrayForumTwigExtension::isHot()
public function isHot(Topic|int|null $subject): boolphpBB's "popular" flag: a topic is hot from hot_threshold replies on.
The templates ask for it instead of carrying the number, so a topic row, a board row and the legend all mean the same thing.
ForumTwigExtension::getHotThreshold()
public function getHotThreshold(): intForumTwigExtension::isActive()
public function isActive(Topic|\DateTimeInterface|null $subject): boolAwake or asleep: a message within forum.active_hours. The folder art says it - the "zzz" face sleeps, the one with a speech bubble talks.
Recent activity, not "unread": base-bundle keeps no per-member read marker, and a badge that meant "unread" could only ever be false.
ForumTwigExtension::topicFolder()
public function topicFolder(Topic $topic): arrayA topic row's badge, phpBB's precedence top down: an announcement (a topic of an announcement board), a post-it, a locked topic, a popular one, then the plain folder - the last three awake or asleep.
- Returns
title is a translation key, or ''
ForumTwigExtension::boardFolder()
public function boardFolder(Category $board, ?Topic $last = null, bool $hot = false): arrayA board's badge: an announcement board, a locked one, one holding a popular topic, then the plain folder - the last three awake or asleep after the board's latest message. A board reserved to a rank wears the ordinary badge; the tooltip still says it is reserved.