Hierarchical States
Hierarchical (nested/compound) states allow you to organize complex state machines with parent-child relationships. Child states inherit transitions from their parents.
Basic Structure
··· 1 hidden line
MachineDefinition::define(
config: [
'initial' => 'active',
'states' => [
'active' => [
'initial' => 'idle',
'states' => [
'idle' => [
'on' => ['START' => 'running'],
],
'running' => [
'on' => ['PAUSE' => 'idle'],
],
],
'on' => [
'STOP' => 'inactive', // Available from any child
],
],
'inactive' => [],
],
],
);State Types
Atomic States
Leaf states with no children:
'idle' => [
'on' => ['START' => 'running'],
],Compound States
Parent states with children:
'active' => [
'initial' => 'idle', // Required for compound states
'states' => [
'idle' => [...],
'running' => [...],
],
],Final States
Terminal states:
'completed' => [
'type' => 'final',
],Transition Inheritance
Child states inherit transitions from ancestors:
'active' => [
'states' => [
'loading' => [...],
'ready' => [...],
],
'on' => [
'RESET' => 'inactive', // Available from loading AND ready
'ERROR' => 'error', // Available from any child
],
],Order of Precedence
Child transitions take priority over parent transitions:
'active' => [
'on' => ['SUBMIT' => 'submitted'], // Default
'states' => [
'editing' => [
'on' => ['SUBMIT' => 'validating'], // Overrides parent
],
'viewing' => [], // Uses parent SUBMIT -> submitted
],
],Deep Nesting
'order' => [
'initial' => 'draft',
'states' => [
'draft' => [...],
'review' => [
'initial' => 'pending',
'states' => [
'pending' => [
'on' => ['APPROVE' => 'approved'],
],
'approved' => [
'on' => ['@always' => '#processing'],
],
],
],
'processing' => [
'initial' => 'validating',
'states' => [
'validating' => [
'on' => ['VALID' => 'charging'],
],
'charging' => [
'on' => ['CHARGED' => 'shipping'],
],
'shipping' => [
'on' => ['SHIPPED' => '#completed'],
],
],
],
'completed' => ['type' => 'final'],
],
],State IDs and Targeting
Automatic ID Generation
// Given this structure:
'id' => 'order',
'states' => [
'processing' => [
'states' => [
'validating' => [],
],
],
],
// State IDs are:
// 'order.processing'
// 'order.processing.validating'Targeting Nested States
// Target using full path
'on' => [
'SKIP_VALIDATION' => 'processing.charging',
],
// Target using ID selector (#)
'on' => [
'SKIP_VALIDATION' => '#processing.charging',
],Breaking Out of Nesting
Use # to target any state by ID:
'review' => [
'states' => [
'approved' => [
'on' => [
'@always' => '#completed', // Jump to root-level completed
],
],
],
],
'completed' => ['type' => 'final'],Initial States
Compound states must specify an initial child:
'processing' => [
'initial' => 'validating', // Required
'states' => [
'validating' => [],
'charging' => [],
],
],When entering processing, it automatically enters validating.
Entry/Exit Actions
Actions execute at each level:
'order' => [
'entry' => 'logOrderEntryAction', // Runs when entering order
'exit' => 'logOrderExitAction', // Runs when leaving order
'states' => [
'processing' => [
'entry' => 'startProcessingAction', // Runs when entering processing
'exit' => 'stopProcessingAction', // Runs when leaving processing
'states' => [
'validating' => [
'entry' => 'startValidationAction',
'on' => ['VALID' => 'charging'],
],
],
],
],
],Execution Order
When entering order.processing.validating:
logOrderEntry(order entry)startProcessing(processing entry)startValidation(validating entry)
When leaving from validating to a sibling:
- Actions on validating exit (if any)
- (No exit on processing - staying within)
When leaving from validating to outside order:
- Validating exit
stopProcessinglogOrderExit
Practical Example
Multi-Step Form
··· 1 hidden line
MachineDefinition::define(
config: [
'id' => 'wizard',
'initial' => 'filling',
'context' => [
'current_step' => 1,
'data' => [],
],
'states' => [
'filling' => [
'initial' => 'step1',
'states' => [
'step1' => [
'on' => [
'NEXT' => [
'target' => 'step2',
'guards' => 'step1ValidGuard',
'actions' => 'saveStep1Action',
],
],
],
'step2' => [
'on' => [
'BACK' => 'step1',
'NEXT' => [
'target' => 'step3',
'guards' => 'step2ValidGuard',
'actions' => 'saveStep2Action',
],
],
],
'step3' => [
'on' => [
'BACK' => 'step2',
'SUBMIT' => [
'target' => '#reviewing',
'guards' => 'step3ValidGuard',
'actions' => 'saveStep3Action',
],
],
],
],
'on' => [
'CANCEL' => 'cancelled',
],
],
'reviewing' => [
'on' => [
'EDIT' => 'filling.step1',
'CONFIRM' => 'submitting',
],
],
'submitting' => [
'entry' => 'submitFormAction',
'on' => [
'SUCCESS' => 'completed',
'FAILURE' => 'error',
],
],
'completed' => ['type' => 'final'],
'cancelled' => ['type' => 'final'],
'error' => [
'on' => ['RETRY' => 'submitting'],
],
],
],
);E-commerce Order
'states' => [
'cart' => [
'on' => ['CHECKOUT' => 'checkout'],
],
'checkout' => [
'initial' => 'shipping',
'states' => [
'shipping' => [
'on' => [
'SUBMIT_SHIPPING' => [
'target' => 'payment',
'guards' => 'validShippingGuard',
'actions' => 'saveShippingAction',
],
],
],
'payment' => [
'on' => [
'BACK' => 'shipping',
'SUBMIT_PAYMENT' => [
'target' => 'review',
'guards' => 'validPaymentGuard',
'actions' => 'savePaymentAction',
],
],
],
'review' => [
'on' => [
'BACK' => 'payment',
'CONFIRM' => '#processing',
],
],
],
'on' => [
'CANCEL' => 'cart',
],
],
'processing' => [
'initial' => 'authorizing',
'states' => [
'authorizing' => [
'entry' => 'authorizePaymentAction',
'on' => [
'AUTHORIZED' => 'fulfilling',
'DECLINED' => '#declined',
],
],
'fulfilling' => [
'entry' => 'reserveInventoryAction',
'on' => ['FULFILLED' => '#completed'],
],
],
],
'completed' => ['type' => 'final'],
'declined' => ['type' => 'final'],
],Checking Current State
$machine = WizardMachine::create();
$machine->send(['type' => 'NEXT']); // Go to step 2
// Check nested state with `matches()` - requires full path from initial state
$machine->state->matches('filling.step2'); // true (full path)
// Partial paths return false
$machine->state->matches('filling'); // false - not a leaf state
$machine->state->matches('step2'); // false - missing parent path
// State value shows full path
$machine->state->value; // ['wizard.filling.step2']Full Path Required
The matches() method requires the full path to the leaf state. Partial paths or just the leaf state name will return false. Always specify the complete path from the machine's initial state.
Best Practices
1. Use Hierarchy for Related States
// Good - related states grouped
'checkout' => [
'states' => ['shipping', 'payment', 'review'],
],
// Avoid - flat structure for related states
'checkout_shipping' => [],
'checkout_payment' => [],
'checkout_review' => [],2. Keep Nesting Shallow
// Prefer 2-3 levels max
'order' => [
'states' => [
'processing' => [
'states' => ['validating', 'charging'],
],
],
],
// Avoid deeply nested structures
'level1' => ['states' => ['level2' => ['states' => ['level3' => [...]]]]],3. Use ID Targeting Sparingly
// Use for breaking out of hierarchy
'approved' => [
'on' => ['@always' => '#completed'],
],
// For siblings, use relative paths
'step1' => [
'on' => ['NEXT' => 'step2'], // Not '#wizard.filling.step2'
],4. Share Common Transitions at Parent Level
'checkout' => [
'on' => [
'CANCEL' => 'cart', // Available from all children
'TIMEOUT' => 'expired', // Available from all children
],
'states' => [
'shipping' => [],
'payment' => [],
],
],