Skip to content

Configuration ​

This guide covers all configuration options for defining state machines.

Machine Configuration ​

The config array defines your machine's structure:

php
MachineDefinition::define(
    config: [
        'id' => 'order',
        'version' => '1.0.0',
        'initial' => 'pending',
        'delimiter' => '.',
        'context' => [...],
        'states' => [...],
        'should_persist' => true,
    ],
);

Root Configuration Keys ​

KeyTypeDefaultDescription
idstring'machine'Unique identifier for the machine
versionstringnullVersion string for tracking
initialstringFirst stateInitial state name
delimiterstring'.'Path separator for state IDs
contextarray|class[]Initial context or ContextManager class
statesarrayRequiredState definitions
onarraynullRoot-level transitions
should_persistbooltrueEnable database persistence
scenarios_enabledboolfalseDeprecated — use config/machine.php scenarios.enabled instead

Machine ID ​

Identifies the machine for persistence and debugging:

php
'id' => 'checkout-flow',

State IDs are prefixed with the machine ID:

  • checkout-flow.cart
  • checkout-flow.payment

Version ​

Track machine versions for migrations:

php
'version' => '2.0.0',

Initial State ​

Specify which state the machine starts in:

php
'initial' => 'idle',

If not specified, the first state in the states array is used.

Delimiter ​

Customize the path separator for nested state IDs:

php
'delimiter' => '/',  // Results in: order/checkout/payment

Default is . (dot): order.checkout.payment

Behavior Configuration ​

The behavior array maps names to implementations:

php
MachineDefinition::define(
    config: [...],
    behavior: [
        'actions' => [
            'sendEmailAction' => SendEmailAction::class,
            'logEventAction' => fn (ContextManager $context) => logger()->info('Event'),
        ],
        'guards' => [
            'isAuthenticatedGuard' => IsAuthenticatedGuard::class,
            'hasPermissionGuard' => fn (ContextManager $context) => $context->get('role') === 'admin',
        ],
        'calculators' => [
            'computeTotalCalculator' => ComputeTotalCalculator::class,
        ],
        'events' => [
            'ADD_ITEM' => AddItemEvent::class,
        ],
        'outputs' => [
            'orderSummaryOutput' => OrderSummaryOutput::class,
        ],
        'context' => OrderContext::class,
    ],
);

Behavior Types ​

TypeDescription
actionsSide effects during transitions
guardsConditions controlling transitions
calculatorsContext computations before guards
eventsCustom event classes
outputsFinal state output computation
contextCustom ContextManager class

Inline vs Class Behaviors ​

Both approaches work:

php
'actions' => [
    // Class reference
    'sendEmailAction' => SendEmailAction::class,

    // Inline closure
    'logEventAction' => function (ContextManager $context): void {
        logger()->info('Event logged');
    },
],

State Configuration ​

Each state supports these keys:

KeyTypeDescription
onarrayEvent-to-transition mappings
entrystring|arrayActions on state entry
exitstring|arrayActions on state exit
typestringState type ('final')
outputstring|arrayOutput behavior for final states
initialstringInitial child state
statesarrayChild state definitions
machinestring (FQCN)Child machine class for delegation
inputstring|array|ClosureData to pass to child machine. MachineInput FQCN, array, or closure. (renamed from with in v9)
failurestring (FQCN)MachineFailure class for typed error data on child @fail
metaarrayCustom metadata
descriptionstringHuman-readable description
php
'processing' => [
    'description' => 'Order is being processed',
    'entry' => 'startProcessingAction',
    'exit' => 'cleanupAction',
    'meta' => ['timeout' => 3600],
    'on' => [
        'COMPLETE' => 'done',
        'FAIL' => 'failed',
    ],
],

Transition Configuration ​

Transitions can be simple strings or detailed arrays:

KeyTypeDescription
targetstringDestination state
actionsstring|arrayActions during transition
guardsstring|arrayConditions to check
calculatorsstring|arrayPre-guard computations
descriptionstringHuman-readable description
php
'SUBMIT' => [
    'target' => 'submitted',
    'guards' => ['isValidGuard', 'canSubmitGuard'],
    'calculators' => 'prepareDataCalculator',
    'actions' => ['validateAction', 'saveAction', 'notifyAction'],
    'description' => 'Submit the form for review',
],

Persistence Configuration ​

Disabling Persistence ​

For ephemeral machines:

php
'should_persist' => false,

Useful for:

  • Testing
  • Short-lived operations
  • Memory-only state machines

Default Behavior ​

