DI and actions
The facade is the recommended default, not the only way in. Three entry points run the same code: the Lifecycles facade (shortest), the manager RoundlyConsulting\Lifecycle\LifecycleManager — the facade’s root, a container singleton that holds no state — injected through the constructor (same API, explicit dependency, no static calls), and actions (single-purpose classes with execute() that take a request DTO, for composing into your own actions, jobs and commands).
Inject the manager
use RoundlyConsulting\Lifecycle\LifecycleManager;
final readonly class ReopenListing
{
public function __construct(private LifecycleManager $lifecycle) {}
public function __invoke(Listing $listing, User $user): void
{
$this->lifecycle->for($listing)->by($user)->because('Back in stock')->apply('reopen');
}
}Call an action
use RoundlyConsulting\Lifecycle\Actions\ApplyTransitionAction;
use RoundlyConsulting\Lifecycle\DataTransferObjects\TransitionRequest;
// The raw use case
app(ApplyTransitionAction::class)->execute(new TransitionRequest(
subject: $listing,
lifecycle: 'status',
transition: 'reopen',
actor: $user,
reason: 'Back in stock',
));Manager methods with request DTOs
Every handle check and change goes through a manager method, most of them taking a request DTO — apply, attempt, check, available, rollback, checkRollback, freeze, unfreeze, schedule, cancelScheduled, changeExpiry, adopt, adoptAll, runDueSchedules, sendExpiryWarnings, retrySchedule, sweep, prune and allowDirectWrites. Reads are handle-only. The facade exposes these methods too:
use Carbon\CarbonInterval;
use RoundlyConsulting\Lifecycle\DataTransferObjects\ExpiryChangeRequest;
use RoundlyConsulting\Lifecycle\DataTransferObjects\PruneOptions;
use RoundlyConsulting\Lifecycle\DataTransferObjects\RollbackRequest;
use RoundlyConsulting\Lifecycle\Enums\ExpiryChange;
use RoundlyConsulting\Lifecycle\Facades\Lifecycles;
Lifecycles::rollback(new RollbackRequest($listing, 'status', toHistoryId: 42, actor: $user));
Lifecycles::checkRollback(new RollbackRequest($listing, 'status')); // Decision
Lifecycles::changeExpiry(new ExpiryChangeRequest($listing, 'status', ExpiryChange::Extend, interval: CarbonInterval::days(7)));
Lifecycles::prune(new PruneOptions(historyOlderThanDays: 365, schedulesOlderThanDays: 30, dryRun: true));Handle method → manager → action
| Handle / facade call | Manager method | Action |
|---|---|---|
apply() / attempt() / transitionTo() | apply(TransitionRequest) / attempt(TransitionRequest) | ApplyTransitionAction |
check() / can() / canTransitionTo() / checkTransitionTo() | check(TransitionRequest) | CheckTransitionAction |
allowedTransitions() / allowedStates() | available(AvailableTransitionsQuery) | ListAvailableTransitionsAction |
rollback() / rollbackTo() | rollback(RollbackRequest) | RollbackAction |
canRollback() / canRollbackTo() | checkRollback(RollbackRequest) | CheckRollbackAction |
freeze() | freeze(FreezeRequest) | FreezeAction |
unfreeze() | unfreeze(UnfreezeRequest) | UnfreezeAction |
schedule() | schedule(ScheduleRequest) | ScheduleTransitionAction |
cancelScheduled() | cancelScheduled(CancelScheduleRequest) | CancelScheduledTransitionAction |
expireAt() / extend() / renew() / neverExpire() | changeExpiry(ExpiryChangeRequest) | ChangeExpiryAction |
Lifecycles::sweep() / schedules()->runDue() | sweep(?$limit, ?$queue, ?$connection) / runDueSchedules(SweepOptions) | RunDueSchedulesAction |
schedules()->warn() | sendExpiryWarnings(SweepOptions) | SendExpiryWarningsAction |
schedules()->retry() / for($model)->retryScheduled() | retrySchedule(int $scheduleId, ?$connection) | RetryScheduleAction |
Lifecycles::prune() | prune(PruneOptions) | PruneAction |
for($model)->adopt() | adopt($model, ?$lifecycle) | AdoptLifecycleAction |
model(Model::class)->adopt() | adoptAll(Model::class, …) | AdoptModelLifecyclesAction |
- Every manager method resolves one action from the current container and calls execute(), so a container override of an action applies to the facade, injected managers, handles and the trait alike.
- Handles and the HasLifecycle trait build request DTOs and call the manager — never an action — so Lifecycles::fake() sees every call, whichever style made it.
- Lifecycles::fake() swaps the manager in the container too, so constructor-injected managers are faked as well (LifecycleFake extends LifecycleManager). Calling an action directly bypasses the fake.
- attempt() is apply() with the refusal returned as a TransitionAttempt instead of thrown; allowDirectWrites() is scoped to the current request or job and has no action.
- The manager is Octane-safe: it keeps no state and resolves everything from the current container; compiled definitions are immutable and shared.
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.