Set an expiry with expiresIn() (seconds) or expiringAt() (a Carbon instance) — on an ask, an approval or a whole request:
use RoundlyConsulting\Approvals\Facades\Approvals;
// A pending sign-off that lapses if legal has not decided within 48 hours.
Approvals::for($contract)->as($legal)->expiresIn(172800)->ask();
// An approval valid for a day.
Approvals::for($budget)->as($cfo)->expiresIn(86400)->approve();
// A request that must be decided within a week.
Approvals::request($budget)->from([$cfo, $ceo])->expiresIn(604800)->open();What expiry does
An approval, ask or request stops counting the moment its expiry passes. Reads such as isApproved(), hasApproved(), approvalCount() and a request’s tally ignore it, and a request past its expiry accepts no more decisions: the next one resolves it as expired and is refused with ClosedApprovalRequestException. An expired approval frees its actor to approve again.
Set approvals.expiry.default to give every approval a lifetime unless the decision sets its own. Answering an ask never inherits the ask’s deadline.
Recording the lapse
Approvals::expire() records what has lapsed: decisions move to expired (ApprovalExpired plus ApprovalStatusChanged), requests resolve as expired (ApprovalRequestResolved plus ApprovalStatusChanged), and it returns how many of both it lapsed. Given a subjectType — a model class or its morph alias — it lapses only decisions on that type of approvable and requests for that type of subject, so a package sweeping its own approvals leaves the rest of the app alone:
Approvals::expire(); // int — decisions and requests lapsed
Approvals::expire(subjectType: Budget::class); // only those on budgets (class or morph alias)
Approvals::expire(now()->addHour()); // lapse everything due within the next hourThe approvals:expire command
The app-wide sweep as an Artisan command — no arguments or options:
php artisan approvals:expire
# Expired 3 approval(s) and request(s).Schedule it so overdue decisions and requests are recorded as expired on their own:
use Illuminate\Support\Facades\Schedule;
Schedule::command('approvals:expire')->hourly();Querying by expiry
The Approval model’s inForce() scope keeps only decisions whose expiry has not passed; expiringBefore() finds the overdue ones:
use RoundlyConsulting\Approvals\Models\Approval;
// Approvals that still count — none past its expiry.
Approval::query()->approved()->inForce()->whereMorphedTo('approvable', $budget)->count();
// Overdue asks and approvals the next sweep will lapse.
Approval::query()->active()->expiringBefore(now())->get();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.