With should_persist => true (default):

  • Every transition creates a MachineEvent record
  • State can be restored from any point
  • Full history is maintained

Scenarios Configuration ​

Scenarios are configured in config/machine.php, not in the machine definition:

php
// config/machine.php
'scenarios' => [
    'enabled' => env('MACHINE_SCENARIOS_ENABLED', false),
],
KeyTypeDefaultDescription
scenarios.enabledboolfalseEnable scenario system. Set to true in staging only.

When enabled, scenarios provide behavior overrides for QA and staging environments via MachineScenario classes. See Scenarios for full documentation.

Configuration Validation ​

EventMachine validates your configuration at definition time:

Invalid Keys ​

php
// Throws: Invalid root level configuration keys: invalid_key
'invalid_key' => 'value',

Invalid State Type ​

php
// Throws: State 'foo' has invalid type: invalid
'foo' => ['type' => 'invalid'],

Final State Constraints ​

php
// Throws: Final state 'done' cannot have transitions
'done' => [
    'type' => 'final',
    'on' => ['RESTART' => 'initial'],  // Not allowed
],

Guarded Transition Order ​

php
// Throws: Default condition must be last
'PAY' => [
    ['target' => 'failed'],           // No guard - must be last!
    ['target' => 'paid', 'guards' => 'isValid'],
],

Complete Configuration Example ​

php
use Tarfinlabs\EventMachine\Definition\MachineDefinition;

MachineDefinition::define(
    config: [
        'id' => 'order',
        'version' => '1.0.0',
        'initial' => 'cart',
        'delimiter' => '.',
        'context' => OrderContext::class,
        'should_persist' => true,
        'states' => [
            'cart' => [
                'description' => 'Shopping cart',
                'meta' => ['icon' => 'shopping-cart'],
                'on' => [
                    'ADD_ITEM' => ['actions' => 'addItemAction'],
                    'CHECKOUT' => [
                        'target' => 'checkout',
                        'guards' => 'hasItemsGuard',
                    ],
                ],
            ],
            'checkout' => [
                'initial' => 'shipping',
                'states' => [
                    'shipping' => [
                        'entry' => 'loadShippingOptionsAction',
                        'on' => [
                            'SET_ADDRESS' => ['actions' => 'setAddressAction'],
                            'CONTINUE' => [
                                'target' => 'payment',
                                'guards' => 'hasAddressGuard',
                            ],
                        ],
                    ],
                    'payment' => [
                        'entry' => 'loadPaymentMethodsAction',
                        'on' => [
                            'PAY' => [
                                [
                                    'target' => 'confirmed',
                                    'guards' => 'paymentValidGuard',
                                    'actions' => ['processPaymentAction', 'reserveStockAction'],
                                ],
                                [
                                    'target' => 'payment',
                                    'actions' => 'showPaymentErrorAction',
                                ],
                            ],
                            'BACK' => 'shipping',
                        ],
                    ],
                    'confirmed' => [],
                ],
                'on' => [
                    'CANCEL' => 'cart',
                ],
            ],
            'processing' => [
                'entry' => 'startFulfillmentAction',
                'on' => [
                    'SHIP' => 'shipped',
                ],
            ],
            'shipped' => [
                'entry' => 'sendTrackingEmailAction',
                'on' => [
                    'DELIVER' => 'delivered',
                ],
            ],
            'delivered' => [
                'type' => 'final',
                'entry' => 'sendDeliveryConfirmationAction',
                'output' => 'orderSummaryOutput',
                'meta' => ['completed' => true],
            ],
        ],
    ],
    behavior: [
        'context' => OrderContext::class,
        'events' => [
            'ADD_ITEM' => AddItemEvent::class,
            'SET_ADDRESS' => SetAddressEvent::class,
            'PAY' => PaymentEvent::class,
        ],
        'actions' => [
            'addItemAction' => AddItemAction::class,
            'setAddressAction' => SetAddressAction::class,
            'processPaymentAction' => ProcessPaymentAction::class,
            'reserveStockAction' => ReserveStockAction::class,
            'startFulfillmentAction' => StartFulfillmentAction::class,
            'sendTrackingEmailAction' => SendTrackingEmailAction::class,
            'sendDeliveryConfirmationAction' => SendDeliveryConfirmationAction::class,
            'loadShippingOptionsAction' => LoadShippingOptionsAction::class,
            'loadPaymentMethodsAction' => LoadPaymentMethodsAction::class,
            'showPaymentErrorAction' => ShowPaymentErrorAction::class,
        ],
        'guards' => [
            'hasItemsGuard' => HasItemsGuard::class,
            'hasAddressGuard' => HasAddressGuard::class,
            'paymentValidGuard' => PaymentValidGuard::class,
        ],
        'outputs' => [
            'orderSummaryOutput' => OrderSummaryOutput::class,
        ],
    ],
);

