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

DI, actions & the DTO

There are three equivalent entry points, and all of them run the same code:

  • The Requests facade — the shortest form and the recommended default.
  • The manager, RoundlyConsulting\Requests\RequestManager — the facade root. Inject it through the constructor for the same API as an explicit dependency, with no static calls. It resolves every action from the container, so your overrides apply.
  • Actions in RoundlyConsulting\Requests\Actions — single-purpose classes with execute(), for composing into your own actions, jobs and commands.

Injecting the manager

use RoundlyConsulting\Requests\Models\Request;
use RoundlyConsulting\Requests\RequestManager;

final class SubmitClaim
{
    public function __construct(private RequestManager $requests) {}

    public function __invoke(User $user): Request
    {
        return $this->requests->make()->author($user)->title('Expense reimbursement')->create();
    }
}

Requests::fake() swaps in a subtype of RequestManager behind the facade and in the container, so a constructor-injected manager lands on the fake too.

Calling an action

CreateRequest takes a CreateRequestDto and dispatches RequestCreated:

use RoundlyConsulting\Requests\Actions\CreateRequest;
use RoundlyConsulting\Requests\DataTransferObjects\CreateRequestDto;
use RoundlyConsulting\Requests\Enums\Status;

$request = app(CreateRequest::class)->execute(new CreateRequestDto(
    status: Status::New,
    author: $user,
    title: 'Expense reimbursement',
    meta: collect(['ip' => request()->ip()]),
    approvers: [$alice, $bob],
    expiresAt: now()->addDays(7),
));

Facade method → action

Facade / manager methodAction
make()->create(), create(CreateRequestDto)CreateRequest
approve(), reject(), reopen()ResolveRequest
cancel($request)CancelRequest
expire($request)ExpireRequest
expireDue(bool $dryRun = false, int $chunk = 500)ExpireDueRequests
canTransition($request, Status $to)— (reads the lifecycle graph)

Actions called directly bypass the manager, so Requests::fake() does not record them.

Every DTO field

CreateRequestDto is a readonly class with named, defaulted constructor arguments:

new CreateRequestDto(
    status: Status::New,               // Status
    author: null,                      // ?Model
    type: null,                        // ?string
    title: null,                       // ?string
    description: null,                 // ?string
    meta: null,                        // ?Collection
    approvers: [],                     // list<Model> — the declared approvers (saved models)
    expiresAt: null,                   // ?CarbonInterface
    rule: ApprovalRule::Unanimous,     // ApprovalRule
    quorum: null,                      // ?int
    stages: [],                        // list<StageDefinition> — an ad-hoc pipeline
    workflow: null,                    // ?string — a named preset
    stageApprovers: [],                // list<list<Model>> — groups for a staged preset
    rejectOnStageRejection: true,      // bool
);

Which approval flow opens

CreateRequest opens at most one approval request, in this order of precedence:

  • workflow — a named preset wins; it uses stageApprovers when given, otherwise the approvers.
  • stages — an explicit staged pipeline, honouring rejectOnStageRejection. Flat approvers passed alongside are handed on too, so the engine refuses the mix.
  • approvers — a flat approval round with the chosen rule and quorum, naming those approvers.
  • none — no approvers declared, so no approval round; the first decision resolves the request.

Approvers must be saved models: a bare id names no model type, so it could never be enforced, and CreateRequest refuses one in approvers or stageApprovers with InvalidApprover. The request and its approval round are written together — if the round can’t open (an unsaved approver, an unknown workflow preset), no request is left behind.

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.