filagora / filagora-forums
Forums for Filagora: forum groups, forums, threads and posts, visibility and posting rules, and moderation, as a Filament plugin.
Requires
- php: ^8.4
- filagora/filagora-core: ^0.2.2
- filament/filament: ^5.0
Requires (Dev)
- filagora/filagora-entitlements: ^0.2.1
- filagora/filagora-search: ^0.1
- intervention/image: ^4.2
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
Suggests
- filagora/filagora-entitlements: Answers the access checks from entitlement grants and lets the forum screens pick entitlement keys. Without it, bind Filagora\Core\Contracts\AccessChecker.
- filagora/filagora-search: Adds the forum search (threads, filters, visibility) with a choice of engine: Meilisearch, or the database.
- intervention/image: Makes thumbnails of uploaded images. Without it files are stored without one.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-18 16:57:31 UTC
README
Forum groups, forums, threads and posts for Filagora, with visibility and posting rules,
moderation, reports and a staff queue, as a Laravel 13 / Filament 5 plugin. Requires
filagora/filagora-core. Who may do what is answered
by core's AccessChecker: install
filagora/filagora-entitlements and it is
answered from entitlement grants, or bind your own checker (roles, policies, a permissions
package) and the forum follows it. With neither, every staff screen denies everyone.
The package is at
0.1: the API may change between0.xminor versions. There is no frontend: it is models, services, events and staff screens, and you build the public pages.
What it does
-
The seven tables (
forum_groups,forums,threads,posts,post_reports,post_moderation_logs,thread_moderation_logs), matching the shape of the forum they were extracted from index for index, including the generatedthreads.last_activity_atcolumn, the FULLTEXT indexes on forum names, thread titles and post bodies, and the tuned listing indexes. They are built for forums with hundreds of thousands of threads. -
Models and enums (
Forum,ForumGroup,Thread,Post,PostReport,PostModerationLog,ThreadModerationLog; forum visibility, posting policy, moderation status, report status). Every stored value is kept, so existing data loads. Model classes can be swapped underfilagora.models.<key>. -
A schema snapshot so
filagora:adopt(from core) can start the package on a database that already holds these tables. A site's own extra columns are declared with a coreSchemaExtension. -
The access rules (
ForumAccess): who can view a forum, open a thread, post and reply, from the forum's visibility (public, registered, staff only, entitlement only, paid only, paid or author, staff or author) and posting policy, the viewer's entitlements, and whether they have authored in the forum. It also says which forums a viewer sees in full, which only their own threads in, and gives query scopes for visible threads and posts. -
Three staff abilities (
filagora.forums.moderate,.view_unapproved,.bypass_approval), each mapped to an entitlement key infilagora-forums.gates; holding any one makes a user staff. Remap them to your own keys in config. -
Two extension points: bind
PaidAccessCheckto say what "paid" means for forums that name no entitlement (the default grants nobody), andStaffPreviewif your frontend can switch staff privileges off to see the forum as a member does (the default is always on). -
Counters kept right. A forum holds
thread_count,post_countand last-post fields, and a thread holdsreply_countand last-post fields, so listings never count rows. They are updated incrementally as threads and posts change (a post counts when it and its thread are approved), andfilagora:forums:sync-statsandfilagora:forums:sync-thread-statsrebuild them from the data if they ever drift. AForumStatsChangedevent tells a frontend when to drop its caches. -
Posting.
Postingstarts threads, replies and edits posts, checkingForumAccessfirst and refusing withPostingNotPermittedand a reason (PostingDenial). A new thread creates its first post with it. Whether content waits for a moderator follows the forum's approval flags, the author'sfilagora.forums.bypass_approvalentitlement and yourContentModerationPolicy; slugs are made unique; an identical post within 15 seconds is returned rather than duplicated; counters are kept right. Authors may edit their own posts within a window set by theforum_post_edit_window_minutessetting (default 30), and an edit shows as "edited" afterforum_post_edit_grace_period_minutes(default 5). EventsThreadCreated,PostCreatedandPostEditedlet add-ons (subscriptions, mentions, notifications, attachments) hook in. -
Two more extension points:
ContentModerationPolicy(users whose content is always moderated, and a first post awaiting review) andPostSignatureResolver(the signature copied onto new posts). Both do nothing by default. -
Moderation. For moderators (
filagora.forums.moderate; anyone else getsModerationNotPermitted):ThreadModerationlocks, unlocks, pins, unpins, moves, renames, approves and permanently deletes threads;PostModerationsoft-deletes (singly or up to 200 at once), restores, approves, unapproves and rejects posts;PostRelocationmoves posts to another thread and promotes posts into a new one, singly or in batches, and removes a thread the move emptied. Each action writes a row to the thread or post moderation log, re-derives the counters, and dispatchesThreadModerated,PostModeratedorThreadDeleted, which is where you notify authors or ban them. Undo is left to the frontend. -
Reports.
PostReports::report()lets a member who can see an approved post report it once;resolve()closes every open report on the post at once asdismiss,keep,removeorremove_thread(the whole thread, if the post is all it holds) and dispatchesPostReportsResolvedwith the reporters' ids. -
Filament screens.
FilagoraForumsPluginadds forum groups and forums (create and edit), threads, posts and post reports (browse, filter, and act through the moderation services, so every action is logged and counted), a Moderation queue page (threads and replies waiting for approval, reported replies one row each) and aModerationQueueStatsdashboard widget. Every screen needs thefilagora.forums.moderateability. Register it on the panel next to the core and entitlements plugins:->plugins([ FilagoraCorePlugin::make(), FilagoraEntitlementsPlugin::make(), FilagoraForumsPlugin::make(), ])
The moderation queue's navigation badge and the widget count what is waiting; add
ModerationQueueStats::classto your dashboard if your panel lists widgets explicitly. -
Site announcements (banners). An editor publishes a message for a time window, to everyone, guests or signed-in users, with a severity, a priority, whether visitors can dismiss it and whether it keeps showing, and optional targeting rules (all must pass).
ActiveAnnouncements::forViewer($user, ['forum_id' => $id])returns what to show one viewer (the shared pool is cached by id for 60 seconds and cleared on every save);AnnouncementDismissalsrecords a dismissal (only for a dismissible announcement) or the first view of a non-persistent one, and lists a user's dismissed ids. The screen (Announcements, in the Forum group) needs its own ability,filagora.forums.announcements, which does not make anyone forum staff. The package has no routes, views or JavaScript: render the banners and add the two endpoints in your application, for exampleRoute::post('/announcements/{id}/dismiss', fn (Request $r, int $id, AnnouncementDismissals $d) => response()->json(['dismissed' => $d->dismiss($r->user(), $id)]))->middleware('auth');
Rules are classes registered on
TargetingRules; it shipsentitlement,username,date_is,time_between,never_posted,post_count_between,not_posted_daysandbrowsing_forum(which readsforum_idfrom the context you pass). ImplementTargetingRule(a type, a label, the form fields, andmatches()) andregister()it to add your own. The username and timezone columns the rules read are set infilagora-forums.announcements. The tables aresite_notificationsanduser_site_notification_dismissals. -
Polls.
Pollsadds a poll to a thread (moderators; one per thread; 1 to 20 questions of 2 to 20 options, single or multiple choice), lets them replace its questions until someone has voted, and close or reopen it.vote($user, $poll, [$questionId => [$optionId, ...]])takes a vote from anyone who can see the thread: every question answered, exactly one option for a single-choice question, at least one for a multiple-choice one, locked once cast.results($poll)gives each option's votes;canSeeResults()follows the rule that staff, voters and everyone (once the poll is closed) may see them. Refusals arePollRefusedwith a reason; the forms, the voting buttons and the results chart are yours. -
Reactions.
Reactions::toggle($user, $post, 'like')adds a reaction to a post the person can see, or takes it back if they already made it; deleted posts, posts awaiting approval and kinds not listed inreactions.typesare refused (ReactionRefused).counts($postIds)andmadeBy($user, $postIds)answer for a whole page of posts in one query each. -
Subscriptions and digests.
Subscriptions::subscribe($user, $thread)follows a thread (a new thread's author follows it by default; a reply's author follows it when their preference says so). A new approved reply becomes updates for the followers: those who want it at once are handed to theSubscribersToNotifyevent, those who chose daily or weekly get a row in the digest queue, and those who turned updates off get nothing.php artisan filagora:forums:send-digest daily|weekly(schedule it) sends the digests that are due throughThreadDigestReady. Emails go out by default; setsubscriptions.mail.enabledto false and listen to the two events to notify by bell or push instead. A person's choice of delivery (instant, daily, weekly, off) and whether posting follows a thread are read from two columns of your user model (subscriptions.user_attributes), or bind your ownSubscriptionPreferences. -
Attachments. Files on posts.
Attachments::storeInline($user, $file)stores a file while a post is being written (nothing owns it yet) andclaimInline($body, $user, $post)claims the ones the body refers to once the post is saved (the pattern that finds a file id in your URLs isattachments.inline_pattern);store($post, $user, $files)attaches files to a saved post. Images get a thumbnail whenintervention/imageis installed.canView($attachment, $viewer)follows the post: a file nobody has claimed is its uploader's alone; one on a post needs the forum, the thread and the post to be visible to the viewer (a post awaiting approval or deleted is visible to its author and staff).stream($attachment, thumbnail:, inline:)serves the file with safe headers (nosniff, a cleaned file name) and counts the download; images and documents are shown in the page only when asked, and SVG never is. A person may uploadattachments.max_per_hourfiles an hour (AttachmentRefusedsays why a file was refused). Uploads that were never claimed are removed afterattachments.orphan_hoursbyfilagora:forums:prune-attachmentsor core's retention run; files whose post was deleted stay with their thread. Other packages that attach files to something else add their own access rule withauthorizeUsing()(private messages do). The upload form, the routes and the controller are yours. -
Mentions.
@namein a post tells that person. New approved posts, edits, posts approved or restored later and the posts of a thread when it is approved all callMentions::sync($post)(switch the automatic part off withmentions.enabled). Names are matched ignoring case against the user columnmentions.user_attribute(username); code and the blocks the Quote button inserts (a> **Author:**line and the>lines after it) are not scanned, so quoting a post does not replay its mentions. The author is never told about their own post, nobody is told twice about the same post, only people who can see the thread are told, and at mostmentions.max_per_postpeople (10) are ever told about one post. Each batch is announced asUsersMentioned; by default each person is emailed (mentions.mail.enabledswitches that off;mentions.routes.threadnames your thread route for the link).Mentions::usernames($body)returns the names in a body, for rendering them as links. -
Read tracking.
ReadTracking::markThreadRead($user, $thread)when someone opens a thread,markThreadUnread()to flag it unread by hand (that flag beats every read time until they open it again),markForumsRead()andmarkAllRead()(every forum they can see),isUnread()andunreadAmong($user, $threads)for a page of threads in two queries. A thread is unread when its latest activity is later than the newest of the person's baseline, their read time for the thread and their read time for its forum.unreadThreads($user, $query)gives a "what's new" list for you to page: it looks back no further thanread_tracking.unread_max_lookback_days(an old baseline would otherwise match every thread) and takes any query, already narrowed to what the person may see. The baseline is the column of your user model named byread_tracking.baseline_attribute(forum_read_baseline_atby default; null for none). -
Search (with
filagora/filagora-searchinstalled). Threads become a search index.ForumSearch::threads($criteria, $viewer, $page)finds threads the viewer may see: by words in the title or in approved posts, and by forum, author (started, or started-or-posted), "mine", minimum replies and recent activity, sorted by last post, newest, replies or relevance. What the viewer may see is turned into a filter from their forum scope, so any engine applies it; results come back as ids, are loaded, and any row that is no longer visible (the index can be stale) is dropped. The engine is the search package's choice: Meilisearch, the database (the default, using MariaDB FULLTEXT on thread titles and post bodies), or your own.unapproved()(for staff who ask to include pending and rejected threads) andposts()(matches inside one thread) are database queries. Saving or deleting a thread, post or forum keeps the documents right. Parsing the request into aForumSearchCriteriaand the search pages are yours. -
Privacy. Forums registers a
PrivacyContributor(a person's threads, posts and reports for an export; erasure keeps what they wrote under the anonymised account) and retention rules for the moderation logs and dismissed reports (retentionin the config) with core. Withfilagora-complianceinstalled, privacy requests andfilagora:compliance:pruneuse them.
Install
composer require filagora/filagora-forums filagora/filagora-entitlements # entitlements is optional, see above php artisan migrate php artisan filagora:entitlements:sync # with entitlements: creates the forum entitlement definitions
Register the plugin on your Filament panel, after the core (and entitlements) plugins. Without
entitlements the keys on a forum (required_entitlement_scope, reply_entitlement_scope) and in an
announcement's entitlement rule are typed in, and mean whatever your checker makes of them:
->plugin(\Filagora\Forums\FilagoraForumsPlugin::make())
To start from a database that already holds these tables, run php artisan filagora:adopt
instead of migrate (see filagora/filagora-core).
Testing
composer test
Tests run against a real MariaDB database named testing.
Licence
MIT. See LICENSE.