Entry/Exit Actions
Entry and exit actions are lifecycle hooks that execute when entering or leaving a state. They're useful for setup, cleanup, logging, and side effects tied to state boundaries.
Basic Syntax
php
'states' => [
'loading' => [
'entry' => 'startLoading',
'exit' => 'stopLoading',
'on' => [
'LOADED' => 'ready',
],
],
],Multiple Actions
php
'loading' => [
'entry' => ['showSpinner', 'logEntry', 'startTimer'],
'exit' => ['hideSpinner', 'logExit', 'stopTimer'],
],Actions execute in the order specified.
Execution Order
Complete Example
php
'states' => [
'stateA' => [
'exit' => 'exitA',
'on' => [
'GO' => [
'target' => 'stateB',
'actions' => 'transitionAction',
],
],
],
'stateB' => [
'entry' => 'enterB',
],
],When GO is sent:
exitAruns (leaving stateA)transitionActionruns (during transition)enterBruns (entering stateB)
Entry Actions
Setup and Initialization
php
'loading' => [
'entry' => 'initializeLoader',
'on' => ['COMPLETE' => 'ready'],
],
'actions' => [
'initializeLoader' => function (ContextManager $context) {
$context->startTime = now();
$context->attempts = 0;
$context->isLoading = true;
},
],Class-Based Entry Action
php
class StartProcessingAction extends ActionBehavior
{
public function __construct(
private readonly ProcessingService $service,
) {}
public function __invoke(ContextManager $context): void
{
$this->service->start($context->processId);
$context->processingStarted = now();
}
}
// In configuration
'processing' => [
'entry' => StartProcessingAction::class,
],Entry Action with Raised Event
php
class ValidateOnEntryAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
$isValid = $this->validate($context);
if ($isValid) {
$this->raise(['type' => 'VALIDATION_PASSED']);
} else {
$this->raise(['type' => 'VALIDATION_FAILED']);
}
}
}
'validating' => [
'entry' => ValidateOnEntryAction::class,
'on' => [
'VALIDATION_PASSED' => 'approved',
'VALIDATION_FAILED' => 'rejected',
],
],Exit Actions
Cleanup
php
'editing' => [
'exit' => 'saveProgress',
'on' => ['SUBMIT' => 'reviewing'],
],
'actions' => [
'saveProgress' => function (ContextManager $context) {
$context->lastSaved = now();
// Save draft to database
},
],Resource Release
php
class ReleaseResourcesAction extends ActionBehavior
{
public function __construct(
private readonly ResourceManager $resources,
) {}
public function __invoke(ContextManager $context): void
{
if ($context->resourceId) {
$this->resources->release($context->resourceId);
}
}
}
'processing' => [
'exit' => ReleaseResourcesAction::class,
],Hierarchical States
Entry and exit actions respect hierarchy:
php
'order' => [
'entry' => 'logOrderStart',
'exit' => 'logOrderEnd',
'states' => [
'processing' => [
'entry' => 'startProcessing',
'exit' => 'stopProcessing',
'states' => [
'validating' => [
'entry' => 'startValidation',
'exit' => 'stopValidation',
],
],
],
],
],Entering Nested State
When entering order.processing.validating:
logOrderStart(order entry)startProcessing(processing entry)startValidation(validating entry)
Exiting to Sibling
When transitioning from validating to a sibling in processing:
stopValidation(validating exit)- Entry action of new sibling
Exiting Hierarchy
When transitioning from validating to outside order:
stopValidation(validating exit)stopProcessing(processing exit)logOrderEnd(order exit)- Entry actions of new target
Practical Examples
Loading State
php
'states' => [
'idle' => [
'on' => ['LOAD' => 'loading'],
],
'loading' => [
'entry' => ['showLoadingIndicator', 'fetchData'],
'exit' => 'hideLoadingIndicator',
'on' => [
'SUCCESS' => 'loaded',
'FAILURE' => 'error',
],
],
'loaded' => [],
'error' => [
'entry' => 'showErrorMessage',
],
],Form Wizard
php
'wizard' => [
'initial' => 'step1',
'entry' => 'initializeWizard',
'exit' => 'cleanupWizard',
'states' => [
'step1' => [
'entry' => 'loadStep1Data',
'exit' => 'saveStep1Data',
'on' => ['NEXT' => 'step2'],
],
'step2' => [
'entry' => 'loadStep2Data',
'exit' => 'saveStep2Data',
'on' => [
'BACK' => 'step1',
'NEXT' => 'step3',
],
],
'step3' => [
'entry' => 'loadStep3Data',
'on' => [
'BACK' => 'step2',
'SUBMIT' => '#submitted',
],
],
],
],Session Management
php
'authenticated' => [
'entry' => [
'startSessionTimer',
'logLogin',
'loadUserPreferences',
],
'exit' => [
'stopSessionTimer',
'logLogout',
'clearSessionData',
],
'states' => [
'active' => [
'on' => [
'ACTIVITY' => ['actions' => 'resetTimer'],
'TIMEOUT' => 'inactive',
],
],
'inactive' => [
'entry' => 'showTimeoutWarning',
'on' => [
'ACTIVITY' => 'active',
'LOGOUT' => '#loggedOut',
],
],
],
],Order Processing
php
'processing' => [
'entry' => ['reserveInventory', 'notifyWarehouse'],
'exit' => 'cleanup',
'states' => [
'authorizing' => [
'entry' => 'initiatePayment',
'on' => [
'AUTHORIZED' => 'fulfilling',
'DECLINED' => '#declined',
],
],
'fulfilling' => [
'entry' => 'startFulfillment',
'exit' => 'finalizeFulfillment',
'on' => [
'SHIPPED' => '#shipped',
],
],
],
],Entry Actions and @always
Entry actions complete before @always transitions check:
php
'checking' => [
'entry' => 'performCheck', // Runs first
'on' => [
'@always' => [ // Checked after entry
['target' => 'passed', 'guards' => 'checkPassed'],
['target' => 'failed'],
],
],
],
'actions' => [
'performCheck' => function ($context) {
$context->checkResult = performCheck();
},
],
'guards' => [
'checkPassed' => fn($ctx) => $ctx->checkResult === 'success',
],Self-Transitions
Self-transitions trigger exit and entry actions:
php
'counting' => [
'entry' => 'logEntry',
'exit' => 'logExit',
'on' => [
'INCREMENT' => [
// Self-transition (no target = same state)
'actions' => 'increment',
],
'RESET' => [
'target' => 'counting', // Explicit self-transition
'actions' => 'reset',
],
],
],When RESET is sent:
logExitrunsresetrunslogEntryruns
Testing Entry/Exit Actions
php
it('executes entry actions on state entry', function () {
$executionLog = [];
$machine = MachineDefinition::define(
config: [
'initial' => 'idle',
'states' => [
'idle' => [
'on' => ['START' => 'active'],
],
'active' => [
'entry' => 'onEnter',
],
],
],
behavior: [
'actions' => [
'onEnter' => function () use (&$executionLog) {
$executionLog[] = 'entered';
},
],
],
);
$machine->transition(['type' => 'START']);
expect($executionLog)->toBe(['entered']);
});Best Practices
1. Use Entry for Setup
php
'processing' => [
'entry' => [
'initializeResources',
'startMonitoring',
],
],2. Use Exit for Cleanup
php
'processing' => [
'exit' => [
'releaseResources',
'stopMonitoring',
],
],3. Keep Actions Focused
php
// Good - single responsibility
'entry' => ['logEntry', 'startTimer', 'loadData'],
// Avoid - one action doing everything
'entry' => 'doEverything',4. Handle Errors in Entry Actions
php
class SafeEntryAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
try {
$this->riskyOperation();
} catch (Exception $e) {
$context->entryError = $e->getMessage();
$this->raise(['type' => 'ENTRY_FAILED']);
}
}
}5. Avoid Side Effects in Exit Actions That Might Fail
Exit actions should be reliable:
php
// Good - unlikely to fail
'exit' => 'clearLocalState',
// Risky - external API might fail
'exit' => 'notifyExternalService',