31.07.2026・TechStuff
31.07.2026・TechStuff

Shopware 6 Hidden Gems #6: Two state machine tricks — diagrams for free and forcing „impossible“ transitions

Fabian Blechschmidt

Order states, delivery states, payment states — Shopware models all of them as state machines with defined transitions. Which is great, until a) a new colleague asks „so which states can an order actually be in?“ and you start drawing boxes on a whiteboard, or b) an order gets stuck in a state it can’t legally leave and you start writing UPDATE statements against the production database. I’ve done both. Both were unnecessary — the core has a tool for each, and neither made it into the docs beyond a bare mention.

Gem 1: state-machine:dump — workflow diagrams from your live shop

The command exists since forever, sitting quietly in the autogenerated command reference with no explanation. Its own --help text is more honest (System/StateMachine/Command/WorkflowDumpCommand.php):

The state-machine:dump command dumps the graphical representation of a
workflow in different formats

DOT:  bin/console state-machine:dump <state machine name> | dot -Tpng > workflow.png

It exports any state machine as Graphviz DOT. Pipe it through dot and you have a diagram:

Export of the order state workflow as a graphic. We have four states:
Offen, In Bearbeitung, Abgebrochen and Abgeschlossen. And the transitions in between: reopen (abgeschlossen or abgebrochen to open), cancel (offen or in Bearbeitung to abgebrochen), process (offen to in Bearbeitung) and complete (in Bearbeitung to Abgeschlossen)
bin/console state-machine:dump order.state | dot -Tpng > order.png
bin/console state-machine:dump order_delivery.state | dot -Tsvg > delivery.svg
bin/console state-machine:dump order_transaction.state | dot -Tpng > payment.png

The point that makes this more than a party trick: the dump comes from the database of the running shop. If a plugin added a custom state or transition (payment providers love doing this), it’s in the diagram. Your documentation can never drift from reality, because it is reality — regenerate it in CI and drop it into the project wiki. Onboarding question answered with one command instead of a whiteboard session.

Gem 2: FORCE_TRANSITION — when an order is stuck

Now the other scenario. An order sits in cancelled, the customer changed their mind, and the state machine says: no transition from cancelled to in_progress. The clean answer is „model a transition for that“. The real-world answer at 5pm on a Friday has historically involved SQL.

Here’s what the Flow Builder does in exactly this situation — its „Set order state“ action has a force transition checkbox, and this is the code behind it (Content/Flow/Dispatching/Action/SetOrderStateAction.php:75-92):

use Shopware\Core\Content\Flow\Dispatching\Action\SetOrderStateAction;

$context->addState(SetOrderStateAction::FORCE_TRANSITION);

try {
    $this->stateMachineRegistry->transition(
        new Transition('order', $orderId, 'in_progress', 'stateId'),
        $context
    );
} finally {
    $context->removeState(SetOrderStateAction::FORCE_TRANSITION);
}

The interesting part is how the registry handles it (System/StateMachine/StateMachineRegistry.php:278-291). Normally it searches for a transition whose from state matches the current state. With FORCE_TRANSITION set, if no legal transition matches, it falls back to interpreting your transition name as the target state’s technical name and jumps there directly. So you don’t force your way through an existing transition — you name the destination state and teleport.

Two things follow from that:

  1. You pass the state you want (in_progress), not a transition action (reopen). That’s also why the Flow action’s config field is literally the target state.
  2. Every guard and business rule attached to legal transitions is bypassed. The events for the state change still fire, but nobody checked whether the change makes sense.

So treat it like the emergency hammer it is: perfect for an order-recovery command or an admin-only „unstick this order“ button, dangerous as a default code path. But it beats UPDATE statements in every possible way — the state history stays consistent, events fire, and the code documents what happened.

And do note the finally — the core removes the state right after the transition, because FORCE_TRANSITION sticks to the context and would happily force every subsequent transition too.

TL;DR

bin/console state-machine:dump order.state | dot -Tpng > order.png renders any state machine — including plugin modifications — as an always-current diagram. And when an order is stuck: $context->addState(SetOrderStateAction::FORCE_TRANSITION) makes the next transition() call jump directly to a target state, bypassing the allowed-transitions check — same mechanism as the Flow Builder’s „force transition“ checkbox, minus the SQL crimes. Remove the state afterwards.

Next up: shopware.dal.max_rule_prices — the config limit that silently shows wrong prices in shops with too many price rules.

Questions or a stuck-order horror story of your own? Drop me an email!


Found in Shopware 6.7.0.0: System/StateMachine/Command/WorkflowDumpCommand.php (Graphviz dump), force fallback in System/StateMachine/StateMachineRegistry.php:278-291, flow action usage in Content/Flow/Dispatching/Action/SetOrderStateAction.php:27,75-92.