Scenarios
Scenarios allow you to define alternative state machine configurations that can be activated at runtime. They're useful for A/B testing, feature flags, and environment-specific behavior.
Basic Usage
Enabling Scenarios
php
MachineDefinition::define(
config: [
'initial' => 'stateA',
'scenarios_enabled' => true, // Enable scenarios
'states' => [
'stateA' => [
'on' => ['EVENT' => 'stateB'],
],
'stateB' => [],
],
],
scenarios: [
'test' => [
'stateA' => [
'on' => [
'EVENT' => 'stateC', // Different target in 'test' scenario
],
],
],
'beta' => [
'stateA' => [
'on' => [
'EVENT' => [
'target' => 'stateB',
'actions' => 'betaAction', // Additional action
],
],
],
],
],
);Activating a Scenario
Include scenarioType in the event payload:
php
// Normal flow
$machine->send(['type' => 'EVENT']);
// Goes to stateB
// Test scenario
$machine->send([
'type' => 'EVENT',
'payload' => ['scenarioType' => 'test'],
]);
// Goes to stateC
// Beta scenario
$machine->send([
'type' => 'EVENT',
'payload' => ['scenarioType' => 'beta'],
]);
// Goes to stateB with betaActionScenario Configuration
Scenarios can override:
Transitions
php
'scenarios' => [
'express' => [
'pending' => [
'on' => [
'SUBMIT' => 'express_processing', // Different target
],
],
],
],Actions
php
'scenarios' => [
'debug' => [
'processing' => [
'on' => [
'COMPLETE' => [
'target' => 'completed',
'actions' => ['logDebug', 'sendNotification'],
],
],
],
],
],Entry/Exit Actions
php
'scenarios' => [
'monitoring' => [
'active' => [
'entry' => ['defaultEntry', 'recordMetrics'],
'exit' => ['defaultExit', 'flushMetrics'],
],
],
],Guards
php
'scenarios' => [
'lenient' => [
'validating' => [
'on' => [
'SUBMIT' => [
'target' => 'submitted',
'guards' => 'lenientValidation', // Less strict
],
],
],
],
],Practical Examples
A/B Testing
php
MachineDefinition::define(
config: [
'id' => 'checkout',
'initial' => 'cart',
'scenarios_enabled' => true,
'states' => [
'cart' => [
'on' => ['CHECKOUT' => 'shipping'],
],
'shipping' => [
'on' => ['CONTINUE' => 'payment'],
],
'payment' => [
'on' => ['PAY' => 'confirmation'],
],
'confirmation' => ['type' => 'final'],
'express_checkout' => [
'on' => ['COMPLETE' => 'confirmation'],
],
],
],
scenarios: [
'express_flow' => [
'cart' => [
'on' => [
'CHECKOUT' => 'express_checkout', // Skip shipping/payment
],
],
],
'upsell_flow' => [
'payment' => [
'on' => [
'PAY' => [
'target' => 'confirmation',
'actions' => 'showUpsellOffer',
],
],
],
],
],
);
// Usage based on user segment
$scenario = $user->isInTestGroup('express') ? 'express_flow' : null;
$machine->send([
'type' => 'CHECKOUT',
'payload' => [
'scenarioType' => $scenario,
],
]);Feature Flags
php
MachineDefinition::define(
config: [
'scenarios_enabled' => true,
'states' => [
'processing' => [
'on' => [
'COMPLETE' => 'legacy_completion',
],
],
'legacy_completion' => ['type' => 'final'],
'new_completion' => [
'entry' => 'enhancedCompletionFlow',
'type' => 'final',
],
],
],
scenarios: [
'new_completion_feature' => [
'processing' => [
'on' => [
'COMPLETE' => 'new_completion',
],
],
],
],
);
// Activate based on feature flag
$machine->send([
'type' => 'COMPLETE',
'payload' => [
'scenarioType' => feature('new_completion') ? 'new_completion_feature' : null,
],
]);Environment-Specific Behavior
php
MachineDefinition::define(
config: [
'scenarios_enabled' => true,
'states' => [
'sending' => [
'entry' => 'sendEmail',
'on' => ['SENT' => 'completed'],
],
],
],
scenarios: [
'testing' => [
'sending' => [
'entry' => 'mockSendEmail', // Don't actually send
],
],
'staging' => [
'sending' => [
'entry' => ['sendEmail', 'logToSlack'], // Extra logging
],
],
],
);
// Activate based on environment
$scenario = match (app()->environment()) {
'testing' => 'testing',
'staging' => 'staging',
default => null,
};
$machine->send([
'type' => 'SEND',
'payload' => ['scenarioType' => $scenario],
]);Multi-Tenant Customization
php
MachineDefinition::define(
config: [
'id' => 'approval',
'scenarios_enabled' => true,
'states' => [
'pending' => [
'on' => ['APPROVE' => 'approved'],
],
'approved' => ['type' => 'final'],
'dual_approved' => ['type' => 'final'],
],
],
scenarios: [
'tenant_enterprise' => [
'pending' => [
'on' => [
'APPROVE' => [
'target' => 'awaiting_second_approval',
'guards' => 'isFirstApproval',
],
],
],
'awaiting_second_approval' => [
'on' => [
'APPROVE' => 'dual_approved',
],
],
],
],
);
// Activate based on tenant
$scenario = $tenant->requiresDualApproval() ? 'tenant_enterprise' : null;Complete Example
php
class OrderMachine extends Machine
{
public static function definition(): MachineDefinition
{
return MachineDefinition::define(
config: [
'id' => 'order',
'initial' => 'pending',
'scenarios_enabled' => true,
'context' => ['count' => 1],
'states' => [
'pending' => [
'on' => [
'SUBMIT' => [
'target' => 'processing',
'actions' => 'incrementAction',
],
],
],
'processing' => [
'on' => ['COMPLETE' => 'completed'],
],
'completed' => ['type' => 'final'],
'fast_completed' => ['type' => 'final'],
],
],
behavior: [
'actions' => [
'incrementAction' => fn($ctx) => $ctx->count++,
'decrementAction' => fn($ctx) => $ctx->count--,
],
],
scenarios: [
'test' => [
'pending' => [
'on' => [
'SUBMIT' => [
'target' => 'fast_completed', // Skip processing
'actions' => 'decrementAction', // Different action
],
],
'exit' => ['decrementAction'], // Additional exit action
],
],
],
);
}
}
// Usage
$machine = OrderMachine::create();
// Normal flow
$machine->send(['type' => 'SUBMIT']);
// count = 2, state = processing
// Test scenario
$testMachine = OrderMachine::create();
$testMachine->send([
'type' => 'SUBMIT',
'payload' => ['scenarioType' => 'test'],
]);
// count = -1 (decremented twice: exit + action), state = fast_completedTesting with Scenarios
php
it('uses test scenario when specified', function () {
$machine = OrderMachine::create();
$machine->send([
'type' => 'SUBMIT',
'payload' => ['scenarioType' => 'test'],
]);
expect($machine->state->matches('fast_completed'))->toBeTrue()
->and($machine->state->context->count)->toBe(-1);
});
it('uses default flow without scenario', function () {
$machine = OrderMachine::create();
$machine->send(['type' => 'SUBMIT']);
expect($machine->state->matches('processing'))->toBeTrue()
->and($machine->state->context->count)->toBe(2);
});Best Practices
1. Use Descriptive Scenario Names
php
'scenarios' => [
'ab_test_checkout_v2' => [...],
'enterprise_tier' => [...],
'staging_debug' => [...],
],2. Document Scenario Differences
php
'scenarios' => [
// Skips validation for testing
'skip_validation' => [
'validating' => [
'on' => ['@always' => 'approved'],
],
],
],3. Keep Scenarios Minimal
Override only what's necessary:
php
// Good - minimal override
'scenarios' => [
'test' => [
'pending' => [
'on' => ['SUBMIT' => 'fast_track'],
],
],
],
// Avoid - duplicating entire configuration4. Use Scenarios for Testing
php
// In tests
$machine->send([
'type' => 'SUBMIT',
'payload' => ['scenarioType' => 'test'],
]);