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

Laravel Statable Laravel Package

iben12/laravel-statable

Laravel Statable makes it easy to add sortable, searchable, paginated data tables to Laravel apps. Define columns and filters on your Eloquent model with a simple trait, then render responsive server-side tables using Blade components and minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (3 steps):

composer require iben12/laravel-statable
  1. Add the state column to the model's table (usually string or enum):
Schema::table('orders', function (Blueprint $table) {
    $table->string('state')->default('pending')->index();
});
  1. Apply the trait and declare the machine in one place:
use Iben12\LaravelStatable\Traits\Statable;

class Order extends Model
{
    use Statable;

    protected $states = ['pending', 'paid', 'shipped', 'cancelled'];

    protected $transitions = [
        'pending' => ['paid', 'cancelled'],
        'paid'    => ['shipped'],
    ];
}
  1. Transition explicitly from a controller/action/job:
$order->transitionTo('paid'); // throws on invalid transition

Where to look first: the trait source (src/Traits/Statable.php or similar) — it is the entire public API surface. Read it before the README, because the README is thin and the method names (transitionTo, canTransitionTo, state, scopes) are what you will actually be calling. Also read tests/ to see the canonical happy path and the exact exception thrown on a rejected transition.

First use case: an order/payment/invoice lifecycle in a domain where an illegal jump must be impossible to write by accident. The value is not "storing an enum" — it is that pending -> shipped fails loudly.

Implementation Patterns

Controller flow — validate, then transition:

public function update(Request $request, Order $order)
{
    $request->validate(['state' => ['required', 'string']]);

    if (! $order->canTransitionTo($request->state)) {
        throw ValidationException::withMessages([
            'state' => "Cannot move from {$order->state} to {$request->state}.",
        ]);
    }

    $order->transitionTo($request->state);

    return back();
}

Checking first (rather than catching the exception) keeps the error a 422 instead of a 500 and puts the user-facing message in your control.

Wrap side effects in Laravel events, not in the controller. The trait's main integration win is that transition events let you decouple "the order got paid" from "send a receipt":

// app/Providers/EventServiceProvider.php
protected $listen = [
    \Iben12\LaravelStatable\Events\StateChanged::class => [
        \App\Listeners\SendPaidReceipt::class,
        \App\Listeners\SyncToWarehouse::class,
    ],
];

Verify the actual event class name and its payload in the source — this is the single most important thing to check, because side effects attached to a non-firing event fail silently.

Query by state — use the provided scope and index the column:

Order::whereState('paid')->get();
Order::whereState(['pending', 'paid'])->get(); // if supported

Always ->index() the state column. Nearly every list screen and every scheduled job ("find all paid orders older than an hour") filters on it.

Guard rails at the model boundary. Put transitionTo() calls in a small domain service or model method rather than sprinkled through controllers:

class Order extends Model
{
    public function markPaid(Payment $payment): void
    {
        $this->transitionTo('paid');
    }
}

This gives you one place to add audit logging, an OrderPaid domain event, and a lock.

Audit trail: pair the transition event listener with a state_changes table (model_type, model_id, from, to, user_id, created_at). The package validates transitions; it does not record history, and you will be asked for history.

Form validation from the machine: expose allowed transitions so the frontend can render only legal buttons instead of a free-text state field:

$allowed = $order->transitionsFromCurrentState(); // check exact name in source

Gotchas and Tips

Last release 2021 — verify compatibility before committing. This package predates Laravel 9/10/11 conventions. Check: does it declare illuminate/support with a constraint that permits your Laravel version? Does it work on PHP 8.2+? If the constraint blocks install, fork it and loosen the requirement — the source is small enough that this is a 10-minute job, and it is safer than excluding the package from a composer update.

Do not mutate the state column directly. $order->update(['state' => 'paid']) bypasses validation and fires no transition events. If state is mass-assignable, someone will eventually do this. Add state to $guarded (or omit it from $fillable) so that the only path is transitionTo().

Undefined transitions are a common source of runtime exceptions. The $transitions array is authoritative. If a state exists in $states but has no key in $transitions (or vice versa), you get either an unreachable state or an exception on a legal-looking move. Keep both arrays adjacent in the file, and add a test that every state appears in $transitions.

Concurrency is not handled for you. Two requests can both read pending, both see ->paid as allowed, and both transition. Use a transaction with a lock for anything financial:

DB::transaction(function () use ($order) {
    $order = Order::lockForUpdate()->find($order->id);
    $order->transitionTo('paid');
});

Terminal states need explicit modelling. A state with no outgoing transitions (shipped in the example above) will throw on any attempt to move out — which is correct, but if you later need shipped -> returned, you must edit the machine and write a migration for any rows already sitting in the terminal state.

Events fire on transition, not on creation. If your model is created with state = 'pending', no transition event fires, so listeners that expect to handle "order entered pending" must also hook created. Don't rely on the state machine for initial-state side effects.

Test against the trait, not the database. Because the package is pure PHP around the trait, unit tests can instantiate the model without touching the DB at all — much faster than factories for validating transition matrices:

$this->expectException(\Iben12\LaravelStatable\Exceptions\InvalidTransition::class);
(new Order(['state' => 'pending']))->transitionTo('shipped');

Extension point: override the guard method rather than duplicating the matrix. If you need role-based transitions (only an admin may cancel a paid order), subclass or override the canTransitionTo logic in a base model instead of adding a second if ladder in every controller. That keeps one source of truth for "is this legal" and layers authorization on top via a Policy.

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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr