State Machine

In Sylius we are using the Winzou StateMachine Bundle. State Machines are an approach to handling changes occurring in the system frequently, that is extremely flexible and very well organised.

Every state machine will have a predefined set of states, that will be stored on an entity that is being controlled by it. These states will have a set of defined transitions between them, and a set of callbacks - a kind of events, that will happen on defined transitions.

States

States of a state machine are defined as constants on the model of an entity that the state machine is controlling.

How to configure states? Let’s see on the example from Checkout state machine.

# CoreBundle/Resources/config/app/state_machine/sylius_order_checkout.yml
winzou_state_machine:
    sylius_order_checkout:
        # list of all possible states:
        states:
            cart: ~
            addressed: ~
            shipping_selected: ~
            payment_selected: ~
            completed: ~

Transitions

On the graph it would be the connection between two states, defining that you can move from one state to another subsequently.

How to configure transitions? Let’s see on the example of our Checkout state machine. Having states configured we can have a transition between the cart state to the addressed state.

# CoreBundle/Resources/config/app/state_machine/sylius_order_checkout.yml
winzou_state_machine:
    sylius_order_checkout:
        transitions:
            address:
                from: [cart, addressed, shipping_selected, payment_selected]  # here you specify which state is the initial
                to: addressed                                                 # there you specify which state is final for that transition

Callbacks

Callbacks are used to execute some code before or after applying transitions. Winzou StateMachineBundle adds the ability to use Symfony services in the callbacks.

How to configure callbacks? Having a configured transition, you can attach a callback to it either before or after the transition. Callback is simply a method of a service you want to be executed.

# CoreBundle/Resources/config/app/state_machine/sylius_order_checkout.yml
winzou_state_machine:
     sylius_order_checkout:
          callbacks:
               # callbacks may be called before or after specified transitions, in the checkout state machine we've got callbacks only after transitions
               after:
                    sylius_process_cart:
                        on: ["address", "select_shipping", "select_payment"]
                        do: ["@sylius.order_processing.order_processor", "process"]
                        args: ["object"]

Configuration

In order to use a state machine, you have to define a graph beforehand. A graph is a definition of states, transitions and optionally callbacks - all attached on an object from your domain. Multiple graphs may be attached to the same object.

In Sylius the best example of a state machine is the one from checkout. It has five states available: cart, addressed, shipping_selected, payment_selected and completed - which can be achieved by applying some transitions to the entity. For example, when selecting a shipping method during the shipping step of checkout we should apply the select_shipping transition, and after that the state would become shipping_selected.

# CoreBundle/Resources/config/app/state_machine/sylius_order_checkout.yml
winzou_state_machine:
    sylius_order_checkout:
        class: "%sylius.model.order.class%" # class of the domain object - in our case Order
        property_path: checkoutState
        graph: sylius_order_checkout
        state_machine_class: "%sylius.state_machine.class%"
        # list of all possible states:
        states:
            cart: ~
            addressed: ~
            shipping_selected: ~
            payment_selected: ~
            completed: ~
        # list of all possible transitions:
        transitions:
            address:
                from: [cart, addressed, shipping_selected, payment_selected] # here you specify which state is the initial
                to: addressed                                                # there you specify which state is final for that transition
            select_shipping:
                from: [addressed, shipping_selected, payment_selected]
                to: shipping_selected
            select_payment:
                from: [payment_selected, shipping_selected]
                to: payment_selected
            complete:
                from: [payment_selected]
                to: completed
        # list of all callbacks:
        callbacks:
        # callbacks may be called before or after specified transitions, in the checkout state machine we've got callbacks only after transitions
            after:
                sylius_process_cart:
                    on: ["address", "select_shipping", "select_payment"]
                    do: ["@sylius.order_processing.order_processor", "process"]
                    args: ["object"]
                sylius_create_order:
                    on: ["complete"]
                    do: ["@sm.callback.cascade_transition", "apply"]
                    args: ["object", "event", "'create'", "'sylius_order'"]
                sylius_hold_inventory:
                    on: ["complete"]
                    do: ["@sylius.inventory.order_inventory_operator", "hold"]
                    args: ["object"]
                sylius_assign_token:
                    on: ["complete"]
                    do: ["@sylius.unique_id_based_order_token_assigner", "assignTokenValueIfNotSet"]
                    args: ["object"]