rizalsaja / laravel-status-transition
A simple and flexible trait to add state machine behaviour to Laravel Eloquent models, with transition validation and automatic history tracking.
Package info
github.com/RizalAnas00/laravel-status-transition
pkg:composer/rizalsaja/laravel-status-transition
Requires
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2026-05-05 04:03:07 UTC
README
A simple and flexible trait to add state machine behaviour to Laravel Eloquent models, with transition validation and automatic history tracking.
Features
- Attach status state machine to any Eloquent model via a single trait
- Define allowed statuses and enforce valid transition paths
- Automatic status history recording with reason and actor tracking
- Polymorphic history — one
status_historiestable for all models - Query scopes for filtering by status
- Configurable: disable history recording globally via config
- Auto-discovery support — no manual provider registration needed
Requirements
| Package version | Laravel | PHP |
|---|---|---|
| 1.x | 10, 11, 12 | 8.1+ |
Installation
Install via Composer:
composer require rizalsaja/laravel-status-transition
The service provider is auto-discovered. No manual registration needed.
Publish the config file:
php artisan vendor:publish --tag=laravel-status-transition-config
Publish and run the migrations:
php artisan vendor:publish --tag=laravel-status-transition-migrations php artisan migrate
Usage
1. Add the trait to your model
use Rizalsaja\LaravelStatusTransition\Traits\HasStatus; class Order extends Model { use HasStatus; /** * All valid statuses for this model. */ protected $statuses = [ 'pending', 'processing', 'shipped', 'delivered', 'cancelled', ]; /** * Allowed transition map. * Omit this property to allow all transitions freely. */ protected $transitions = [ 'pending' => ['processing', 'cancelled'], 'processing' => ['shipped', 'cancelled'], 'shipped' => ['delivered'], 'delivered' => [], 'cancelled' => [], ]; }
Make sure your model's table has a status column:
$table->string('status')->default('pending');
2. Transition to a new status
$order = Order::create(['title' => 'New Order']); // Simple transition $order->transitionTo('processing'); // With a reason $order->transitionTo('cancelled', reason: 'Customer requested cancellation');
3. Check current status
$order->getCurrentStatus(); // 'processing' $order->isStatus('processing'); // true $order->isNotStatus('shipped'); // true $order->canTransitionTo('shipped'); // true $order->availableTransitions(); // ['shipped', 'cancelled']
4. Query by status
Order::whereStatus('pending')->get(); Order::whereNotStatus('cancelled')->get(); Order::whereStatusIn(['pending', 'processing'])->get();
5. Access history
// All history records (ordered by latest inserted) $order->statusHistory; // Most recent record only $order->latestStatus; // History fields $history->from; // 'pending' $history->to; // 'processing' $history->reason; // 'Payment confirmed' $history->changed_by; // user id (nullable) $history->created_at;
6. Resolve back to the model
$history = $order->statusHistory->first(); $history->statusable; // returns the Order instance
Configuration
After publishing, edit config/laravel-status-transition.php:
return [ /* * Default statuses if the model does not define its own $statuses property. */ 'default_statuses' => ['active', 'inactive'], /* * Set to false to disable status history recording entirely. */ 'record_history' => true, ];
Customisation
Custom status column
// default: 'status' protected $statusColumn = 'state';
Custom initial status
// default: first item in $statuses protected $initialStatus = 'draft';
Allow all transitions freely
Omit $transitions from your model. Without it, any status can transition to any other status defined in $statuses.
Error Handling
use Rizalsaja\LaravelStatusTransition\Exceptions\InvalidStatusTransitionException; try { $order->transitionTo('shipped'); // invalid from 'pending' } catch (InvalidStatusTransitionException $e) { // "Cannot transition from [pending] to [shipped]. Allowed transitions: [processing, cancelled]." report($e); } try { $order->transitionTo('unknown'); } catch (\InvalidArgumentException $e) { // "Status [unknown] is not a valid status." report($e); }
Testing
vendor/bin/phpunit --testdox
Changelog
Please see CHANGELOG.md for recent changes.
Contributing
Please see CONTRIBUTING.md for details.
License
The MIT License (MIT). Please see LICENSE.md for more information.