Event Behaviors
Event behaviors define the structure, validation, and metadata for events sent to your machine.
Basic Event Class
··· 1 hidden line
class SubmitOrderEvent extends EventBehavior
{
public static function getType(): string
{
return 'SUBMIT_ORDER';
}
}Event Registration
Register event classes in the behavior configuration:
MachineDefinition::define(
config: [
'states' => [
'cart' => [
'on' => [
'SUBMIT_ORDER' => 'processing',
// Or use class directly
SubmitOrderEvent::class => 'processing',
],
],
],
],
behavior: [
'events' => [
'SUBMIT_ORDER' => SubmitOrderEvent::class,
],
],
);Sending Events
// As array
$machine->send(['type' => 'SUBMIT_ORDER']);
// As array with payload
$machine->send([
'type' => 'SUBMIT_ORDER',
'payload' => ['express' => true],
]);
// As event class
$machine->send(new SubmitOrderEvent());Event with Typed Properties
class AddItemEvent extends EventBehavior
{
public function __construct(
public readonly int $productId,
public readonly int $quantity = 1,
public readonly ?float $customPrice = null,
) {
parent::__construct();
}
public static function getType(): string
{
return 'ADD_ITEM';
}
}
// Usage - constructor
$machine->send(new AddItemEvent(
productId: 123,
quantity: 2,
customPrice: 29.99,
));
// Usage - from() static method (via Spatie Laravel Data)
$machine->send(AddItemEvent::from([
'productId' => 123,
'quantity' => 2,
]));The from() Method
The from() static method is provided by Spatie's Laravel Data package, which EventBehavior extends. It creates an instance from an array, handling type casting and validation automatically. You can use either the constructor or from() based on your preference.
Event Validation
Using Laravel Rules
··· 1 hidden line
class SubmitOrderEvent extends EventBehavior
{
public static function getType(): string
{
return 'SUBMIT_ORDER';
}
public static function rules(): array
{
return [
'payload.items' => ['required', 'array', 'min:1'],
'payload.shipping_address' => ['required', 'string', 'min:10'],
'payload.payment_method' => ['required', 'in:card,bank,cash'],
];
}
public static function messages(): array
{
return [
'payload.items.required' => 'Your cart is empty',
'payload.items.min' => 'Add at least one item to checkout',
'payload.shipping_address.required' => 'Shipping address is required',
];
}
}Validation in Action
try {
$machine->send([
'type' => 'SUBMIT_ORDER',
'payload' => [
'items' => [], // Invalid
],
]);
} catch (MachineEventValidationException $e) {
// 'Your cart is empty'
$errors = $e->errors();
}Using Spatie Data Attributes
use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
class TransferEvent extends EventBehavior
{
public function __construct(
#[Required]
#[IntegerType]
#[Min(1)]
public int $amount,
#[Required]
public string $recipientId,
public ?string $note = null,
) {
parent::__construct();
}
public static function getType(): string
{
return 'TRANSFER';
}
}Event Properties
type
The event identifier:
public static function getType(): string
{
return 'ORDER_SUBMITTED';
}payload
Event data passed through arrays:
$machine->send([
'type' => 'ADD_ITEM',
'payload' => [
'productId' => 123,
'quantity' => 2,
],
]);
// Access in behaviors
$event->payload['productId'];isTransactional
Whether to wrap the transition in a database transaction:
··· 1 hidden line
class CriticalEvent extends EventBehavior
{
public bool $isTransactional = true; // Default
public static function getType(): string
{
return 'CRITICAL_OPERATION';
}
}
class FastEvent extends EventBehavior
{
public bool $isTransactional = false;
public static function getType(): string
{
return 'QUICK_UPDATE';
}
}actor
Track who triggered the event:
··· 2 hidden lines
class SubmitEvent extends EventBehavior
{
public static function getType(): string
{
return 'SUBMIT';
}
public function actor(ContextManager $context): mixed
{
return auth()->user()?->id ?? 'system';
}
}source
Event origin (set automatically):
use Tarfinlabs\EventMachine\Enums\SourceType;
$event->source; // SourceType::EXTERNAL (user-sent)
// SourceType::INTERNAL (system-generated)Event Sources Explained
external: Events sent via$machine->send()- user actions, API calls, webhook triggersinternal: System-generated events like state entry/exit, machine initialization, action completion, raised events from actions
You can filter events by source when querying history:
$userEvents = $machine->state->history->filter(
fn($event) => $event->source === SourceType::EXTERNAL
);version
Event versioning for schema evolution:
··· 1 hidden line
class SubmitEventV2 extends EventBehavior
{
public int $version = 2;
public static function getType(): string
{
return 'SUBMIT';
}
}Practical Examples
E-commerce Events
··· 1 hidden line
class AddToCartEvent extends EventBehavior
{
public static function getType(): string
{
return 'ADD_TO_CART';
}
public static function rules(): array
{
return [
'payload.product_id' => 'required|integer|exists:products,id',
'payload.quantity' => 'required|integer|min:1|max:100',
];
}
}
class CheckoutEvent extends EventBehavior
{
public static function getType(): string
{
return 'CHECKOUT';
}
public static function rules(): array
{
return [
'payload.shipping_method' => 'required|in:standard,express,overnight',
'payload.payment_method' => 'required|in:card,paypal,bank',
'payload.address_id' => 'required|exists:addresses,id',
];
}
}Financial Events
··· 2 hidden lines
class TransferFundsEvent extends EventBehavior
{
public bool $isTransactional = true;
public static function getType(): string
{
return 'TRANSFER_FUNDS';
}
public static function rules(): array
{
return [
'payload.amount' => 'required|numeric|min:0.01|max:1000000',
'payload.from_account' => 'required|exists:accounts,id',
'payload.to_account' => 'required|exists:accounts,id|different:payload.from_account',
'payload.description' => 'nullable|string|max:255',
];
}
public function actor(ContextManager $context): mixed
{
return [
'user_id' => auth()->id(),
'ip' => request()->ip(),
];
}
}Workflow Events
··· 2 hidden lines
class ApproveRequestEvent extends EventBehavior
{
public static function getType(): string
{
return 'APPROVE';
}
public static function rules(): array
{
return [
'payload.comment' => 'nullable|string|max:500',
'payload.conditions' => 'nullable|array',
];
}
public function actor(ContextManager $context): mixed
{
$user = auth()->user();
return [
'id' => $user->id,
'name' => $user->name,
'role' => $user->role,
];
}
}
class RejectRequestEvent extends EventBehavior
{
public static function getType(): string
{
return 'REJECT';
}
public static function rules(): array
{
return [
'payload.reason' => 'required|string|min:10|max:500',
];
}
}Scenario Support
Events can specify scenarios:
$machine->send([
'type' => 'SUBMIT',
'payload' => [
'scenarioType' => 'test', // Use 'test' scenario
'data' => [...],
],
]);See Scenarios for details.
Event in Actions
Access event data in behaviors:
··· 3 hidden lines
class ProcessOrderAction extends ActionBehavior
{
public function __invoke(
ContextManager $context,
EventBehavior $event,
): void {
// Access typed properties (class events)
if ($event instanceof AddItemEvent) {
$productId = $event->productId;
}
// Access payload (array events)
$productId = $event->payload['productId'] ?? null;
// Access event type
$type = $event->type;
}
}Testing Events
it('validates event payload', function () {
$machine = OrderMachine::create();
expect(fn() => $machine->send([
'type' => 'ADD_ITEM',
'payload' => [
'quantity' => -1, // Invalid
],
]))->toThrow(MachineEventValidationException::class);
});
it('processes valid event', function () {
$machine = OrderMachine::create();
$machine->send(new AddItemEvent(
productId: 123,
quantity: 2,
));
expect($machine->state->context->items)->toHaveCount(1);
});Best Practices
1. Use Descriptive Event Names
// Good
'ORDER_SUBMITTED'
'PAYMENT_PROCESSED'
'INVENTORY_RESERVED'
// Avoid
'SUBMIT'
'PROCESS'
'UPDATE'2. Validate at Event Level
··· 1 hidden line
class SubmitEvent extends EventBehavior
{
public static function rules(): array
{
return [
'payload.items' => 'required|array|min:1',
];
}
}3. Use Classes for Complex Events
// Simple event - array is fine
$machine->send(['type' => 'CANCEL']);
// Complex event - use class
$machine->send(new TransferEvent(
amount: 1000,
recipientId: 'user-123',
));4. Include Actor Information
public function actor(ContextManager $context): mixed
{
return auth()->user() ?? 'anonymous';
}