---
title: "DI, actions & DTOs — Reports for Laravel | Roundly"
description: "Inject ReportsManager instead of calling the facade, or run one of six Action classes with readonly DTOs from jobs and services."
url: https://roundly-consulting.com/open-source/docs/reports-for-laravel/actions-dtos
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Reports for Laravel](https://roundly-consulting.com/open-source/docs/reports-for-laravel.md)

# DI, actions & DTOs

The facade is the recommended default, not the only way in. Three entry points run the same code:

- The Reports facade — the shortest form.
- The manager, injected through the constructor — the same API with an explicit dependency and no static calls. RoundlyConsulting\\Reports\\ReportsManager is the facade root, non-final so the fake can extend it.
- Actions — single-purpose classes with execute(), resolvable from the container, for composing into your own jobs, services and commands.

## Injecting the manager

Reports::fake() swaps the manager in the container too, so injected managers land on the fake:

```php
use RoundlyConsulting\Reports\ReportsManager;

final class ReportPost
{
    public function __construct(private ReportsManager $reports) {}

    public function __invoke(User $user, Post $post): Report
    {
        return $this->reports->report($post)->by($user)->for('spam')->create();
    }
}

// The whole facade API is on the manager:
$this->reports->review($report);
```

## Running an action

```php
use RoundlyConsulting\Approvals\Enums\ApprovalRule;
use RoundlyConsulting\Reports\Actions\ChangeReportStatusAction;
use RoundlyConsulting\Reports\Actions\CreateReportAction;
use RoundlyConsulting\Reports\Actions\OpenModerationAction;
use RoundlyConsulting\Reports\Actions\PruneReportsAction;
use RoundlyConsulting\Reports\Actions\RejectReportAction;
use RoundlyConsulting\Reports\Actions\ResolveReportAction;
use RoundlyConsulting\Reports\DataTransferObjects\CreateReportData;
use RoundlyConsulting\Reports\DataTransferObjects\ResolveReportData;
use RoundlyConsulting\Reports\Enums\Reason;
use RoundlyConsulting\Reports\Enums\Status;

$report = app(CreateReportAction::class)->execute(new CreateReportData(
    subject: $post,
    reason: Reason::Spam,        // Reason|string, normalised to a slug
    reporter: $user,             // optional
    description: 'Looks like spam.',
    guestIdentifier: null,       // optional dedupe key for anonymous reports
));

app(ChangeReportStatusAction::class)->execute($report, Status::InReview);

app(ResolveReportAction::class)
    ->execute($report, new ResolveReportData(resolver: $moderator, note: 'Handled.'));

// or

app(RejectReportAction::class)
    ->execute($report, new ResolveReportData(resolver: $moderator, note: 'No violation.'));

app(OpenModerationAction::class)->execute($report, [$alice, $bob], ApprovalRule::Quorum, 1); // ApprovalRequest

app(PruneReportsAction::class)->execute(days: 90, force: false); // int
```

## Facade method → action

| Facade / manager method | Action |
| --- | --- |
| `report($subject) / from($reporter)` | `CreateReportAction (via ->create())` |
| `create(CreateReportData $data)` | `CreateReportAction` |
| `moderate($report)` | `OpenModerationAction (via ->open())` |
| `resolve($report, ?$by, ?$note)` | `ResolveReportAction` |
| `reject($report, ?$by, ?$note)` | `RejectReportAction` |
| `changeStatus($report, Status $status)` | `ChangeReportStatusAction` |
| `review($report) / close($report)` | `ChangeReportStatusAction` |
| `prune(?int $days = null, bool $force = false)` | `PruneReportsAction` |

- CreateReportAction — refuses an unsaved subject or reporter with a LogicException, validates the reason, enforces duplicate prevention (filings against one subject are serialized), stores the report as Pending and checks the threshold.
- ResolveReportAction / RejectReportAction — settle the report in one locked step checked against the stored status; while a moderation request is open, record a named moderator’s decision instead and refuse anyone else with ModeratorRequiredException.
- ChangeReportStatusAction — owns every status move: a no-op on the same status, InvalidStatusTransitionException under strict\_transitions, ModeratorRequiredException for a move out of the open statuses while a moderation request is open, otherwise the update and ReportStatusChanged.
- OpenModerationAction — throws MissingModeratorsException without moderators and, under the report’s row lock, ModerationNotAllowedException for a settled report or one already under moderation; otherwise opens the approvals request.
- PruneReportsAction — soft-deletes, or with force permanently deletes, Resolved, Rejected and Closed reports created before the cutoff.

The reason reads have no action behind them. ReportsManager::openModeration() — the terminal of moderate()->open() — is @internal. Calling an action directly skips the manager, so Reports::fake() does not record it.

## DTOs

```php
final readonly class CreateReportData
{
    public string $reason; // normalised from Reason|string

    public function __construct(
        public Model $subject,
        Reason|string $reason,
        public ?Model $reporter = null,
        public ?string $description = null,
        public ?string $guestIdentifier = null,
    ) {}
}

final readonly class ResolveReportData
{
    public function __construct(
        public ?Model $resolver = null,
        public ?string $note = null,
    ) {}
}
```

CreateReportData normalises a Reason case to its slug, so $data->reason is always a string.

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/reports-for-laravel.md)
