Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lifecycle For Laravel Laravel Package

roundly-consulting/lifecycle-for-laravel

Lifecycle for Laravel adds configurable lifecycles to Eloquent models: define states, allowed transitions, guards, and hooks so business rules stay organized, predictable, and testable instead of scattered across controllers, observers, and service classes.

View on GitHub
Deep Wiki
Context7

Getting Started

This package gives your Eloquent models an append-only transition history: every state change is written as a TransitionRecord, and the package ships API resources that render that history (and the current state) without you hand-rolling serializers. 1.1.0 is a small, additive release — the headline is that the actor on a history row now has a proper, swappable resource.

Minimal setup:

  1. Require the package and publish its config + migrations (php artisan vendor:publish), then php artisan migrate to create the transitions table.
  2. Add the package's lifecycle trait to any model you want to track, and record transitions from your domain code (controller/service/observer) rather than from the model's saving hook — you want an explicit actor and timestamp per transition.
  3. Expose history from your API with the packaged resources: LifecycleResource for a model's current state plus last_transition, and TransitionRecordResource for each row in the history list.

First use case: an audit/history endpoint on an existing resource. Add last_transition to your show response, then a GET /orders/{order}/transitions endpoint returning the collection. You get {id, type} for the actor for free — 1.1.0's TransitionActorResource wraps the TransitionRecord, so rendering history loads no actor model and adds no query. That is the design constraint to internalize: the actor is a reference, not a hydrated relation.

Where to look first: the transitions migration (know your columns before writing queries/filters), then TransitionRecordResource to see the exact wire shape, then config/lifecycle.php for resources.actor.

Implementation Patterns

Render history as a nested, not top-level, resource. LifecycleResource already carries last_transition; use TransitionRecordResource::collection($model->transitions) for the full timeline and keep both endpoints shaped from the same resource classes so actor.type is consistent everywhere.

Customize the actor once, in config. The day-one ask is almost always "we need the user's name on the history row". Don't fight the serializer in each endpoint — subclass JsonResource:

class ActorResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'    => $this->id,
            'type'  => class_basename($this->resource::class),
            'name'  => $this->name,
            'email' => $this->email,
        ];
    }
}
// config/lifecycle.php
'resources' => [
    'actor' => App\Http\Resources\ActorResource::class,
],

Because the resource receives the record (or a lazily-resolved actor reference), anything you add beyond id/type is your responsibility to eager load — see Gotchas.

Treat type as a contract, not an implementation detail. Since 1.1.0, type is the actor model's class basename (User), not the morph class (App\Models\User when no morph map is registered). If you do register a morph map, type is the basename of the mapped class, not the alias. If your frontend already keys off App\Models\User or user, either normalize on the client or map it in your own ActorResource:

'type' => match ($this->resource::class) {
    \App\Models\User::class   => 'user',
    \App\Models\System::class => 'system',
},

Wire actor identity through consistently. Use the same actor-resolution path (auth guard → Auth::user(), or a system/service actor) for every record call, otherwise type becomes a grab-bag of classes and clients can't branch on it.

Version or alias the API shape. If you expose history publicly, document {id, type} as a stable enum of basenames and add new actor types there rather than letting model class names leak into your public API contract.

Upgrade pattern for 1.1.0. No migration or config change is required to adopt it — the new default applies immediately. Check your API tests for hardcoded App\Models\... or alias strings in actor.type, update them, and ship. If you have external consumers, this is a wire-format change even though it's listed under "Changed": announce it.

Gotchas and Tips

  • actor.type silently changed in 1.1.0. No exception, no deprecation — just different strings. Any client, cached payload, snapshot test, or stored JSON that persisted App\Models\User (or your morph alias) is now stale. Grep both your codebase and, if you store rendered history, your database.
  • Morph maps flip the behaviour in a way that surprises people. With a morph map, the pre-1.1.0 output was the alias (user); now it's the mapped class basename (User). If you added a morph map because you wanted clean API strings, this release moves you backwards — subclass the actor resource and re-map deliberately.
  • Config validation is strict and fails at render time. A blank string or null falls back to the packaged resource, but anything that isn't an instantiable JsonResource subclass throws InvalidLifecycleConfigurationException with reason notAResource. That includes: an interface, an abstract class, a class that doesn't exist, and a class whose constructor requires arguments. Keep your actor resource constructor-less.
  • notAResource in production usually means a stale config cache. Always php artisan config:clear (or config:cache in deploy) after changing lifecycle.resources.actor. The exception message names the reason — read it before assuming a missing class.
  • Adding fields to the actor resource reintroduces N+1. The zero-query guarantee holds only for the packaged {id, type}. The moment you render name or avatar, either eager load on the host query (respecting the morph map: with('transitions.actor') won't work if actor isn't a real relation — check how the package exposes it) or guard with $this->whenLoaded(...) and accept null when not loaded.
  • Don't hydrate actors in loops. If you need a batch of names, collect the (type, id) pairs from the timeline and do one whereIn per actor class, then key a lookup array passed to the resource. This keeps the packaged resource's no-query property for the common case while making the rich case acceptable.
  • last_transition and the history list must agree. Both go through TransitionRecordResource in 1.1.0, so they will — but only if you don't bypass the resource anywhere (raw $model->transitions->toArray(), custom serializers, broadcast events). Centralize.
  • Watch ordering and nullability. A model with no transitions must still return a sane last_transition (null), and history order should be explicit (newest-first for feeds, oldest-first for audit). Don't rely on insert order.
  • Use the package's test matrix as a hint. CI now runs PostgreSQL and MySQL legs on tmpfs — so JSON columns, timestamps, and ordering behave the same across engines. If you're on SQLite locally, verify ordering/json assertions against MySQL or PG before shipping.
  • Extension points worth knowing: only lifecycle.resources.actor is swappable in 1.1.0. If you need to change the transition row shape itself, subclass TransitionRecordResource and use it at your endpoint — but then you own the consistency risk with LifecycleResource::last_transition, which will keep rendering the packaged shape unless you swap it too.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource