---
title: "DI and actions — Lifecycle for Laravel | Roundly"
description: "Skip the facade: inject LifecycleManager or call a single-purpose action with a request DTO — the handle → manager → action map."
url: https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel/without-facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Lifecycle for Laravel](https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel.md)

# 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

```php
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

```php
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:

```php
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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel.md)
