Configuration
Every key has a default, so publishing config/lifecycle.php is optional. The published file in full:
return [
'key_type' => env('LIFECYCLE_KEY_TYPE', 'bigint'),
'actor_key_type' => env('LIFECYCLE_ACTOR_KEY_TYPE', 'bigint'),
'models' => [
'state' => LifecycleState::class,
'transition' => LifecycleTransition::class,
'schedule' => LifecycleSchedule::class,
],
'subjects' => [],
'strict_writes' => env('LIFECYCLE_STRICT_WRITES', true),
'actor' => [
'from_auth' => env('LIFECYCLE_ACTOR_FROM_AUTH', true),
'guard' => env('LIFECYCLE_ACTOR_GUARD'),
],
'transactions' => [
'attempts' => 3,
'mysql_read_committed' => env('LIFECYCLE_MYSQL_READ_COMMITTED', true),
],
'history' => [
'store_payload' => env('LIFECYCLE_HISTORY_STORE_PAYLOAD', true),
'max_context_bytes' => 16384,
'reason_max_length' => 1000,
'purge_on_force_delete' => env('LIFECYCLE_HISTORY_PURGE_ON_FORCE_DELETE', true),
'prune_after_days' => env('LIFECYCLE_HISTORY_PRUNE_AFTER_DAYS'),
],
'rollback' => [
'default_window' => env('LIFECYCLE_ROLLBACK_DEFAULT_WINDOW'),
'max_steps' => 50,
],
'schedules' => [
'batch_size' => 500,
'max_per_run' => 10000,
'max_attempts' => 5,
'retry_after' => '5 minutes',
'queue' => [
'enabled' => env('LIFECYCLE_QUEUE_SWEEPS', false),
'connection' => env('LIFECYCLE_QUEUE_CONNECTION'),
'name' => env('LIFECYCLE_QUEUE'),
],
'prune_after_days' => env('LIFECYCLE_SCHEDULES_PRUNE_AFTER_DAYS', 30),
],
'rate_limits' => [
'prefix' => 'lifecycle',
],
'graph' => [
'default_format' => 'mermaid',
],
];Every key
| Key | Default | Env | Purpose |
|---|---|---|---|
key_type | bigint | LIFECYCLE_KEY_TYPE | Id type of the subject columns the migrations create: bigint, uuid or ulid (case-insensitive). Set before migrating; any other value throws. |
actor_key_type | bigint | LIFECYCLE_ACTOR_KEY_TYPE | Id type of the actor columns (who transitioned, froze, scheduled): bigint, uuid or ulid; any other value throws. |
models.state | LifecycleState | — | Swap the state-record model for your subclass (it must extend LifecycleState). |
models.transition | LifecycleTransition | — | Swap the history model for your subclass (it must extend LifecycleTransition). |
models.schedule | LifecycleSchedule | — | Swap the schedule model for your subclass (it must extend LifecycleSchedule). |
subjects | [] | — | Your models with a lifecycle. lifecycle:validate without arguments checks every lifecycle of every listed model, including the columns its definition names. Models work without being listed. |
strict_writes | true | LIFECYCLE_STRICT_WRITES | Saving a directly changed lifecycle attribute throws DirectStateWriteException. |
actor.from_auth | true | LIFECYCLE_ACTOR_FROM_AUTH | With no by(), the authenticated user is the actor. |
actor.guard | null | LIFECYCLE_ACTOR_GUARD | The auth guard for from_auth (null = the default guard). |
transactions.attempts | 3 | — | How often a transaction is retried after a deadlock (1–10) — only when the package opened the outermost transaction. |
transactions.mysql_read_committed | true | LIFECYCLE_MYSQL_READ_COMMITTED | MySQL/MariaDB: a transaction that counts a quota runs at READ COMMITTED. Off: it keeps your isolation and the quota count takes locking reads. |
history.store_payload | true | LIFECYCLE_HISTORY_STORE_PAYLOAD | Keep the validated payload (minus sensitive() keys) in the history row. |
history.max_context_bytes | 16384 | — | JSON size cap of the stored context (1024–1048576); larger is refused as invalid_payload. |
history.reason_max_length | 1000 | — | Longer reasons are refused as reason_too_long — transitions, schedules and rollbacks; a longer freeze reason throws InvalidLifecycleUsageException (1–10000). |
history.purge_on_force_delete | true | LIFECYCLE_HISTORY_PURGE_ON_FORCE_DELETE | Force-deleting a subject deletes its records, history and schedules. |
history.prune_after_days | null | LIFECYCLE_HISTORY_PRUNE_AFTER_DAYS | lifecycle:prune default for history rows (1–36500; null or blank = never). Limits survive pruning; older rollback points and idempotency keys do not. |
rollback.default_window | null | LIFECYCLE_ROLLBACK_DEFAULT_WINDOW | How long a transition stays reversible when it declares no window (“7 days”; null or blank = unlimited). |
rollback.max_steps | 50 | — | Most rows one rollbackTo() may revert (1–1000). |
schedules.batch_size | 500 | — | Rows per sweep batch (1–10000). |
schedules.max_per_run | 10000 | — | Most schedules one sweep run handles (1–1000000). |
schedules.max_attempts | 5 | — | Attempts before a retryable scheduled transition is marked failed (1–100). |
schedules.retry_after | 5 minutes | — | Delay before a refused, errored or frozen scheduled transition is tried again. A blank value is not set → 5 minutes. |
schedules.queue.enabled | false | LIFECYCLE_QUEUE_SWEEPS | The sweep dispatches one job per due schedule instead of running it inline. |
schedules.queue.connection | null | LIFECYCLE_QUEUE_CONNECTION | Queue connection of those jobs. |
schedules.queue.name | null | LIFECYCLE_QUEUE | Queue name of those jobs. |
schedules.prune_after_days | 30 | LIFECYCLE_SCHEDULES_PRUNE_AFTER_DAYS | lifecycle:prune default for finished schedule rows (1–36500; null or blank = never). |
rate_limits.prefix | lifecycle | — | Prefix of the rate-limiter keys of rateLimit() transitions; it counts towards the key’s 250-character limit. |
graph.default_format | mermaid | — | Exactly mermaid or dot — the graph format when none is given. |
Strict values
Every value is validated when it is read, so a typo never silently falls back to the default:
- A blank value (LIFECYCLE_X= or whitespace only) is not set: the key’s default applies, and a nullable key (LIFECYCLE_HISTORY_PRUNE_AFTER_DAYS=, LIFECYCLE_SCHEDULES_PRUNE_AFTER_DAYS=, LIFECYCLE_ROLLBACK_DEFAULT_WINDOW=) reads as unset (null).
- Booleans accept true/false/1/0/yes/no/on/off, case-insensitive (a blank value takes the default, so LIFECYCLE_STRICT_WRITES= keeps strict writes on); any other value throws InvalidLifecycleConfigurationException naming the key and the value.
- key_type and actor_key_type accept only bigint, uuid or ulid (case-insensitive; blank reads as bigint); anything else throws the toolkit’s InvalidConfigurationException — not a LifecycleException, so catch (LifecycleException) does not cover it — during migrate and about.
- Numbers must be canonical integers: '5.5', '+5' and '1e3' throw. A number or interval outside its range throws InvalidLifecycleConfigurationException.
- graph.default_format must be exactly mermaid or dot. A swapped model (models.*) must be an existing class that extends the package model, else InvalidLifecycleConfigurationException — it never falls back to the package model.
- A value throws when something first reads it. php artisan about reads the key types, most booleans, the models and the subjects, so run it after every configuration change to surface a typo at once.
Register your subjects
List your models with a lifecycle in subjects, so lifecycle:validate has something to check in CI. Models work without being listed; with nothing listed, lifecycle:validate --strict fails instead of passing vacuously:
// config/lifecycle.php
'subjects' => [
App\Models\Listing::class,
App\Models\Order::class,
],Environment
Every env variable the config reads, with its default:
LIFECYCLE_KEY_TYPE=bigint
LIFECYCLE_ACTOR_KEY_TYPE=bigint
LIFECYCLE_STRICT_WRITES=true
LIFECYCLE_ACTOR_FROM_AUTH=true
LIFECYCLE_ACTOR_GUARD=
LIFECYCLE_MYSQL_READ_COMMITTED=true
LIFECYCLE_HISTORY_STORE_PAYLOAD=true
LIFECYCLE_HISTORY_PURGE_ON_FORCE_DELETE=true
LIFECYCLE_HISTORY_PRUNE_AFTER_DAYS=
LIFECYCLE_ROLLBACK_DEFAULT_WINDOW=
LIFECYCLE_QUEUE_SWEEPS=false
LIFECYCLE_QUEUE_CONNECTION=
LIFECYCLE_QUEUE=
LIFECYCLE_SCHEDULES_PRUNE_AFTER_DAYS=30Show 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.