Actions
Actions execute side effects during state transitions. They can modify context, call external services, raise events, and perform any operation needed during state changes.
When Actions Run
Actions execute at different points in the state lifecycle:
- Exit actions: When leaving a state
- Transition actions: During the transition
- Entry actions: When entering a state
Defining Actions
Inline Functions
php
MachineDefinition::define(
config: [
'states' => [
'idle' => [
'on' => [
'INCREMENT' => [
'actions' => 'incrementCount',
],
],
],
],
],
behavior: [
'actions' => [
'incrementCount' => fn(ContextManager $context) => $context->count++,
],
],
);Class-Based Actions
php
use Tarfinlabs\EventMachine\Behavior\ActionBehavior;
class IncrementAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
$context->count++;
}
}
// Registration
'actions' => [
'incrementCount' => IncrementAction::class,
],Direct Class Reference
php
'on' => [
'INCREMENT' => [
'actions' => IncrementAction::class,
],
],Multiple Actions
Array of Actions
php
'on' => [
'SUBMIT' => [
'target' => 'submitted',
'actions' => ['validateInput', 'saveData', 'sendNotification'],
],
],Entry/Exit Actions
php
'states' => [
'loading' => [
'entry' => ['startSpinner', 'logEntry'],
'exit' => ['stopSpinner', 'logExit'],
'on' => [
'LOADED' => 'success',
],
],
],Action Parameters
Actions receive injected parameters:
php
class ProcessAction extends ActionBehavior
{
public function __invoke(
ContextManager $context,
EventBehavior $event,
State $state,
): void {
// Access context
$orderId = $context->orderId;
// Access event payload
$amount = $event->payload['amount'];
// Access current state
$currentState = $state->currentStateDefinition->id;
}
}Available Parameters
| Parameter | Type | Description |
|---|---|---|
$context | ContextManager | Current machine context |
$event | EventBehavior | Triggering event with payload |
$state | State | Current machine state |
$history | EventCollection | Event history |
$arguments | array | Arguments from behavior string |
Action Arguments
Pass arguments using colon syntax:
php
// Configuration
'actions' => 'addValue:100',
// Multiple arguments
'actions' => 'multiply:2,10',
// In action
class AddValueAction extends ActionBehavior
{
public function __invoke(
ContextManager $context,
array $arguments,
): void {
$value = $arguments[0] ?? 0;
$context->total += $value;
}
}Dependency Injection
Class actions support constructor injection:
php
class ProcessOrderAction extends ActionBehavior
{
public function __construct(
private readonly OrderService $orderService,
private readonly PaymentGateway $paymentGateway,
private readonly NotificationService $notifications,
) {}
public function __invoke(ContextManager $context): void
{
$order = $this->orderService->create($context->items);
$this->paymentGateway->charge($order->total);
$this->notifications->orderConfirmed($order);
$context->orderId = $order->id;
}
}Raising Events
Actions can queue events for processing:
php
class ProcessAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
$context->processed = true;
// Queue event for after current transition completes
$this->raise(['type' => 'PROCESSING_COMPLETE']);
// With payload
$this->raise([
'type' => 'NOTIFICATION',
'payload' => ['message' => 'Order processed'],
]);
}
}Raised events are processed after the current transition and all entry actions complete.
Required Context
Declare required context keys:
php
class ChargePaymentAction extends ActionBehavior
{
public static array $requiredContext = [
'userId' => 'string',
'amount' => 'numeric',
'paymentMethod' => 'string',
];
public function __invoke(ContextManager $context): void
{
// Context is guaranteed to have these keys
$this->paymentGateway->charge(
$context->userId,
$context->amount,
$context->paymentMethod,
);
}
}Logging
Enable execution logging:
php
class ImportantAction extends ActionBehavior
{
public bool $shouldLog = true;
public function __invoke(ContextManager $context): void
{
// Execution will be logged
}
}Practical Examples
Updating Context
php
class AddItemAction extends ActionBehavior
{
public function __invoke(
ContextManager $context,
EventBehavior $event,
): void {
$item = $event->payload['item'];
$context->items[] = $item;
$context->itemCount = count($context->items);
$context->total += $item['price'];
}
}External Service Call
php
class SendEmailAction extends ActionBehavior
{
public function __construct(
private readonly Mailer $mailer,
) {}
public function __invoke(ContextManager $context): void
{
$this->mailer->send(
to: $context->userEmail,
template: 'order-confirmation',
data: [
'orderId' => $context->orderId,
'items' => $context->items,
'total' => $context->total,
],
);
}
}Database Operation
php
class CreateOrderAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
$order = Order::create([
'user_id' => $context->userId,
'items' => $context->items,
'total' => $context->total,
'status' => 'pending',
]);
$context->orderId = $order->id;
}
}Conditional Logic
php
class ProcessPaymentAction extends ActionBehavior
{
public function __construct(
private readonly PaymentGateway $gateway,
) {}
public function __invoke(ContextManager $context): void
{
$result = $this->gateway->charge(
$context->paymentMethod,
$context->total,
);
if ($result->successful) {
$context->paymentId = $result->id;
$this->raise(['type' => 'PAYMENT_SUCCESS']);
} else {
$context->paymentError = $result->message;
$this->raise(['type' => 'PAYMENT_FAILED']);
}
}
}Chained Actions
php
'on' => [
'CHECKOUT' => [
'target' => 'processing',
'actions' => [
ValidateCartAction::class,
CalculateTotalsAction::class,
ReserveInventoryAction::class,
CreateOrderAction::class,
SendConfirmationAction::class,
],
],
],Testing Actions
php
use Tests\Stubs\Actions\IncrementAction;
it('increments the count', function () {
IncrementAction::fake();
IncrementAction::shouldRun()
->once()
->andReturnUsing(function (ContextManager $context) {
$context->count++;
});
$machine = CounterMachine::create();
$machine->send(['type' => 'INCREMENT']);
IncrementAction::assertRan();
expect($machine->state->context->count)->toBe(1);
});Best Practices
1. Keep Actions Focused
One action, one responsibility:
php
// Good
class IncrementCountAction extends ActionBehavior { ... }
class SendNotificationAction extends ActionBehavior { ... }
// Avoid
class DoEverythingAction extends ActionBehavior { ... }2. Use Dependency Injection
php
class ProcessOrderAction extends ActionBehavior
{
public function __construct(
private readonly OrderService $orders,
) {}
}3. Handle Errors Gracefully
php
class ExternalApiAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
try {
$result = $this->api->call($context->data);
$context->apiResult = $result;
} catch (ApiException $e) {
$context->apiError = $e->getMessage();
$this->raise(['type' => 'API_ERROR']);
}
}
}4. Use Raised Events for Flow Control
php
class ValidateAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
if ($this->isValid($context)) {
$this->raise(['type' => 'VALIDATION_PASSED']);
} else {
$this->raise(['type' => 'VALIDATION_FAILED']);
}
}
}