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 method | Action |
|---|---|
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 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.