Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
somnathmuluk opened this issue Mar 31, 2016 · 12 comments
Open

Comments

@somnathmuluk
Copy link

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?

@realshadow
Copy link
Contributor

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.

@somnathmuluk
Copy link
Author

@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?

@yohang
Copy link
Owner

yohang commented Apr 17, 2016

@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 ?

@somnathmuluk
Copy link
Author

@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.

@yohang
Copy link
Owner

yohang commented Apr 18, 2016

@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.

@roman7722
Copy link

roman7722 commented Nov 2, 2017

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.

@tao996
Copy link

tao996 commented Apr 17, 2018

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

@BurningDog
Copy link

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

@jkufner
Copy link

jkufner commented Nov 12, 2020

@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.)

@BurningDog
Copy link

BurningDog commented Nov 13, 2020

@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: #114 (comment)

@jkufner
Copy link

jkufner commented Nov 20, 2020

@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.

@BurningDog
Copy link

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants