Advanced Features
Create Multiple Transitions
You can create multiple transitions from different states to a single state at once:
use ByJG\StateMachine\TransitionConditionInterface;
$condition = new class implements TransitionConditionInterface {
public function canTransition(?array $data): bool {
return isset($data["approved"]) && $data["approved"] === true;
}
};
$transitions = Transition::createMultiple(
[OrderState::Draft, OrderState::Rejected],
OrderState::Review,
$condition
);
$machine = FiniteStateMachine::createMachine(OrderState::class)
->addTransitions($transitions);
This is useful when multiple states can transition to the same destination state under the same conditions.
Naming a Transition
A pair of states usually identifies a move, so most transitions need no name. Sometimes it does
not: an order goes from DRAFT to PAID by PIX, by card or by bank transfer, and those are three
moves with three conditions and three side effects. Collapsing them into one transition would lose
exactly what distinguishes them.
There is still one PAID state — a state is a case of the enum and cannot be duplicated. What is
duplicated is the arrow, and the name is what tells one arrow from the other:
$stateMachine = FiniteStateMachine::createMachine(OrderState::class)
->addTransition(Transition::named('PIX', OrderState::Draft, OrderState::Paid, new PaidBy('PIX'), $confirmPix))
->addTransition(Transition::named('CARD', OrderState::Draft, OrderState::Paid, new PaidBy('CARD'), $capturePreAuth))
->addTransition(Transition::named('ETF', OrderState::Draft, OrderState::Paid, new PaidBy('ETF'), $reconcile));
autoTransitionFrom() needs no help: it evaluates the conditions of every move leaving the state,
and the routes happening to share a destination changes nothing.
$paid = $stateMachine->autoTransitionFrom(OrderState::Draft, ['method' => 'CARD']);
$paid->getState(); // 'PAID'
$paid->getTransitionName(); // 'CARD' <- which route got you here
$paid->process(); // runs $capturePreAuth, and only that
getTransitionName() is worth persisting next to the state. PAID alone does not record how the
money arrived; PAID plus CARD does.
In a definition file, name is one more optional key:
transitions:
- name: PIX
from: DRAFT
to: PAID
condition: 'App\Fsm\PaidByPix'
action: 'App\Fsm\ConfirmPix'
- name: CARD
from: DRAFT
to: PAID
condition: 'App\Fsm\PaidByCard'
action: 'App\Fsm\CapturePreAuth'
Naming an explicit move
Once two states are joined more than once, the pair alone no longer identifies a move, so
getTransition(), canTransition() and transition() take the name as a further argument:
$stateMachine->canTransition(OrderState::Draft, OrderState::Paid, $data, 'PIX');
$paid = $stateMachine->transition(OrderState::Draft, OrderState::Paid, $data, 'PIX');
The name is what picks the arrow, and the arrow is what decides which condition is asked and which action ends up running:
Drop the 'PIX' and step 1 has no answer — three arrows join those two states, so the machine
raises rather than guessing.
Asking without a name while several exist is a question with no answer, and is reported as one:
$stateMachine->getTransition(OrderState::Draft, OrderState::Paid);
// TransitionException: There is more than one transition from DRAFT to PAID: PIX, CARD, ETF.
// Name the one you mean.
While a pair of states is joined only once — the common case — the name stays optional everywhere.
Names are compared uppercased, like state names, so 'pix' and 'PIX' are the same route. Two
moves between the same two states must differ by name; two unnamed moves between the same two
states are still a declaration error, because nothing tells them apart.
Get Possible Transitions
Get all possible transitions from a specific state:
$possibleTransitions = $stateMachine->possibleTransitions(OrderState::Draft);
This returns an array of Transition objects representing all valid transitions from the given state.
Get State Object
Retrieve the State object a reference names:
$state = $stateMachine->state(OrderState::OutOfStock);
$state = $stateMachine->state('OUT_OF_STOCK'); // the same state
This cannot fail: every case of the enum is a state of the machine, and a reference that names
no case raises a TransitionException. There is no "does this state exist" question left to ask.
State names are compared uppercased, so 'out_of_stock' and 'OUT_OF_STOCK' name the same
state — and so does an enum whose case values are not uppercase.
Get Specific Transition
Get a specific transition between two states:
$transition = $stateMachine->getTransition(OrderState::Draft, OrderState::Review);
// ...or, when several moves join that pair
$transition = $stateMachine->getTransition(OrderState::Draft, OrderState::Paid, 'PIX');
Returns the Transition object if it exists, or null otherwise. Throws if the pair is joined by
more than one move and no name was given — see Naming a Transition.
A transition hands back what hangs on it: getTransitionCondition() and getTransitionAction()
return the guard and the side effect, or null when there is none. Conditions are free of side
effects, so asking the guard again is a safe way to explain a refusal:
$transition = $stateMachine->getTransition(OrderState::Draft, OrderState::Review);
if ($transition === null) {
$reason = "no arrow DRAFT -> REVIEW";
} elseif (!$transition->runTransitionFunction($data)) {
$reason = get_class($transition->getTransitionCondition()) . " said no";
}