HasLifecycle adds query scopes for the state column and for the package’s records and schedules:
Listing::query()->whereState(ListingStatus::Active)->get();
Listing::query()->whereState([ListingStatus::Active, ListingStatus::Closed])->get();
Listing::query()->whereNotState(ListingStatus::Archived)->get();
Listing::query()->whereExpiringWithin('3 days')->get(); // expires after now, within 3 days
Listing::query()->whereExpired()->get(); // expiry passed (in grace or not yet swept)
Listing::query()->whereNotExpired()->get();
Listing::query()->whereInGrace()->get();
Listing::query()->whereFrozen()->get();
Listing::query()->whereInStateFor('14 days')->get(); // entered the current state at least 14 days ago
Listing::query()->withLifecycle()->paginate(); // eager-load records, open schedules and latest history rows
Order::query()->whereState(PaymentStatus::Captured, 'payment_status')->get();| Scope | Selects |
|---|---|
whereState($states, ?$lifecycle) | Rows in one of the states (a state or an array). |
whereNotState($states) | The complement. |
whereExpired() | The expiry has passed (in grace, or not yet swept). |
whereNotExpired() | No passed pending expiry. |
whereExpiringWithin('3 days') | Expires after now, within the interval. |
whereInGrace() | Expired, but the expiry transition is not due yet. |
whereFrozen() | Frozen right now. |
whereInStateFor('14 days') | Entered the current state at least that long ago. |
withLifecycle() | Eager-loads the state records, open schedules and latest history rows. |
States are checked against the definition; an undeclared state throws UnknownStateException. Each scope takes an optional lifecycle name (default: the model’s first lifecycle). whereState() and whereNotState() filter the model’s own column, which is the source of truth; the expiry, freeze and dwell scopes are EXISTS subqueries on the package tables that combine with your other scopes and with SoftDeletes.
Avoiding N+1
Handle reads (enteredAt(), version(), isFrozen(), expiresAt(), scheduled(), lastTransition(), …) use the eager-loaded relations when present and query otherwise. LifecycleResource also accepts a model directly, so a list reads the state, freeze, expiry and last transition without a query per model:
// A list: a model stands for its primary lifecycle, with the actor from auth
return LifecycleResource::collection(Listing::query()->withLifecycle()->get());allowedTransitions() still runs the guard pipeline per subject, so guards, Gate policies and quota counts may run their own queries. Index your lifecycle column together with quota scope columns.
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.