Cancel & expire
Requests::cancel($request); // status = Cancelled
Requests::expire($request); // status = Expiredcancel() fires RequestStatusChanged and RequestCancelled; expire() fires RequestStatusChanged and RequestExpired.
Both also close the request’s open approval round, so a late decision can’t resolve it and pull the request back: through Requests it throws RequestAlreadyResolved, straight through the approvals engine ($alice->approve($request)) the engine’s ClosedApprovalRequestException. The round is closed through the engine’s own close: approvers still asked for a decision are released (ApprovalCancelled and ApprovalStatusChanged for each), then the round is closed as cancelled or expired and announced with ApprovalRequestResolved. A round already past its own expiry closes as expired. A cancelled request is final; an expired one can only be reopened — cancelling it throws RequestAlreadyResolved.
Deadlines
Stamp an expiry on creation — per request with expiresAt(), or globally with default_ttl. default_ttl is a positive whole number of minutes (a numeric string from env() works too); not set (null or blank) means no default expiry, and anything else throws InvalidConfigurationException:
// Per request:
Requests::make()->title('Leave request')->expiresAt(now()->addDays(7))->create();
// Globally — config/requests.php, in minutes (integer):
'default_ttl' => 60 * 24 * 14, // 14 days unless expiresAt() is givenA deadline alone doesn’t change the status. Until requests:expire or expire() runs, a past-due request stays New, and isExpired() returns true — but it can no longer be decided: approve() or reject() expires it on the spot and throws RequestAlreadyResolved (Resolving requests). The approval round carries the same deadline, so the engine stops taking decisions at the same moment:
$request->isExpired(); // true when still New and past expires_atExpiring from code
Requests::expireDue() expires every open, past-due request and returns how many this call expired. Each row is re-checked under its lock when its turn comes: one decided, cancelled, given a new deadline or expired by an overlapping sweep since its chunk was read is skipped and not counted, so overlapping sweeps announce each request once:
use RoundlyConsulting\Requests\Facades\Requests;
Requests::expireDue(); // int — how many requests were expired
Requests::expireDue(dryRun: true); // int — how many are due; nothing changes
Requests::expireDue(chunk: 1000); // rows processed per batchExpiring on a schedule
The requests:expire command is a thin wrapper over Requests::expireDue() (see Artisan commands). Schedule it:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('requests:expire')->hourly();Only open (New) requests past their expires_at are expired, and their approval rounds are closed with them; approved and rejected requests are untouched. The command also lapses the approval decisions and rounds on requests whose own expiry has passed — scoped to the request model, so the rest of your app’s approvals are left to their own sweep. Requests::expireDue() expires requests only, so make that scoped call yourself when you sweep from code:
use RoundlyConsulting\Approvals\Facades\Approvals;
use RoundlyConsulting\Requests\Facades\Requests;
use RoundlyConsulting\Requests\Models\Request;
Requests::expireDue(); // expire past-due requests
Approvals::expire(subjectType: Request::class); // lapse their decisions and rounds — requests onlyShow 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.