filagora/filagora-forums
Filagora Forums is a Laravel/PHP package for adding a complete discussion forum to your app. It includes categories, topics, replies, moderation, role-based permissions, configurable models, migrations, and a clean Blade UI, making forum setup and customization straightforward.
Install filagora/filagora v0.6.0. It pulls filagora-core ^0.2.2, which is a floor, not a nicety — attachments and the adoption tooling rely on it. Run php artisan migrate: the package now ships eight migrations across twenty-one tables, and filagora:adopt expects exactly that shape. Existing sites should re-run filagora:adopt against their database before trusting the new tables; if you maintain a profile package that adds importer columns or composite indexes, declare them through core 0.2.2's drop_indexes rather than hand-editing.
Post a thread, reply, and browse once so you have realistic data. Then, in this order:
ReadTracking — the highest-value, lowest-friction addition. Wire unreadThreads() into a "what's new" page first; it handles baseline, lookback and per-viewer read times in one call.Mentions — enable via mentions.enabled, confirm UserMentionedNotification sends (mentions.mail.enabled), then decide whether the default named route mentions.routes.thread is good enough or should point at your own thread route.Attachments — the biggest lift. Needs intervention/image, a storage disk you are happy with, forums.orphaned_attachments retention configured, filagora:forums:prune-attachments scheduled, and a decision about authorizeUsing() if other packages' files live alongside post attachments.composer require filagora/filagora-search, publish config, php artisan filagora:search:backfill, then look at config/filagora-search.php (search.post_text_limit, search.fulltext), ForumSearch, ForumSearchCriteria, and the two engines ThreadSearchIndex / ThreadDatabaseSearch.First use case: give users a "what's new" list with ReadTracking::unreadThreads(). Every forum eventually grows one, and the package handles the three things hand-rolled versions get wrong: per-user baseline (from the user column named by read_tracking.baseline_attribute), a bounded lookback window (read_tracking.unread_max_lookback_days), and read checks that are subqueries so they compose with whatever thread query you already have.
Read tracking belongs at the edges of your controllers, not in your models. Mark read on the thread show action, mark forums read on the forum list action, and use unreadAmong() when hydrating a page of threads rather than looping isUnread() per row — it exists precisely to avoid N+1. unreadThreads() returns a bounded "what's new" list; treat it as a page source, not an unbounded feed. A hand-made unread flag beats every read time, so if your UI exposes "mark unread", respect that the package will keep it unread even after the user later opens the thread until your code clears the flag.
Mentions are listeners, not a service call you make. Turn on mentions.enabled and the package hooks new, edited, approved and restored posts, plus approved threads. The Mentions service does extraction (skipping code fences and quoted blocks, never paging the author, deduping so nobody is told twice, only paging people who can actually see the thread, capped per post by mentions.max_per_post). Your job is the edge: listen for UsersMentioned if you want to add push/webhook/in-app notification on top of the default email, and decide whether mentions.mail.enabled should be on in your environment.
Treat mentions as a per-post budget, not a per-edit budget. The cap is counted across edits with the post row locked, so re-saving a post does not give the author a fresh allowance. If you expose a "preview mentions" UI, count against the same limit or users will be confused when the real post drops some.
Attachments are two-step: upload inline, claim on submit. The Attachments service supports uploading during composition, then claiming the upload to a post when it saves — unattached uploads are the ones the prune command cleans up. Serve files through the package controller (safe headers, download counter, access that follows the post's visibility) rather than linking to your storage disk directly; if you do bypass it, you re-implement visibility and you will get it wrong. SVGs are deliberately never rendered inline in the page — they are download-only.
Register authorizeUsing() if you have other file-bearing packages. It is the documented hook for "this person may access this file given this post" when ownership is not simply "post attached to". Use it instead of subclassing the service.
Schedule the prune command. filagora:forums:prune-attachments implements the forums.orphaned_attachments retention rule. Without a schedule, failed uploads and abandoned drafts accumulate forever. Run it daily in a low-traffic window.
Keep the search patterns from v0.5.0. Build a ForumSearchCriteria from the request, pass the real viewer (never null), reach for unapproved() for the moderation queue and threads() for the public path, and use posts() when you need to show which reply matched. Bind ForumSearch to ThreadDatabaseSearch in tests so CI needs no search service; you can also fall back to it in production knowingly. Re-run backfill when you add filter fields.
Privacy export and erasure now cover all three new modules. Uploads, mention records and read markers are all included in export and removed on erasure. If you have a custom export/erasure pipeline, verify it still runs end-to-end with the new tables — the package's hooks are the canonical path.
filagora:adopt expectations changed. Twenty-one tables, eight migrations. On an existing install, re-run adopt after migrating; if it complains about tables it does not recognise, that is almost certainly a profile package that has not caught up. Declare importer columns and composite indexes in the profile package via core 0.2.2's drop_indexes.unreadThreads() won't surface something read (or created) outside read_tracking.unread_max_lookback_days. That is a feature — "what's new" should not return year-old threads — but users will file it as a bug. If you need full history, query threads directly and use unreadAmong() on the page.read_tracking.baseline_attribute names the column (typically something like a registration or "tracking started" date) used as the starting point when a user has no read rows. Point it at the wrong column and new users appear to have read everything (or nothing).@name inside a fenced block didn't notify. Do not "fix" it by loosening extraction; you will page people on pasted log snippets.UsersMentioned yourself if that is genuinely what you want.AttachmentRefused is the one exception to catch. It is thrown by the hourly per-person limit (and similar refusal cases). Surface it as a validation-style message, not a 500.image/svg+xml from a domain that shares cookies with your app.forums.orphaned_attachments is your lever, and intervention/image thumbnails are the other storage cost.intervention/image install with the right GD/Imagick driver. A broken driver usually surfaces as silent missing thumbnails rather than an exception.search.post_text_limit truncates post text in thread documents (raise it and re-backfill); unapproved content is invisible to threads() by design; deactivated forums drop out of search without deleting anything; search.fulltext must match across environments or MariaDB dev results will diverge from staging.ForumSearch, ReadTracking, Attachments, Mentions), then dump the criteria/marker/upload object your controller built, then check the underlying rows (ThreadSearchIndex, thread_reads, post_attachments, post_mentions). Recent "it's broken" reports split roughly: read tracking is usually a GET-vs-POST or baseline issue; mentions is almost always a duplicate-suppression or visibility expectation; attachments is usually a missing claim, a disk misconfiguration or a schedule that isn't running; search is a criteria/viewer problem, not an indexing one.ThreadSearchIndex (document contents), SearchRelations (what triggers a resync), ForumSearchCriteria (new filterable dimensions), Attachments::authorizeUsing() (cross-package access), and the UsersMentioned event (extra notification channels). Prefer these over subclassing the façade services (ForumSearch, Attachments, Mentions, ReadTracking), which are the thin, least-stable layer.mentions.*, read_tracking.*, forums.orphaned_attachments) are read at runtime but cached with the rest of your config. php artisan config:cache after editing, or the values will look ignored.How can I help you explore Laravel packages today?