Weave Code
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 Model States Laravel Package

spatie/laravel-model-states

Add state and state machine behavior to Laravel Eloquent models. Represent each state as a class, automatically serialize to/from the database, and perform clean, explicit transitions with configurable rules—ideal for workflows like payments, orders, and approvals.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require spatie/laravel-model-states
    
  2. Database Migration: Add a state column to your model table (e.g., payments):

    Schema::table('payments', function (Blueprint $table) {
        $table->string('state')->nullable();
    });
    
  3. Model Integration: Use the HasStates trait and cast the state field:

    use Spatie\ModelStates\HasStates;
    
    class Payment extends Model
    {
        use HasStates;
    
        protected $casts = [
            'state' => PaymentState::class,
        ];
    }
    
  4. Define Abstract State Class: Create an abstract state class (e.g., PaymentState) extending Spatie\ModelStates\State:

    abstract class PaymentState extends State
    {
        public static function config(): StateConfig
        {
            return parent::config()
                ->default(Pending::class);
        }
    }
    
  5. Create Concrete State Classes: Implement concrete states (e.g., Pending, Paid):

    class Pending extends PaymentState {}
    class Paid extends PaymentState {}
    
  6. First Transition:

    $payment = Payment::find(1);
    $payment->state->transitionTo(Paid::class);
    

First Use Case: State-Driven UI Logic

Use states to dynamically render UI elements (e.g., buttons, labels) based on the model's state:

@if($payment->state instanceof Paid)
    <span class="text-green-500">Paid</span>
@else
    <span class="text-yellow-500">Pending</span>
@endif

Implementation Patterns

Workflow: State Transitions

  1. Define Transitions: Configure allowed transitions in the abstract state class:

    public static function config(): StateConfig
    {
        return parent::config()
            ->allowTransition(Pending::class, Paid::class)
            ->allowTransition(Pending::class, Failed::class);
    }
    
  2. Trigger Transitions: Use transitionTo() in controllers or services:

    $payment->state->transitionTo(Paid::class);
    
  3. Handle Transition Logic: Override methods in concrete states for side effects:

    class Paid extends PaymentState
    {
        public function transitionToPaid(): void
        {
            $this->model->update(['paid_at' => now()]);
        }
    }
    

Integration Tips

  1. Event-Driven Architecture: Listen to StateChanged events to trigger actions (e.g., notifications, logs):

    use Spatie\ModelStates\Events\StateChanged;
    
    StateChanged::listen(function (StateChanged $event) {
        Log::info("State changed from {$event->previousState} to {$event->newState}");
    });
    
  2. State-Driven Validation: Validate transitions or state-specific fields:

    public function rules()
    {
        return [
            'amount' => ['required', Rule::requiredUnless(fn () => $this->state instanceof Paid)],
        ];
    }
    
  3. API Responses: Serialize state-specific data in API responses:

    return PaymentResource::make($payment)->additional([
        'state_data' => $payment->state->getMetadata(),
    ]);
    
  4. State Machines for Complex Workflows: Use nested states or composite patterns for multi-step processes (e.g., order fulfillment):

    class OrderState extends State
    {
        public static function config(): StateConfig
        {
            return parent::config()
                ->allowTransition(Processing::class, Shipped::class)
                ->allowTransition(Shipped::class, Delivered::class);
        }
    }
    
  5. Testing States: Mock state transitions in tests:

    $payment = Payment::factory()->create();
    $this->assertInstanceOf(Pending::class, $payment->state);
    
    $payment->state->transitionTo(Paid::class);
    $this->assertInstanceOf(Paid::class, $payment->state);
    

Gotchas and Tips

Pitfalls

  1. Circular Dependencies: Avoid circular references in state transitions (e.g., A → B → A). Use allowTransition() to enforce one-way flows.

  2. Database Serialization: Ensure state classes are autoloadable. Place them in a single directory (e.g., app/States/Payment) to avoid resolution issues:

    app/
      States/
        Payment/
          Pending.php
          Paid.php
          PaymentState.php
    
  3. State Name Conflicts: Use static $name properties for custom state names (e.g., public static $name = 'paid'). Avoid hyphens (-) to prevent conflicts with Laravel’s internal naming.

  4. Default State Overrides: If no default state is set, the model’s state will be null. Always define a default in StateConfig:

    ->default(Pending::class)
    
  5. Mass Assignment Risks: Prevent mass assignment of state fields in fillable or guarded arrays to avoid unauthorized state changes:

    protected $guarded = ['state'];
    

Debugging

  1. Transition Errors: Use try-catch to handle invalid transitions gracefully:

    try {
        $payment->state->transitionTo(InvalidState::class);
    } catch (InvalidTransition $e) {
        Log::error("Invalid transition: {$e->getMessage()}");
    }
    
  2. State Resolution Issues: Verify state classes are registered and autoloaded. Run:

    composer dump-autoload
    
  3. Event Listener Conflicts: Ensure custom StateChanged events are properly bound:

    Event::listen(StateChanged::class, function ($event) {
        // Custom logic
    });
    

Extension Points

  1. Custom State Metadata: Store additional state-specific data using getMetadata():

    class Paid extends PaymentState
    {
        public function getMetadata(): array
        {
            return ['transaction_id' => $this->model->transaction_id];
        }
    }
    
  2. State-Specific Queries: Filter models by state using whereState():

    $paidPayments = Payment::whereState(Paid::class)->get();
    
  3. State Validation: Add state-specific validation rules:

    class Paid extends PaymentState
    {
        public function validate(): void
        {
            if ($this->model->amount <= 0) {
                throw new InvalidStateTransition("Amount must be positive.");
            }
        }
    }
    
  4. State Transitions with Callbacks: Use transitionTo() with callbacks for async operations:

    $payment->state->transitionTo(Paid::class, function () {
        dispatch(new ProcessPayment($payment));
    });
    
  5. State-Specific API Resources: Dynamically transform state data in API responses:

    public function toArray($request)
    {
        return array_merge(parent::toArray($request), [
            'state_color' => $this->state->color(),
        ]);
    }
    

Performance Tips

  1. Eager Load States: Avoid N+1 queries by eager loading state-dependent relationships:

    $payments = Payment::with(['state' => function ($query) {
        $query->select('state');
    }])->get();
    
  2. Cache State Configurations: Cache the StateConfig if performance is critical (e.g., in high-traffic apps):

    public static function config(): StateConfig
    {
        static $config;
        return $config ??= parent::config()
            ->default(Pending::class)
            ->allowTransition(Pending::class, Paid::class);
    }
    
  3. Batch State Updates: Use update() with a closure for bulk state transitions:

    Payment::where('user_id', $userId)
        ->update(['state' => Paid::class]);
    
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.
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope
anil/file-picker
broqit/fields-ai