nathandunn / laravel-state-history
Laravel package for managing enum-based model states with enforced transitions and automatic status history tracking.
v1.0.2
2025-08-25 07:34 UTC
Requires
- php: ^8.2
- illuminate/database: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- laravel/framework: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.24
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-08-25 07:37:01 UTC
README
A Laravel package for managing enum-based model states with enforced transitions and automatic history tracking.
Features
- Native PHP Enums (PHP 8.2+)
- Enforced Transitions with a pluggable state machine
- Automatic History with metadata
- Smart Casting of historical
from
/to
values (enums, dates, primitives, custom casts) - Atomic Operations – state change + history in one transaction
- Current State Columns (
current_{field}
) for indexing & querying - Events, Guards & Effects for lifecycle hooks
- Laravel 11–12 Support
Installation
composer require nathandunn/laravel-state-history php artisan vendor:publish --provider="NathanDunn\StateHistory\StateHistoryServiceProvider" --tag="migrations" php artisan migrate
Quick Start
1. Define States
enum ArticleState: string { case Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; }
2. Create a State Machine
use NathanDunn\StateHistory\Contracts\StateMachine; use NathanDunn\StateHistory\TransitionMap; class ArticleStateMachine implements StateMachine { public function getTransitions(): TransitionMap { return TransitionMap::build(ArticleState::class) ->allowFromNull(ArticleState::Draft) ->allow(ArticleState::Draft, ArticleState::Published) ->allow(ArticleState::Published, ArticleState::Archived) ->allowAnyTo(ArticleState::Draft); } }
3. Configure Your Model
use NathanDunn\StateHistory\Traits\HasState; class Article extends Model { use HasState; protected $casts = [ 'state' => ArticleState::class, ]; protected function stateMachine(): array { return ['state' => ArticleStateMachine::class]; } }
Usage
Transitions
$article = Article::create(['state' => ArticleState::Draft]); $article->transitionTo('state', ArticleState::Published, meta: [ 'editor' => 'alice' ]);
Querying
$published = Article::whereState('state', ArticleState::Published)->get(); if ($article->isInState('state', ArticleState::Published)) { // published } $allowed = $article->getAllowedTransitions('state');
Current State
$state = $article->getState('state'); // ArticleState::Published $raw = $article->getCurrentState('state'); // "published"
History
$history = $article->states('state'); foreach ($history as $h) { $from = $h->from; // Enum instance $to = $h->to; $meta = $h->meta; }
Advanced
Multiple State Fields
class Order extends Model { use HasState; protected $casts = [ 'status' => OrderStatus::class, 'payment_status' => PaymentStatus::class, ]; protected function stateMachine(): array { return [ 'status' => OrderStateMachine::class, 'payment_status' => PaymentStateMachine::class, ]; } }
Guards
use NathanDunn\StateHistory\Contracts\Guard; class PublishedArticleGuard implements Guard { public function allows($model, $from, $to): bool { if ($to === ArticleState::Archived && $model->published_at < now()->subDays(30)) { throw new \Exception('Must be published 30 days before archiving'); } return true; } }
Effects
use NathanDunn\StateHistory\Contracts\Effect; class PublishEffect implements Effect { public function execute($model, $from, $to): void { if ($to === ArticleState::Published) { $model->update(['published_at' => now()]); } } }
Events
StateTransitioning
– fired before a transitionStateTransitioned
– fired after success
use NathanDunn\StateHistory\Events\StateTransitioned; Event::listen(StateTransitioned::class, function ($event) { Log::info("Model {$event->model->id} {$event->from} → {$event->to}"); });
Current State Columns
Optional current_{field}
columns improve indexing & analytics.
Schema::table('articles', function (Blueprint $t) { $t->string('current_state')->nullable()->index(); });
Config (config/state-history.php
):
return [ 'use_current_columns' => true, 'prefix' => 'current_', 'model' => \App\Models\CustomStateHistory::class, ];
State Casting
History values auto-cast to configured types:
foreach ($article->states('state') as $h) { $from = $h->from; // Enum $to = $h->to; }
Supports: enums, dates, primitives, custom casts.