Parallel Dispatch Configuration ​

Parallel dispatch runs region entry actions as concurrent queue jobs. Configure in config/machine.php:

php
return [
    'parallel_dispatch' => [
        'enabled'        => env('MACHINE_PARALLEL_DISPATCH_ENABLED', false),
        'queue'          => env('MACHINE_PARALLEL_DISPATCH_QUEUE', null),
        'lock_timeout'   => env('MACHINE_PARALLEL_DISPATCH_LOCK_TIMEOUT', 30),
        'lock_ttl'       => env('MACHINE_PARALLEL_DISPATCH_LOCK_TTL', 60),
        'job_timeout'    => env('MACHINE_PARALLEL_DISPATCH_JOB_TIMEOUT', 300),
        'job_tries'      => env('MACHINE_PARALLEL_DISPATCH_JOB_TRIES', 3),
        'job_backoff'    => env('MACHINE_PARALLEL_DISPATCH_JOB_BACKOFF', 30),
        'region_timeout' => env('MACHINE_PARALLEL_DISPATCH_REGION_TIMEOUT', 0),
    ],
];
KeyDefaultDescription
enabledfalseEnable concurrent dispatch of region entry actions
queuenullQueue name for region jobs (null = default)
lock_timeout30Max seconds for blocking lock acquisition
lock_ttl60Lock auto-expiry for stale lock cleanup
job_timeout300Laravel job execution timeout (seconds)
job_tries3Max retry attempts for failed region jobs
job_backoff30Seconds between retry attempts
region_timeout0Seconds before stuck parallel state triggers @fail (0 = disabled)

For details, see Parallel Dispatch.

Testing

For testing configuration options like should_persist, see Testing Overview.

Syntax Shorthands ​

EventMachine accepts both verbose and shorthand forms for common configuration patterns. Prefer the short form when no extra options are needed — it's easier to read and reduces noise.

Transitions ​

php
// ✅ Short — target only
'SUBMIT' => 'processing',

// Verbose equivalent
'SUBMIT' => ['target' => 'processing'],

// Use verbose only when adding guards, actions, or calculators
'SUBMIT' => [
    'target'  => 'processing',
    'guards'  => IsValidGuard::class,
    'actions' => LogAction::class,
],

Entry / Exit Actions ​

php
// ✅ Short — single action
'entry' => InitAction::class,
'exit'  => CleanupAction::class,

// Multiple actions require array
'entry' => [InitAction::class, NotifyAction::class],

Guards, Actions, Calculators on Transitions ​

php
// ✅ Short — single behavior
'guards'      => IsValidGuard::class,
'actions'     => LogAction::class,
'calculators' => TotalCalculator::class,

// Multiple behaviors require array
'guards' => [IsValidGuard::class, HasBalanceGuard::class],

@done / @fail (Child Delegation) ​

php
// ✅ Short — target only
'@done' => 'completed',
'@fail' => 'failed',

// Verbose — with actions
'@done' => [
    'target'  => 'completed',
    'actions' => CaptureOutputAction::class,
],

// Per-final-state routing
'@done.approved' => 'processing',
'@done.rejected' => 'declined',

Queue (Async Delegation) ​

php
// ✅ Short — default queue
'queue' => true,

// Named queue
'queue' => 'child-queue',

// Full config
'queue' => [
    'queue'      => 'child-queue',
    'connection' => 'redis',
    'retry'      => 3,
],

Quick Reference ​

ElementShort FormVerbose Form
Transition target'EVENT' => 'state''EVENT' => ['target' => 'state']
Forbidden event'EVENT' => null—
Single entry action'entry' => Action::class'entry' => [Action::class]
Single exit action'exit' => Action::class'exit' => [Action::class]
Single guard'guards' => Guard::class'guards' => [Guard::class]
Single action'actions' => Action::class'actions' => [Action::class]
Single calculator'calculators' => Calc::class'calculators' => [Calc::class]
@done target'@done' => 'state''@done' => ['target' => 'state']
@fail target'@fail' => 'state''@fail' => ['target' => 'state']
Default queue'queue' => true'queue' => ['queue' => null]
Named queue'queue' => 'name''queue' => ['queue' => 'name']

Rule of Thumb

If you're only specifying a target or a single value, use the short form. Switch to verbose only when you need extra options (guards, actions, calculators, description).

Released under the MIT License.