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.

Maintainers

Package info

github.com/RizalAnas00/laravel-status-transition

pkg:composer/rizalsaja/laravel-status-transition

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-05-05 03:55 UTC

This package is auto-updated.

Last update: 2026-05-05 04:03:07 UTC


README

Tests Latest Version on Packagist Total Downloads License PHP Version

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_histories table 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.