Reports & moderation
The bundled Post implements Reportable through the HasPostReports concern, built on reports-for-laravel. The reporter is always passed explicitly:
use RoundlyConsulting\Reports\Enums\Reason;
use RoundlyConsulting\Reports\Facades\Reports;
Reports::report($post)
->by($user)
->for(Reason::Spam)
->because('Obvious spam.')
->create();
// Anonymous / guest report:
Reports::report($post)->asGuest(hash('sha256', $request->ip()))->for('spam')->create();Dedup, the reason allow-list and reason labels come from reports: a duplicate report throws DuplicateReportException, an unknown reason UnknownReportReasonException.
Moderation queue
Build a needs-review queue straight off the Post query:
use RoundlyConsulting\Reports\Enums\Status;
$post->hasBeenReported(); // bool
$post->isReportedBy($user); // bool
$post->reportsCount(Status::Pending); // int
$post->reports; // every report on the post
$post->pendingReports; // pending reports only
Post::query()->withReportCounts()->get(); // eager reports_count
Post::query()->mostReported()->paginate(); // by total report count
Post::query()->reportedMoreThan(5)->get(); // over a thresholdMulti-moderator sign-off
reports routes resolution through approvals-for-laravel, so a report can require several moderators to agree before it settles. Moderators are saved Eloquent models — typically your User with the approvals GivesApprovals trait, which adds its givenApprovals() relation:
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Approvals\Interfaces\GivesApprovalsInterface;
use RoundlyConsulting\Approvals\Traits\GivesApprovals;
class User extends Model implements GivesApprovalsInterface
{
use GivesApprovals;
}use RoundlyConsulting\Approvals\Enums\ApprovalRule;
Reports::moderate($report)->requiring([$alice, $bob])->rule(ApprovalRule::Quorum)->quorum(2)->open();
Reports::resolve($report, by: $alice); // 1 of 2 — stays open
Reports::resolve($report, by: $bob, note: 'Spam'); // quorum reached → Resolved → post archived
Reports::resolve($report, by: $mallory); // not named → ModeratorRequiredExceptionOnly the moderators the request names, or their delegates, can settle it: anyone else — and a resolve() with no actor — gets reports’ ModeratorRequiredException, nothing is recorded and the post stays as it is.
Auto-unpublish
When a report is upheld (ReportResolved) or a post crosses the global reports.threshold (ReportThresholdReached), the post is unpublished through its own lifecycle — $post->archive() or $post->unpublish(), so Posts::fake() records it — re-emitting PostArchived or PostDrafted. It touches a post that is published or scheduled: a scheduled post is taken off the schedule, so publishDue() never puts moderated content live. Drafts and archived posts are left alone, so repeats are harmless, and report subjects that are not the configured post model are ignored:
'moderation' => [
'on_resolved' => 'archive', // 'archive' | 'draft' | null (disable the upheld-report path); anything else throws
'auto_unpublish' => true, // auto-archive on ReportThresholdReached
],reports.threshold is null by default in reports-for-laravel, so the threshold path only fires once you set it. Set on_resolved to null and auto_unpublish to false to drive visibility yourself.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.