The Lifecycles facade
Everything goes through one facade, RoundlyConsulting\Lifecycle\Facades\Lifecycles (also registered as the global alias Lifecycles). for($model) returns a handle on one lifecycle of one subject, model() works on a model class, definitions() and schedules() group the definition and schedule operations, and a few flat verbs run the package-wide jobs:
use RoundlyConsulting\Lifecycle\Facades\Lifecycles;
// A handle per subject (and lifecycle), carrying the call's context
Lifecycles::for($listing)->by($user)->because('Back in stock')->apply('reopen');
Lifecycles::for($order, 'payment_status')->apply('authorize');
// Ask before acting
Lifecycles::for($listing)->by($user)->can('close');
// Time: schedules, expiry and freezes
Lifecycles::for($listing)->by($editor)->schedule('publish', $publishAt);
Lifecycles::for($listing)->renew();
Lifecycles::for($listing)->by($moderator)->because('Under review')->freeze(until: now()->addDays(3));
// Undo
Lifecycles::for($listing)->by($user)->rollback();
// Model-level helpers, definitions, schedules and the sweep
Lifecycles::model(Listing::class)->graph();
Lifecycles::definitions()->validate(ListingLifecycle::class);
Lifecycles::schedules()->due();
Lifecycles::sweep();
// Deliberate direct writes, adopted at once
Lifecycles::allowDirectWrites(fn () => $listing->update(['status' => ListingStatus::Closed]));Flat methods
| Method | Returns | Does |
|---|---|---|
for($model, ?$lifecycle) | LifecycleHandle | A handle on one lifecycle of a subject (the first declared one when none is named). |
model(Model::class, ?$lifecycle) | ModelLifecycle | Class-level helpers: states, initial and terminal states, transitions, graph, bulk adopt. |
definitions() | DefinitionsAccessor | Compiled definitions, validation, graphs and the configured subjects. |
schedules() | SchedulesAccessor | Run due schedules, send warnings, list due schedules, retry a failed one. |
sweep(?$limit, ?$queue, ?$connection) | SweepResult | Warnings, then every due expiry and schedule — what lifecycle:sweep runs. A null queue follows schedules.queue.enabled; a null connection sweeps the default database connection. |
prune(PruneOptions) | PruneResult | Delete old history rows and finished schedules. |
adopt($model, ?$lifecycle) / adoptAll(Model::class, ?$lifecycle, $chunk, $scheduleExpiry) | bool / int | Reconcile one subject, or every row of a model, with the stored state. |
allowDirectWrites(fn () => …) | mixed | Run code that writes lifecycle attributes directly; the change is adopted on save. |
fake() | LifecycleFake | Swap the manager for a recording fake (see Testing). |
apply(TransitionRequest), check(), rollback(RollbackRequest), … | — | The request-DTO layer — see DI and actions. |
The for($model) handle
Lifecycles::for($model, ?$lifecycle) returns an immutable LifecycleHandle; null selects the first lifecycle of lifecycleDefinitions(). The context methods each return a new handle, so a handle can be shared safely. A model that is not a LifecycleSubject throws UnknownLifecycleException, and so does an undeclared lifecycle.
| Method | Returns | Does |
|---|---|---|
by(?$actor), asSystem(), because(?$reason), with(array $payload), expectingVersion(int), idempotencyKey(string) | LifecycleHandle | Context for the next calls — each returns a new handle; the last of by() / asSystem() wins. |
apply($transition) / transitionTo($state) | TransitionResult | Apply by name, or the one transition to a target state. Throws TransitionDeniedException. |
attempt($transition) | TransitionAttempt | Like apply(), with the refusal returned instead of thrown. |
can($transition) / canTransitionTo($state) | bool | Would it be allowed right now? |
check($transition) / checkTransitionTo($state) | Decision | The full, advisory decision with every denial. |
allowedTransitions(includeDenied: false) / allowedStates() | list | AvailableTransition objects (refused ones too with includeDenied), or the reachable states. |
state() / effectiveState() / is(...$states) / isTerminal() | state / bool | The model’s attribute as loaded (not re-read from the database), the state once an overdue expiry runs, membership, finality. |
enteredAt() / version() / definition() | ?CarbonImmutable / int / CompiledDefinition | Entry time of the current stay, the record version, the compiled definition. |
history($limit = 50) / lastTransition() | Collection / ?TransitionRecord | History rows, newest first. |
rollback(force: false) / rollbackTo($record, force: false) | RollbackResult | Undo the last transition, or everything after a history row — all or nothing. |
canRollback() / canRollbackTo($record) | Decision | Would the rollback be allowed right now? |
freeze(until: null) / unfreeze() / isFrozen() / frozenUntil() / frozenReason() | bool / … | Freeze this lifecycle (the handle’s reason and actor are recorded) and read the freeze. |
schedule($transition, $at) / cancelScheduled($transition) / scheduled() | ScheduledTransition / bool / list | Run a transition later, cancel it, list the open schedules. |
retryScheduled($transition) | bool | Put this subject’s newest failed schedule of a transition back to pending (an expiry by its expiry transition), on the subject’s own connection. |
expiresAt() / isExpired() / isInGrace() / isExpiringWithin($interval) | ?CarbonImmutable / bool | Read the pending expiry. |
expireAt($at) / extend($by) / renew(?$for) / neverExpire() | CarbonImmutable / bool | Change the expiry of the current stay. |
adopt() | bool | Reconcile this subject with its stored state now. |
Lifecycles::model()
Class-level helpers for one lifecycle of a model:
| Method | Returns | Does |
|---|---|---|
definition() | CompiledDefinition | The compiled definition. |
states() / initial() / terminal() | list / state / list | Declared, initial and terminal states. |
transitions() | list<TransitionDefinition> | Every transition, in declaration order. |
graph(?GraphFormat) | string | A Mermaid or DOT diagram. |
adopt(chunk: 500, scheduleExpiry: true) | int | Reconcile every row of the model; returns how many changed (= lifecycle:adopt). |
Lifecycles::definitions()
| Method | Returns | Does |
|---|---|---|
get($definitionClass) | CompiledDefinition | Compile (once per process) and return a definition. |
of($model, ?$lifecycle) | CompiledDefinition | The definition behind a model’s lifecycle. |
validate($definitionClass) | ValidationReport | isValid(), errors(), warnings() — without throwing. |
graph($definitionClass, ?GraphFormat) | string | A Mermaid or DOT diagram. |
subjects() / registered() | list | config('lifecycle.subjects') and the definitions behind them. |
flush() | void | Drop the compiled definitions (tests that redefine a lifecycle). |
Lifecycles::schedules()
| Method | Returns | Does |
|---|---|---|
runDue(?$limit, ?$queue, ?$connection) | SweepResult | Run due schedules, without warnings. |
warn(?$limit, ?$connection) | int | Send due expiry warnings; returns how many fired. |
retry($scheduleId, ?$connection) | bool | Failed → pending, attempts reset. Schedule ids are per database connection. |
due(?$now, $limit = 100, ?$connection) | Collection<ScheduledTransition> | Pending schedules due by now, oldest first. Read-only. |
failed($limit = 100, ?$connection) | Collection<ScheduledTransition> | Failed schedules, most recently failed first — with their subject and outcome. |
The HasLifecycle shorthands ($listing->transition(), $listing->lifecycle()->…) call the same manager, so everything here — including Lifecycles::fake() — applies to them too.
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.