finite PHP Finite State Machine

repository·main·Indexed 23 days ago

https://github.com/yohang/finite

A lightweight, low-dependency PHP Finite State Machine library that uses PHP Enums to manage state transitions and business logic. It features PSR-14 event integration for guards and post-action logic, a Symfony bundle with Twig helpers, and a console command to export state machine graphs in Mermaid format.

Tokens
2.7K
Snippets
9
Records
15
Agent score
80%

What's inside finite

  1. Define states using PHP Enums

    main

    In Finite 2.0+, states are defined using a PHP Enum that implements the Finite\State interface. You must implement a static getTransitions() method that returns an array of Finite\Transition objects. Each Transition defines the name, the allowed source states (as an array), and the target state.

    enum DocumentState: string implements State
    {
        case DRAFT = 'draft';
        case PUBLISHED = 'published';
        case REPORTED = 'reported';
        case DISABLED = 'disabled';
    
        public static function getTransitions(): array
        {
            return [
                new Transition('publish', [self::DRAFT], self::PUBLISHED),
                new Transition('clear', [self::REPORTED, self::DISABLED], self::PUBLISHED),
                new Transition('report', [self::PUBLISHED], self::REPORTED),
                new Transition('disable', [self::REPORTED, self::PUBLISHED], self::DISABLED),
            ];
        }
    }
  2. Define a Stateful Object

    main

    To make an object stateful, it must have a property to hold the current state and corresponding getter/setter methods. The state property should be typed with your state Enum.

    class Document
    {
        private DocumentState $state = DocumentState::DRAFT;
    
        public function getState(): DocumentState
        {
            return $this->state;
        }
    
        public function setState(DocumentState $state): void
        {
            $this->state = $state;
        }
    }
  3. Add business logic to states via Enum methods

    main

    Instead of using metadata properties, Finite 2.0 encourages adding logic directly to your State Enum. Since states are Enums, you can define methods that return boolean values or other data based on the current state. This allows you to check business rules (like isDeletable()) directly on the state object without needing the StateMachine instance.

    enum DocumentState: string implements State
    {
        // ...
    
        public function isDeletable(): bool
        {
            return in_array($this, [self::DRAFT, self::DISABLED]);
        }
    
        public function isPrintable(): bool
        {
            return in_array($this, [self::PUBLISHED, self::REPORTED]);
        }
    }
    
    // Usage:
    var_dump($document->getState()->isDeletable());
  4. Integrate Finite with Symfony

    main

    To use Finite in a Symfony project, register the Finite\Extension\Symfony\Bundle\FiniteBundle in your config/bundles.php file. This provides the StateMachine service and Twig extensions automatically.

    return [
        // ...
        Finite\Extension\Symfony\Bundle\FiniteBundle::class => ['all' => true],
    ];
  5. How StateMachine manages transitions and events

    main

    The StateMachine acts as the orchestrator for state changes. The lifecycle of a transition follows this sequence:

    1. Validation: can() checks if the transition is valid for the current state and if any CanTransitionEvent listeners block it.
    2. Pre-Transition: apply() dispatches a PreTransitionEvent.
    3. Processing: The transition's process() method is called to execute custom business logic.
    4. State Update: The object's state property is updated to the transition's target state.
    5. Post-Transition: A PostTransitionEvent is dispatched.

    This lifecycle ensures that business logic and side effects can be hooked into at every critical stage.

  6. Use Events and Guards to control transitions

    main

    Finite dispatches PSR-14 events. You can use the dispatcher to implement Guards (to block transitions dynamically) or Post-Action logic (to trigger side effects after a transition completes).

    Key events:

    • Finite\Event\CanTransitionEvent: Use $event->blockTransition() to prevent a transition.
    • Finite\Event\PostTransitionEvent: Triggered after a transition is successful.
    use Finite\Event\CanTransitionEvent;
    use Finite\Event\PostTransitionEvent;
    
    $dispatcher = $stateMachine->getDispatcher();
    
    // 1. Guard: Prevent a transition dynamically
    $dispatcher->addEventListener(CanTransitionEvent::class, function (CanTransitionEvent $event) {
        if ('publish' === $event->getTransition()->getName() && !$event->getObject()->title) {
            // Block the transition if the title is empty
            $event->blockTransition(); 
        }
    });
    
    // 2. Post-Action: Do something after a transition
    $dispatcher->addEventListener(PostTransitionEvent::class, function (PostTransitionEvent $event) {
        // e.g. Send an email, log activity...
        echo 'Transition ' . $event->getTransition()->getName() . ' completed!';
    });
  7. Use the StateMachine to manage transitions

    main

    Initialize a Finite\StateMachine and pass your stateful object to its methods. Use can($object, 'transition_name') to check if a transition is valid for the current state, and apply($object, 'transition_name') to execute the transition and update the object's state.

    use Finite\StateMachine;
    
    $document = new Document;
    $sm = new StateMachine;
    
    // Can we process a transition ?
    $sm->can($document, 'publish');
    
    // Apply a transition
    $sm->apply($document, 'publish'); 
  8. Use Finite Twig helpers

    main

    Once the Symfony bundle is installed, you can use these Twig helpers:

    • finite_can(object, 'transition_name'): Returns true if the transition is available for the object.
    • finite_reachable_transitions(object): Returns a list of all reachable transitions for the object.
    {# Check if a transition is available #}
    {% if finite_can(document, 'publish') %}
        <a href="...">Publish</a>
    {% endif %}
    
    {# List all reachable transitions #}
    <ul>
        {% for transition in finite_reachable_transitions(document) %}
            <li>{{ transition.name }}</li>
        {% endfor %}
    </ul>
  9. Visualize the state machine graph via Symfony Console

    main

    You can dump your state machine graph in Mermaid format using the Symfony console command. Replace App\State\DocumentState with your actual state Enum class name.

    php bin/console finite:state-machine:dump "App\State\DocumentState" mermaid
  10. Initialize the StateMachine

    main

    The StateMachine constructor accepts two optional dependencies:

    1. EventDispatcherInterface $dispatcher: An implementation of the PSR-14 event dispatcher. Defaults to a new Finite\Event\EventDispatcher.
    2. StatePropertyExtractor $statePropertyExtractor: An extractor used to locate the state property on the target object. Defaults to MemoizedStatePropertyExtractor.
    use Finite\StateMachine;
    use Finite\Event\EventDispatcher;
    
    $stateMachine = new StateMachine(new EventDispatcher());
  11. List all available transitions with StateMachine::getReachablesTransitions()

    main
    Use getReachablesTransitions() to retrieve an array of all TransitionInterface objects that are currently valid for the object's current state. This is useful for determining which actions a user is permitted to take in the current UI context.