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

Filing reports

The fluent facade is the recommended entry point. Reports::report() starts a PendingReport about a subject; create() validates and stores it:

use RoundlyConsulting\Reports\Enums\Reason;
use RoundlyConsulting\Reports\Facades\Reports;

$report = Reports::report($post)
    ->by($user)
    ->for(Reason::Abuse)            // typed, validated reason
    ->because('This post violates the community guidelines.')
    ->create();

You can also start from the reporter, or pass a custom reason slug:

// 'copyright' must be listed in config('reports.reasons')
Reports::from($user)->about($post)->for('copyright')->create();

Or hand over a DTO in one call:

use RoundlyConsulting\Reports\DataTransferObjects\CreateReportData;
use RoundlyConsulting\Reports\Enums\Reason;
use RoundlyConsulting\Reports\Facades\Reports;

Reports::create(new CreateReportData(subject: $post, reason: Reason::Spam, reporter: $user));

From the reporter trait

The trait methods go through the manager, so Reports::fake() records them too:

// Description and reason are optional — the reason falls back to default_reason.
$report = $user->giveReportTo($post);
$report = $user->giveReportTo($post, 'Violates the guidelines.', Reason::Abuse);

// Or start a fluent build from the reporter:
$user->report($post)->for(Reason::Spam)->because('Spammy.')->create();

The PendingReport builder

  • about($subject) — the reported model. Reports::report() sets it for you.
  • by($reporter) — the reporting model. Clears any guest identifier.
  • asGuest($identifier) — a guest dedupe key instead of a reporter. Clears the reporter.
  • for($reason) / reason($reason) — a Reason case or a slug string; reason() is an alias.
  • because($description) — the reporter’s free-text description.
  • create() — validates the reason, enforces duplicate prevention, stores the report as Pending and returns it.

Without a reason, create() uses default_reason. Without a subject it throws a LogicException.

In a controller

A typical report endpoint validates against the allowed slugs — custom ones included — and turns a duplicate into a friendly message:

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use RoundlyConsulting\Reports\Exceptions\DuplicateReportException;
use RoundlyConsulting\Reports\Facades\Reports;

public function store(Request $request, Post $post): RedirectResponse
{
    $data = $request->validate([
        'reason' => ['required', Rule::in(array_keys(Reports::reasons()))],
        'description' => ['nullable', 'string', 'max:2000'],
    ]);

    $pending = Reports::report($post)->by($request->user())->for($data['reason']);

    if (filled($data['description'] ?? null)) {
        $pending->because($data['description']);
    }

    try {
        $pending->create();
    } catch (DuplicateReportException) {
        return back()->with('status', 'You have already reported this post.');
    }

    return back()->with('status', 'Thanks, a moderator will review it.');
}

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.