winzou/state-machine

repository·master·Indexed 19 days ago

https://github.com/winzou/state-machine

A lightweight PHP state machine for defining states, transitions, and lifecycle callbacks for domain objects. It provides a Factory for instantiating state machines, a TransitionEvent for intercepting state changes, and a Twig extension (SMExtension) for accessing state logic within templates. The library supports guard, before, and after callbacks, and integrates with Symfony Expression Language for argument evaluation.

Tokens
4.5K
Snippets
12
Records
14
Agent score
67%

What's inside winzou/state-machine

  1. How callbacks work in the state machine

    master

    Callbacks allow you to intercept transitions to validate or execute logic. They are divided into three types:

    1. guard: Used to prevent a transition. The callback function must return a bool. If it returns false, the transition is blocked.
      • Use to to trigger when moving to a specific state.
    2. before: Executed before a transition occurs.
      • Use from to trigger when leaving a specific state.
    3. after: Executed after a transition is applied.
      • Use on to trigger for a specific transition name.
      • Use to to trigger when entering a specific state.

    Callbacks can be defined as anonymous functions or as an array ['object', 'methodName'] to call a method on the domain object.

  2. Use the state machine to manipulate objects

    master

    The state machine is the engine that manipulates your domain object. A state machine instance is specific to a unique combination of an object and a graph. If you want to use a different graph on the same object, or a different object with the same graph, you must instantiate a new state machine.

    To obtain a state machine instance, use a Factory. You provide the domain object and the graph name, and the factory returns the state machine for that specific pair. This allows you to:

    • Test if a transition can be applied.
    • Apply a transition.
    • Retrieve the current state of the object.
  3. Configure callbacks for transitions

    master

    You can define callbacks in your configuration array to execute logic at specific points during a transition. Callbacks are triggered by the apply() method.

    Callback Positions:

    • guard: Executed during the can() check. If any guard returns false, the transition is blocked.
    • before: Executed after the PRE_TRANSITION event but before the state is actually changed.
    • after: Executed after the state has been updated on the object.

    Configuration Example:

    $config = [
        'graph' => 'example',
        'states' => ['idle', 'running'],
        'transitions' => [
            'start' => ['from' => ['idle'], 'to' => 'running'],
        ],
        'callbacks' => [
            'guard' => [
                [$myService, 'checkPermissions'], // Method call
                'some_function_name',             // Function name
            ],
            'before' => [
                function ($event) { /* logic */ }, // Closure
            ],
            'after' => [
                // ...
            ],
        ],
    ];

    Note: Callbacks receive a TransitionEvent object as an argument.

    $config = [
        'graph' => 'example',
        'states' => ['idle', 'running'],
        'transitions' => [
            'start' => ['from' => ['idle'], 'to' => 'running'],
        ],
        'callbacks' => [
            'guard' => [
                [$myService, 'checkPermissions'],
            ],
            'before' => [
                function ($event) { /* logic */ },
            ],
            'after' => [
                // ...
            ],
        ],
    ];
  4. Configure a state machine graph

    master

    A graph is a definition of states, transitions, and optional callbacks attached to a domain object. You define a graph using an associative array with the following keys:

    • graph: The unique name of the current graph (multiple graphs can be attached to the same object).
    • property_path: The property path on your domain object that holds the current state.
    • states: An array of all possible states.
    • transitions: An array defining how to move between states. Each transition requires a from (array of starting states) and a to (target state).
    • callbacks: Logic to execute during state changes. Callbacks are categorized into guard, before, and after.
    $config = array(
        'graph'         => 'myGraphA',
        'property_path' => 'stateA',
        'states'        => array(
            'checkout',
            'pending',
            'confirmed',
            'cancelled'
        ),
        'transitions' => array(
            'create' => array(
                'from' => array('checkout'),
                'to'   => 'pending'
            ),
            'confirm' => array(
                'from' => array('checkout', 'pending'),
                'to'   => 'confirmed'
            ),
            'cancel' => array(
                'from' => array('confirmed'),
                'to'   => 'cancelled'
            )
        ),
        'callbacks' => array(
            'guard' => array(
                'guard-cancel' => array(
                    'to' => array('cancelled'),
                    'do' => function() { return false; }
                )
            ),
            'before' => array(
                'from-checkout' => array(
                    'from' => array('checkout'),
                    'do'   => function() { /* logic */ }
                )
            ),
            'after' => array(
                'on-confirm' => array(
                    'on' => array('confirm'),
                    'do' => function() { /* logic */ }
                ),
                'to-cancelled' => array(
                    'to' => array('cancelled'),
                    'do' => function() { /* logic */ }
                ),
                'cancel-date' => array(
                    'to' => array('cancelled'),
                    'do' => array('object', 'setCancelled'),
                ),
            )
        )
    );
  5. Define custom callbacks with the Callback class

    master

    The SM\Callback\Callback class allows you to define logic that executes during state transitions based on specific conditions. You can specify when a callback should run using several clauses and define which arguments should be passed to the callable.

    Specification Clauses

    When constructing a Callback, you can provide a $specs array containing:

    • on: The transition name(s) this callback applies to.
    • from: The source state(s) this callback applies to.
    • to: The target state(s) this callback applies to.
    • excluded_on: Transitions to ignore.
    • excluded_from: Source states to ignore.
    • excluded_to: Target states to ignore.
    • args: An array of arguments to pass to the callable. These can be literal values or Symfony Expression Language strings. If args is not provided, the TransitionEvent is passed as the sole argument.

    Callable Types

    1. Standard Callable: A standard PHP callable (e.g., a closure or [$instance, 'method']).
    2. Object-bound Callable: An array where the first element is a property path starting with object. This allows you to target a specific property of the state machine's object. For example, ['object.user', function($user) { ... }] will pass the user property of the state machine's object to the function.

    Argument Evaluation

    If args are provided as strings, they are evaluated using Symfony's ExpressionLanguage. The evaluation context includes:

    • object: The object being managed by the state machine.
    • event: The current TransitionEvent.
    use SM\Callback\Callback;
    
    // Example: A callback that runs only when transitioning 'to' the 'published' state
    $callback = new Callback(
        [
            'to' => 'published',
            'args' => ['object.id', 'event.getTransition().getName()']
        ],
        function ($id, $transitionName) {
            echo "Transitioning object $id via $transitionName";
        }
    );
  6. Apply a transition with apply()

    master

    Use the apply(string $transition, bool $soft = false) method to execute a state change.

    Behavior:

    • If the transition is valid, the state machine updates the underlying object's state property.
    • If $soft is set to true and the transition is invalid, the method returns false instead of throwing an SMException.
    • If $soft is false (default) and the transition is invalid, an SMException is thrown.

    Lifecycle of an apply() call:

    1. can() check.
    2. Dispatch SMEvents::PRE_TRANSITION event.
    3. Execute before callbacks.
    4. Update the object's state.
    5. Execute after callbacks.
    6. Dispatch SMEvents::POST_TRANSITION event.

    Returns true on success.

    // Throws exception if invalid
    $stateMachine->apply('pay');
    
    // Returns false if invalid
    if ($stateMachine->apply('pay', true)) {
        // Success
    }
  7. Retrieve current state and transitions

    master

    The StateMachine provides methods to inspect the current state and available actions:

    • getState(): Returns the current state string from the underlying object using the configured property_path.
    • getPossibleTransitions(): Returns an array of all transition names that are currently valid (i.e., where can($transition) would return true).
    • getGraph(): Returns the name of the graph defined in the configuration.
    • getObject(): Returns the underlying object being managed.
    echo "Current state: " . $stateMachine->getState() . "\n";
    
    echo "Available actions: " . implode(', ', $stateMachine->getPossibleTransitions()) . "\n";
  8. Instantiate state machines with the Factory class

    master

    The SM\Factory\Factory class is used to create state machine instances for specific objects based on a provided configuration array.

    When instantiating the factory, you can optionally provide a Symfony EventDispatcherInterface to handle state machine events and a CallbackFactoryInterface to manage callbacks.

    By default, the factory uses SM\StateMachine\StateMachine. However, you can specify a custom state machine class in your configuration using the state_machine_class key. If the specified class does not exist, an SM\SMException will be thrown.

    use SM\Factory\Factory;
    use Symfony\Component\EventDispatcher\EventDispatcher;
    
    // $configs is an array of state machine configurations
    $factory = new Factory($configs, new EventDispatcher());
    
    // To use a custom state machine class, include it in the config:
    // $configs = [
    //    'my_machine' => [
    //        'state_machine_class' => 'App\MyCustomStateMachine',
    //        // ... other config
    //    ]
    // ];
    
    $stateMachine = $factory->create($myObject, 'my_machine');
  9. Use the Twig extension for state machine logic

    master

    The SMExtension class allows you to access state machine information directly within Twig templates. It requires a FactoryInterface instance to resolve the state machine for a given object.

    Once registered as a Twig extension, you can use three global functions in your templates to check transitions, retrieve the current state, or list available transitions.

    {# Check if a transition is possible #}
    {% if sm_can(my_object, 'approve') %}
        <button>Approve</button>
    {% endif %}
    
    {# Get the current state #}
    <p>Current status: {{ sm_state(my_object) }}</p>
    
    {# List all possible transitions #}
    <ul>
    {% for transition in sm_possible_transitions(my_object) %}
        <li>{{ transition }}</li>
    {% endfor %}
    </ul>
  10. Initialize the StateMachine

    master

    To use the state machine, instantiate the StateMachine class by providing the object you want to manage, a configuration array defining the graph, and optionally an event dispatcher or a custom callback factory.

    By default, the state machine looks for a property named state on your object to track its current status. You can override this by providing a property_path in the configuration.

    Constructor Parameters:

    • $object: The underlying object being managed.
    • $config: An array containing the graph definition (must include graph, states, and transitions).
    • $dispatcher (optional): An implementation of EventDispatcherInterface to allow listening to state machine events.
    • $callbackFactory (optional): An implementation of CallbackFactoryInterface to handle custom callbacks.
    use SM//StateMachine\StateMachine;
    
    $config = [
        'graph' => 'order_process',
        'states' => ['new', 'paid', 'shipped'],
        'transitions' => [
            'pay' => ['from' => ['new'], 'to' => 'paid'],
        ],
        'property_path' => 'status', // Use 'status' instead of 'state'
    ];
    
    $stateMachine = new StateMachine($order, $config);
  11. Handle state transitions with TransitionEvent

    master

    The TransitionEvent is dispatched during a state transition. You can listen to this event to inspect the transition details or to prevent the transition from occurring by calling setRejected(true).

    Key properties available in the event:

    • getTransition(): Returns the name of the transition being applied.
    • getState(): Returns the state from which the transition is being applied (the 'from' state).
    • getConfig(): Returns the configuration array associated with the transition.
    • getStateMachine(): Returns the StateMachineInterface instance performing the transition.
    • isRejected(): Returns whether the transition has been marked for rejection.
    • setRejected(bool $reject): Marks the transition as rejected, preventing it from completing.
    use SM//Event//TransitionEvent;
    
    // Inside an event listener:
    public function onTransition(TransitionEvent $event)
    {
        if ($event->getTransition() === 'publish' && $event->getState() === 'draft') {
            // Perform logic or prevent transition
            if ($someConditionFails) {
                $event->setRejected();
            }
        }
    }