NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Lifecycle for Laravel

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

MethodReturnsDoes
for($model, ?$lifecycle)LifecycleHandleA handle on one lifecycle of a subject (the first declared one when none is named).
model(Model::class, ?$lifecycle)ModelLifecycleClass-level helpers: states, initial and terminal states, transitions, graph, bulk adopt.
definitions()DefinitionsAccessorCompiled definitions, validation, graphs and the configured subjects.
schedules()SchedulesAccessorRun due schedules, send warnings, list due schedules, retry a failed one.
sweep(?$limit, ?$queue, ?$connection)SweepResultWarnings, 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)PruneResultDelete old history rows and finished schedules.
adopt($model, ?$lifecycle) / adoptAll(Model::class, ?$lifecycle, $chunk, $scheduleExpiry)bool / intReconcile one subject, or every row of a model, with the stored state.
allowDirectWrites(fn () => …)mixedRun code that writes lifecycle attributes directly; the change is adopted on save.
fake()LifecycleFakeSwap 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.

MethodReturnsDoes
by(?$actor), asSystem(), because(?$reason), with(array $payload), expectingVersion(int), idempotencyKey(string)LifecycleHandleContext for the next calls — each returns a new handle; the last of by() / asSystem() wins.
apply($transition) / transitionTo($state)TransitionResultApply by name, or the one transition to a target state. Throws TransitionDeniedException.
attempt($transition)TransitionAttemptLike apply(), with the refusal returned instead of thrown.
can($transition) / canTransitionTo($state)boolWould it be allowed right now?
check($transition) / checkTransitionTo($state)DecisionThe full, advisory decision with every denial.
allowedTransitions(includeDenied: false) / allowedStates()listAvailableTransition objects (refused ones too with includeDenied), or the reachable states.
state() / effectiveState() / is(...$states) / isTerminal()state / boolThe 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 / CompiledDefinitionEntry time of the current stay, the record version, the compiled definition.
history($limit = 50) / lastTransition()Collection / ?TransitionRecordHistory rows, newest first.
rollback(force: false) / rollbackTo($record, force: false)RollbackResultUndo the last transition, or everything after a history row — all or nothing.
canRollback() / canRollbackTo($record)DecisionWould 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 / listRun a transition later, cancel it, list the open schedules.
retryScheduled($transition)boolPut 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 / boolRead the pending expiry.
expireAt($at) / extend($by) / renew(?$for) / neverExpire()CarbonImmutable / boolChange the expiry of the current stay.
adopt()boolReconcile this subject with its stored state now.

Lifecycles::model()

Class-level helpers for one lifecycle of a model:

MethodReturnsDoes
definition()CompiledDefinitionThe compiled definition.
states() / initial() / terminal()list / state / listDeclared, initial and terminal states.
transitions()list<TransitionDefinition>Every transition, in declaration order.
graph(?GraphFormat)stringA Mermaid or DOT diagram.
adopt(chunk: 500, scheduleExpiry: true)intReconcile every row of the model; returns how many changed (= lifecycle:adopt).

Lifecycles::definitions()

MethodReturnsDoes
get($definitionClass)CompiledDefinitionCompile (once per process) and return a definition.
of($model, ?$lifecycle)CompiledDefinitionThe definition behind a model’s lifecycle.
validate($definitionClass)ValidationReportisValid(), errors(), warnings() — without throwing.
graph($definitionClass, ?GraphFormat)stringA Mermaid or DOT diagram.
subjects() / registered()listconfig('lifecycle.subjects') and the definitions behind them.
flush()voidDrop the compiled definitions (tests that redefine a lifecycle).

Lifecycles::schedules()

MethodReturnsDoes
runDue(?$limit, ?$queue, ?$connection)SweepResultRun due schedules, without warnings.
warn(?$limit, ?$connection)intSend due expiry warnings; returns how many fired.
retry($scheduleId, ?$connection)boolFailed → 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 crypto

By 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.