spatie/laravel-model-states

repository·main·Indexed 23 days ago

https://github.com/spatie/laravel-model-states

A package that adds state machine and state pattern capabilities to Laravel Eloquent models. It allows developers to represent complex model states as dedicated classes with controlled transitions, automatic database serialization, and built-in query scopes for filtering records by state. It supports PHP 7.4+ and Laravel 8.0+, offering features like the ValidStateRule for request validation and configuration via the config() method or PHP 8 attributes.

Tokens
9.5K
Snippets
40
Records
58
Agent score
80%

What's inside laravel-model-states

  1. What is laravel-model-states?

    main
    The laravel-model-states package adds state support to Eloquent models by combining the State pattern and State Machines. It allows you to represent different states of a model as separate classes, handles the serialization of these states to the database automatically, and provides a controlled way to manage state transitions.
  2. Best practices for side effects in transitions

    main
    When designing transitions, avoid injecting too many dependencies. A high number of injected dependencies often indicates that the transition is performing too many side effects. In such cases, consider refactoring the logic to use an event-based system instead of handling complex side effects directly within the transition class.
  3. Organize state files for automatic resolution

    main

    If you use custom names for your states, you must ensure the package can resolve those names back into class instances when loading from the database.

    To enable automatic resolution, keep your abstract state class and all its concrete implementations in the same directory. The abstract class will automatically detect all relevant states within its directory.

    States/
      ├── Failed.php
      ├── Paid.php
      ├── PaymentState.php // This abstract class will automatically detect all relevant states within this directory.
      └── Pending.php
  4. Define states using an abstract State class

    main

    States are represented by classes that extend Spatie\ModelStates\State. You should create an abstract base state class for your model to define common logic and configuration.

    In the abstract class, use the config() method to return a StateConfig object. This allows you to:

    • Set a default() state.
    • Define allowed transitions using allowTransition(fromClass, toClass).

    Example:

    use Spatie\ModelStates\State;
    use Spatie\ModelStates\StateConfig;
    
    abstract class PaymentState extends State
    {
        abstract public function color(): string;
        
        public static function config(): StateConfig
        {
            return parent::config()
                ->default(Pending::class)
                ->allowTransition(Pending::class, Paid::class)
                ->allowTransition(Pending::class, Failed::class)
            ;
        }
    }
  5. Validate state values in requests using ValidStateRule

    main

    Use the ValidStateRule to ensure that incoming request data matches a valid implementation of a specific state class. This prevents invalid state transitions or values from being processed by your application.

    To use it, pass the state class name (e.g., PaymentState::class) to the constructor of ValidStateRule within a Laravel validation array.

    use Spatie\ModelStates\Validation\ValidStateRule;
    
    request()->validate([
        'state' => new ValidStateRule(PaymentState::class),
    ]);
  6. Extend DefaultTransition to pass custom data to event listeners

    main

    If you need to pass additional contextual data to all StateChanged event listeners, you can extend the Spatie\ModelStates\DefaultTransition class. This allows you to include custom properties in the transition object that is passed to your listeners.

    Important Requirements:

    • Custom parameters are only accessible within the context of event listeners.
    • All custom parameters must be serializable if you intend to use queued state change listeners.
    use Spatie\ModelStates\DefaultTransition;
    use Spatie\ModelStates\State;
    
    class CustomDefaultTransitionWithAttributes extends DefaultTransition
    {
        public function __construct($model, string $field, State $newState, public bool $silent = false)
        {
            parent::__construct($model, $field, $newState);
        }
    }
  7. Allow null state values with ValidStateRule

    main

    If the state field in your request is optional or can be null, use the make() method on ValidStateRule and chain the nullable() method. This allows the validation to pass if the value is null, while still enforcing valid state values if a value is provided.

    use Spatie\ModelStates\Validation\ValidStateRule;
    
    request()->validate([
        'state' => ValidStateRule::make(PaymentState::class)->nullable(),
    ]);
  8. Register custom transition classes in your StateConfig

    main

    To use a custom transition class, you must register it in your state's configuration using the allowTransition() method within the config() method of your State class. This maps a specific source state and target state to your custom transition class.

    abstract class PaymentState extends State
    {
        // …
    
        public static function config(): StateConfig
        {
            return parent::config()
                ->allowTransition(Pending::class, Failed::class, PendingToFailed::class);
        }
    }
  9. Configure state transitions

    main

    Transitions define how a model can move from one state to another. You configure these rules within the config() method of your state classes, which should return a StateConfig object.

    Transitions can be simple (defining a source and destination state) or custom (providing a specific transition class to handle side effects or complex logic).

    abstract class PaymentState extends State
    {
        public static function config(): StateConfig
        {
            return parent::config()
                ->allowTransition(Pending::class, Paid::class)
                ->allowTransition(Pending::class, Failed::class, PendingToFailed::class);
        }
    }
  10. Allow multiple source states for a single destination

    main

    If several different states are allowed to transition to the same destination state, you can pass an array of source states as the first argument to allowTransition().

    abstract class PaymentState extends State
    {
        public static function config(): StateConfig
        {
            return parent::config()
                ->allowTransition([Created::class, Pending::class], Failed::class, ToFailed::class);
        }
    }