Introduction to Behaviors
Behaviors are the building blocks for logic in EventMachine. They define how your machine responds to events, validates transitions, computes values, and produces results.
This section covers all behavior types in depth. If you're just getting started, read through Actions and Guards first - they're the most commonly used.
Behavior Types
| Type | Purpose | Returns |
|---|---|---|
| Actions | Execute side effects | void |
| Guards | Control transition execution | bool |
| Validation Guards | Validate with error messages | bool |
| Calculators | Compute values before guards | void |
| Event Behaviors | Define event structure | Event data |
| Results | Compute final state output | mixed |
Behavior Registration
Register behaviors in the behavior parameter:
MachineDefinition::define(
config: [...],
behavior: [
'actions' => [
'incrementCount' => IncrementAction::class,
'logAction' => fn($ctx) => logger()->info('Action executed'),
],
'guards' => [
'isValid' => IsValidGuard::class,
'canProceed' => fn($ctx) => $ctx->count > 0,
],
'calculators' => [
'calculateTotal' => CalculateTotalCalculator::class,
],
'events' => [
'SUBMIT' => SubmitEvent::class,
],
'results' => [
'getFinalResult' => FinalResultBehavior::class,
],
],
);InvokableBehavior Base Class
All behavior classes extend InvokableBehavior:
use Tarfinlabs\EventMachine\Behavior\InvokableBehavior;
abstract class InvokableBehavior
{
// Required context declaration
public static array $requiredContext = [];
// Whether to log execution
public bool $shouldLog = false;
// Queue for raised events (protected)
protected ?Collection $eventQueue;
// Raise an event from within the behavior
public function raise(EventBehavior|array $eventBehavior): void;
// Get behavior type name
public static function getType(): string;
}Inline vs Class Behaviors
Inline Functions
Quick and simple:
'actions' => [
'increment' => fn(ContextManager $context) => $context->count++,
],
'guards' => [
'isPositive' => fn(ContextManager $context) => $context->count > 0,
],Class Behaviors
For complex logic or dependency injection:
class ProcessOrderAction extends ActionBehavior
{
public function __construct(
private readonly OrderService $orderService,
private readonly NotificationService $notificationService,
) {}
public function __invoke(ContextManager $context): void
{
$order = $this->orderService->process($context->orderId);
$this->notificationService->notify($order);
}
}Parameter Injection
Behaviors receive parameters through dependency injection:
public function __invoke(
ContextManager $context, // Current context
EventBehavior $event, // Current event
State $state, // Current state
EventCollection $history, // Event history
array $arguments, // Behavior arguments
): void {
// Use injected parameters
}Available Parameters
| Type | Description |
|---|---|
ContextManager | Current machine context |
EventBehavior | Event that triggered the transition |
State | Current machine state |
EventCollection | Event history |
array | Arguments passed to behavior |
Behavior Arguments
Pass arguments to behaviors:
// In configuration
'actions' => 'addValue:10,20', // Passes [10, 20]
// In behavior
public function __invoke(ContextManager $context, array $arguments): void
{
[$amount, $multiplier] = $arguments;
$context->total += $amount * $multiplier;
}Required Context
Declare required context keys:
class ProcessOrderAction extends ActionBehavior
{
public static array $requiredContext = [
'orderId' => 'string',
'items' => 'array',
'total' => 'numeric',
];
public function __invoke(ContextManager $context): void
{
// Context is guaranteed to have these keys
}
}If required context is missing, MissingMachineContextException is thrown.
Behavior Execution Flow
Logging
Enable logging for debugging:
class DebugAction extends ActionBehavior
{
public bool $shouldLog = true;
public function __invoke(ContextManager $context): void
{
// This execution will be logged
}
}Raising Events
Behaviors can queue events:
class ProcessAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
$context->processed = true;
// Queue event for processing after current transition
$this->raise(['type' => 'PROCESSING_COMPLETE']);
}
}See Raised Events for details.
Faking Behaviors
For testing, behaviors can be faked:
// In test
ProcessOrderAction::fake();
ProcessOrderAction::shouldRun()
->once()
->andReturnUsing(function (ContextManager $context) {
$context->processed = true;
});
// Run machine
$machine->send(['type' => 'PROCESS']);
// Assert
ProcessOrderAction::assertRan();See Fakeable Behaviors for details.
Best Practices
1. Keep Behaviors Focused
// Good - single responsibility
class IncrementCountAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
$context->count++;
}
}
// Avoid - multiple responsibilities
class DoEverythingAction extends ActionBehavior
{
public function __invoke(ContextManager $context): void
{
$context->count++;
$this->sendEmail();
$this->updateDatabase();
$this->notifySlack();
}
}2. Use Classes for Complex Logic
// Simple - inline is fine
'guards' => [
'isPositive' => fn($ctx) => $ctx->count > 0,
],
// Complex - use a class
'guards' => [
'isValidOrder' => ValidateOrderGuard::class,
],3. Declare Required Context
class RequireContextAction extends ActionBehavior
{
public static array $requiredContext = [
'userId' => 'string',
'amount' => 'numeric',
];
}4. Use Dependency Injection
class SendNotificationAction extends ActionBehavior
{
public function __construct(
private readonly NotificationService $notifications,
) {}
public function __invoke(ContextManager $context): void
{
$this->notifications->send($context->userId, 'Order processed');
}
}