Finite icon indicating copy to clipboard operation
Finite copied to clipboard

How to start state machine with normal state coming from database?

Open somnathmuluk opened this issue 8 years ago • 12 comments

Is there any example which shows state machine starting with normal state?

Transitions can be affected by different users. I need to save all transitions in database. And state machine can be started with normal state (mid-level state). Any example how to save state in database and start with same state and then apply transition?

somnathmuluk avatar Mar 31 '16 12:03 somnathmuluk

I use an event listener which will store transition information in a history table, something like this

$stateMachine->getDispatcher()
    ->addListener(FiniteEvents::PRE_TRANSITION, function (TransitionEvent $event) {
        $history = (new TransitionHistory)
            ->setFrom($status->findByName($event->getInitialState()->getName()))
            ->setTo($status->findByName($event->getTransition()->getState()));

        $repository->create($history);
    });

This will be run before every transition.

State machine will always start with the state your object has when the machine is initialized. Just make sure your object has the appropriate state when you set it in your state machine.

realshadow avatar Apr 06 '16 19:04 realshadow

@realshadow : What is $repository Object?

Is there any simple method or logic by that we can start with normal state easily?

Like setCurrentState('StateName') method?

somnathmuluk avatar Apr 11 '16 11:04 somnathmuluk

@somnathmuluk I do not perfectly understand your request. What are you trying to do ? Persisting your state & transition graph in database ? Or just the inital state ?

yohang avatar Apr 17 '16 15:04 yohang

@yohang I just want to start State Machine with Normal State rather than initial state. And in database I am saving current state (i.e Normal state). So I wanted to check if any other state can be transited or not from normal state.

somnathmuluk avatar Apr 18 '16 07:04 somnathmuluk

@somnathmuluk Oh, I see. This is the "normal" use of Finite. If your stateful object has the state set before the state machine initialization, it'll work !

The common use of Finite is with Doctrine Entities / Document, which does this transparently at entity retrieving.

yohang avatar Apr 18 '16 07:04 yohang

Yohan, but tell me please, can I somehow specify the state with which I can initialize FSM without saving the whole object in the database, but only the name of the state? The object is very large in my opinion to be constantly transmitted over the network, but the name of the state is not. It would be ideal, as wrote @somnathmuluk here such method as setCurrentState ('StateName'). Thanks in advance.

roman7722 avatar Nov 02 '17 17:11 roman7722

now I make a class extend the StateMachine;

class StateMachineEx extends StateMachine
{

    /**
     * @param $state
     * @throws \Finite\Exception\TransitionException
     */
    public function setState($state)
    {
        $this->currentState = $this->getState($state);
    }
}

now $smEx->setState($stateName) is support

tao996 avatar Apr 17 '18 14:04 tao996

The current state is a string, so if you can save the string to your database, then when you set up your state machine you first load up the value from the database, then use the setFiniteState() function on your class. Adapting the example at https://github.com/yohang/Finite/blob/master/examples/basic-graph.php:

$initialBookingState = 'proposed'; // load this from the database

$document = new Document();
$loader = new \Finite\Loader\ArrayLoader([
    'class' => 'Document',
    'states' => [
        'draft' => [
            'type' => \Finite\State\StateInterface::TYPE_INITIAL,
            'properties' => ['deletable' => true, 'editable' => true],
        ],
        'proposed' => [
            'type' => \Finite\State\StateInterface::TYPE_NORMAL,
            'properties' => [],
        ],
        'accepted' => [
            'type' => \Finite\State\StateInterface::TYPE_FINAL,
            'properties' => ['printable' => true],
        ],
    ],
    'transitions' => [
        'propose' => ['from' => ['draft'], 'to' => 'proposed'],
        'accept' => ['from' => ['proposed'], 'to' => 'accepted'],
        'reject' => ['from' => ['proposed'], 'to' => 'draft'],
    ],
]);

$document->setFiniteState($initialBookingState); // set our initial state here

$stateMachine = new \Finite\StateMachine\StateMachine($document);
$loader->load($stateMachine);
$stateMachine->initialize();

// Current state
var_dump($stateMachine->getCurrentState()->getName()); // proposed

BurningDog avatar Nov 12 '20 10:11 BurningDog

@BurningDog How do you update the document during transitions? The loading of the state is only a half of the answer. (Very nice example, btw.)

jkufner avatar Nov 12 '20 15:11 jkufner

@jkufner I wrap the state machine transition in a service, and call the service to do the transition. If the transition succeeds, then I update the database by calling $myEntity->setState().

StateMachineService::applyTransitionIfPossible($myEntity, 'my custom transition');

class StateMachineService
{
    /**
     * Attempts to apply the transition. If we can't, don't fail but return null.
     */
    public static function applyTransitionIfPossible(MyEntity $myEntity, string $transition)
    {
        $stateMachine = $myEntity->getStateMachine();
        if ($stateMachine->can($transition)) {
            $stateMachine->apply($transition);
            $myEntity->setBookingState($stateMachine->getCurrentState()->getName());
        }
    }
}

class MyEntity
{
    // This entity is the one I care about knowing the state of by using the state machine.
    // It saves the state in the database
    private ?string $state = null;
    private ?StateMachine $stateMachine = null;

    // All sorts of other code
    // ...
  
    public function getState(): ?string
    {
        return $this->state;
    }

    /**
     * Sets the new State. If it doesn't match the current state of the state machine, throw an error.
     *
     * @throws Exception
     */
    public function setState(string $newState): self
    {
        if ($newState !== $this->getStateMachine()->getCurrentState()->getName()) {
            throw new \Exception('The new state does not match the current state machine state');
        }

        $this->state = $newState;

        return $this;
    }

    public function getStateMachine(): StateMachine
    {
        if (!$this->stateMachine) {
            $this->stateMachine = StateMachineService::createStateMachine($this, $this->state);
        }

        return $this->stateMachine;
    }
}

In StateMachineService::createStateMachine() I pretty much do the same code as a I previously posted here: https://github.com/yohang/Finite/issues/114#issuecomment-725978111

BurningDog avatar Nov 13 '20 11:11 BurningDog

@BurningDog Thank you. So if I understand correctly, there is no encapsulation of the entity and the state machine to enforce the modelled behavior and deny any other modifications.

jkufner avatar Nov 20 '20 17:11 jkufner

@jkufner not quite. Finite provides a state machine but doesn't provide any implementation of persisting to the database - that's left to the developer to implement. When instantiating the state machine, you can set any initial state, but once that's done you can't transition to any arbitrary state - Finite enforces the transition rules.

When saving the state to the database, you can implement that however you want to, but it's independent of Finite. In my implementation in Symfony 4.4 I've needed an entity (for database persistence) and a service (to link the entity state with the state machine state, and a few other helper functions).

Other state machine libraries have wrappers for a particular framework. For instance, https://github.com/winzou/state-machine can be used in Laravel with https://github.com/sebdesign/laravel-state-machine and https://github.com/iben12/laravel-statable

BurningDog avatar Nov 21 '20 09:11 BurningDog