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' => 'startLoadingAction',
'exit' => 'stopLoadingAction',
'on' => [
'LOADED' => 'ready',
],
],
],Multiple Actions
php
'loading' => [
'entry' => ['showSpinnerAction', 'logEntryAction', 'startTimerAction'],
'exit' => ['hideSpinnerAction', 'logExitAction', 'stopTimerAction'],
],Actions execute in the order specified.
Execution Order
Complete Example
php
'states' => [
'state_a' => [
'exit' => 'exitAAction',
'on' => [
'GO' => [
'target' => 'state_b',
'actions' => 'transitionAction',
],
],
],
'state_b' => [
'entry' => 'enterBAction',
],
],When GO is sent:
exitAruns (leaving state_a)transitionActionruns (during transition)enterBruns (entering state_b)
Entry Actions
Setup and Initialization
php
'loading' => [
'entry' => 'initializeLoaderAction',
'on' => ['COMPLETE' => 'ready'],
],
'actions' => [
'initializeLoaderAction' => function (ContextManager $context) {
$context->startTime = now();
$context->attempts = 0;
$context->isLoading = true;
},
],Class-Based Entry Action
php
··· 2 hidden lines
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
··· 2 hidden lines
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' => 'saveProgressAction',
'on' => ['SUBMIT' => 'reviewing'],
],
'actions' => [
'saveProgressAction' => function (ContextManager $context) {
$context->lastSaved = now();
// Save draft to database
},
],Resource Release
php
··· 2 hidden lines
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' => 'logOrderStartAction',
'exit' => 'logOrderEndAction',
'states' => [
'processing' => [
'entry' => 'startProcessingAction',
'exit' => 'stopProcessingAction',
'states' => [
'validating' => [
'entry' => 'startValidationAction',
'exit' => 'stopValidationAction',
],
],
],
],
],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' => ['showLoadingIndicatorAction', 'fetchDataAction'],
'exit' => 'hideLoadingIndicatorAction',
'on' => [
'SUCCESS' => 'loaded',
'FAILURE' => 'error',
],
],
'loaded' => [],
'error' => [
'entry' => 'showErrorMessageAction',
],
],Form Wizard
php
'wizard' => [
'initial' => 'step1',
'entry' => 'initializeWizardAction',
'exit' => 'cleanupWizardAction',
'states' => [
'step1' => [
'entry' => 'loadStep1DataAction',
'exit' => 'saveStep1DataAction',
'on' => ['NEXT' => 'step2'],
],
'step2' => [
'entry' => 'loadStep2DataAction',
'exit' => 'saveStep2DataAction',
'on' => [
'BACK' => 'step1',
'NEXT' => 'step3',
],
],
'step3' => [
'entry' => 'loadStep3DataAction',
'on' => [
'BACK' => 'step2',
'SUBMIT' => '#submitted',
],
],
],
],Session Management
php
'authenticated' => [
'entry' => [
'startSessionTimerAction',
'logLoginAction',
'loadUserPreferencesAction',
],
'exit' => [
'stopSessionTimerAction',
'logLogoutAction',
'clearSessionDataAction',
],
'states' => [
'active' => [
'on' => [
'ACTIVITY' => ['actions' => 'resetTimerAction'],
'TIMEOUT' => 'inactive',
],
],
'inactive' => [
'entry' => 'showTimeoutWarningAction',
'on' => [
'ACTIVITY' => 'active',
'LOGOUT' => '#loggedOut',
],
],
],
],Order Processing
php
'processing' => [
'entry' => ['reserveInventoryAction', 'notifyWarehouseAction'],
'exit' => 'cleanupAction',
'states' => [
'authorizing' => [
'entry' => 'initiatePaymentAction',
'on' => [
'AUTHORIZED' => 'fulfilling',
'DECLINED' => '#declined',
],
],
'fulfilling' => [
'entry' => 'startFulfillmentAction',
'exit' => 'finalizeFulfillmentAction',
'on' => [
'SHIPPED' => '#shipped',
],
],
],
],Entry Actions and @always
Entry actions complete before @always transitions check:
php
'checking' => [
'entry' => 'performCheckAction', // Runs first
'on' => [
'@always' => [ // Checked after entry
['target' => 'passed', 'guards' => 'checkPassedGuard'],
['target' => 'failed'],
],
],
],
'actions' => [
'performCheckAction' => function ($context) {
$context->checkResult = performCheck();
},
],
'guards' => [
'checkPassedGuard' => fn($ctx) => $ctx->checkResult === 'success',
],Self-Transitions
Self-transitions trigger exit and entry actions:
php
'counting' => [
'entry' => 'logEntryAction',
'exit' => 'logExitAction',
'on' => [
'INCREMENT' => [
// Self-transition (no target = same state)
'actions' => 'incrementAction',
],
'RESET' => [
'target' => 'counting', // Explicit self-transition
'actions' => 'resetAction',
],
],
],When RESET is sent:
logExitrunsresetrunslogEntryruns
Testing Entry/Exit Actions
php
··· 1 hidden line
it('executes entry actions on state entry', function () {
$executionLog = [];
$machine = MachineDefinition::define(
config: [
'initial' => 'idle',
'states' => [
'idle' => [
'on' => ['START' => 'active'],
],
'active' => [
'entry' => 'onEnterAction',
],
],
],
behavior: [
'actions' => [
'onEnterAction' => function () use (&$executionLog) {
$executionLog[] = 'entered';
},
],
],
);
$machine->transition(['type' => 'START']);
expect($executionLog)->toBe(['entered']);
});Best Practices
1. Use Entry for Setup
php
'processing' => [
'entry' => [
'initializeResourcesAction',
'startMonitoringAction',
],
],2. Use Exit for Cleanup
php
'processing' => [
'exit' => [
'releaseResourcesAction',
'stopMonitoringAction',
],
],3. Keep Actions Focused
php
// Good - single responsibility
'entry' => ['logEntryAction', 'startTimerAction', 'loadDataAction'],
// Avoid - one action doing everything
'entry' => 'doEverythingAction',4. Handle Errors in Entry Actions
php
··· 2 hidden lines
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' => 'clearLocalStateAction',
// Risky - external API might fail
'exit' => 'notifyExternalServiceAction',