NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Posts for Laravel

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 threshold

Multi-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 → ModeratorRequiredException

Only 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 crypto

By 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.