fof/best-answer
Flarum extension for Q&A-style discussions. Let authors or staff mark a reply as the best answer, highlight it prominently in the thread, and help users find solutions faster. Includes permissions and notifications for selected answers.
Context first: this is a Flarum 1.x extension, not a Laravel package — there's no service provider you register in config/app.php and no Artisan command of its own. The good news is that Flarum runs on the Illuminate components you already know (illuminate/database, illuminate/events, illuminate/validation, Eloquent models with saved/deleted events), so the mental model maps cleanly:
extend.php ≈ your service provider / registration filesrc/Listeners/ ≈ event subscribersmigrations/ ≈ normal Laravel migrationsjs/ is a separate webpack build (flarum-webpack-config), not Vite/MixInstall:
composer require fof/best-answer
php flarum cache:clear # or "Clear cache" in admin
Then enable it in Admin → Extensions, and immediately configure Admin → Permissions for the group(s) that should be able to select an answer (moderators, discussion authors, or both). The most common "it's installed but nothing happens" is simply that neither the group permission nor the extension setting allowing the discussion author to pick was enabled.
Where to look first, in order:
README.md — feature list and the setting toggles.migrations/ — the schema is small: a best_answer_post_id column on discussions and an is_best_answer flag on posts. Confirm the exact names here before writing any of your own queries.src/ — serializers (what the API exposes), listeners (what fires when an answer is set), and the notification class.js/src/ — the "Best Answer" action in the post footer and the badge rendering. This is where you'll extend UI.First use case: a support/Q&A forum. A user asks a question, a responder replies, and the OP (or a moderator) clicks Best Answer on the reply. The post is highlighted, surfaced near the top of the discussion, indicated in the discussion list, and the responder gets a notification. If you're building a knowledge base or an internal help desk on Flarum, this is the entire "accepted answer" primitive, and you should decide on day one who is allowed to mark it.
1. Reading the data in your own code. Because the relationship lives on the standard Discussion model, you can query it like any Eloquent relation:
use Flarum\Discussion\Discussion;
$answered = Discussion::whereNotNull('best_answer_post_id')
->with('bestAnswerPost')
->get();
For time-to-answer metrics (the most useful thing you'll build on top of this package), go straight to SQL:
SELECT d.id,
d.title,
p.user_id AS answerer_id,
TIMESTAMPDIFF(HOUR, d.start_time, p.created_at) AS hours_to_answer
FROM discussions d
JOIN posts p ON p.id = d.best_answer_post_id
WHERE d.best_answer_post_id IS NOT NULL;
That gives you a resolution-rate dashboard ("% of questions in tag X that received a best answer in the last 30 days") and lets you target nagging/pinging campaigns at unanswered threads.
2. Headless / API consumers. The discussion resource exposes the best-answer pointer and the post resource exposes the flag, so a docs-site generator or custom frontend can do:
GET /api/discussions?filter[q]=is:unanswered&include=bestAnswerPost,user
GET /api/posts?filter[discussion]=123&sort=-isBestAnswer
Include bestAnswerPost explicitly — Flarum does not ship it by default in every response. If you need extra fields on the discussion (e.g. bestAnswerSetAt), add them with Extend\ApiSerializer on DiscussionSerializer rather than post-processing JSON in the client.
3. Reacting to an answer being set. Two approaches, in order of preference:
src/Listeners/ (check there first — it's the cleanest hook and gives you the actor and post).Illuminate\Database\Eloquent\Model. Register in your own extension's extend.php / boot:Discussion::saved(function (Discussion $discussion) {
if ($discussion->wasChanged('best_answer_post_id') && $discussion->best_answer_post_id) {
// sync to Slack, index in Meilisearch, close a Jira ticket…
}
});
wasChanged() is the key — a plain saved listener fires on every rename and tag change too.
4. Permission policy refinement. Group-level permissions in Flarum are global, not per-tag. If you want "best answers allowed only in the support tag," don't hack the JS; register a model policy for Discussion in your own extension and gate the ability on the discussion's tags:
(new Extend\Policy())
->modelPolicy(Discussion::class, CustomDiscussionPolicy::class),
Then keep the package's own group permission as the coarse filter and let your policy add the tag/tier logic.
5. Frontend customisation. Standard Flarum extender patterns work:
import { extend } from 'flarum/common/extend';
import Post from 'flarum/forum/components/Post';
extend(Post.prototype, 'actionItems', function (items) {
if (this.attrs.post.isBestAnswer()) {
// inject a "copy permalink to answer" item, etc.
}
});
Style overrides go in your own less/forum.less — target the best-answer class on the post/discussion-list item (inspect the rendered DOM rather than guessing; the class names are stable but not documented).
6. Notifications. The extension notifies the post author. If you're using this in an internal tool, check the notification is actually enabled for your user groups, and remember notifications are queued — if your queue worker is down, "it worked but nobody was told" is a queue problem, not an extension bug.
best_answer_post_id column. If the marked post is deleted (by a user, by moderation, or by a GDPR-style scrub), verify the column is cleared; if not, add a listener for Flarum\Post\Event\Deleted in your own extension that nulls it. This is the #1 thing to check before going live.composer update of this or any extension: php flarum cache:clear. If you edited anything under js/, you must rebuild (cd js && npm ci && npm run build, npm run dev while iterating) — a browser hard-refresh will not pick up unbuilt changes, and this is the usual "my edit did nothing."src/ routes/listeners. The package is small enough to read end to end in ten minutes — do that instead of reverse-engineering.php flarum info (version/compat sanity) → storage/logs/flarum-*.log (PHP side) → browser console + network (JS side) → permissions screen (the silent killer). Nine times out of ten it's the permission toggle.composer.json, read release notes before bumping, and be prepared to fork — the codebase is small enough that a fork is a day's work, not a quarter's. (The published last-release date looks odd/future-dated; sanity-check the actual git tags rather than trusting the metadata.)best_answer_post_id; don't assume the package ships test scaffolding you can reuse.How can I help you explore Laravel packages today?