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
{
// getType() auto-generates 'SUBMIT_ORDER' from class name — no override needed!
}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, auto-derived from the class name by stripping the Event suffix and converting to SCREAMING_SNAKE_CASE:
// OrderSubmittedEvent → 'ORDER_SUBMITTED' (automatic)
// Override only when auto-generation doesn't match:
public static function getType(): string
{
return 'CUSTOM_TYPE';
}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';
}
}Auto-Propagation
When events are raised via $this->raise(), the actor is automatically inherited from the triggering event if not explicitly set. See Raised Events — Actor Propagation.
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 [
'userId' => 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 Activation
Events can be sent with a scenario slug to activate behavior overrides in staging. See Scenarios — Endpoint Integration 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
EventBehavior::forTesting()
Events are data, not behavior — construct them easily, don't mock them:
// Base — sensible defaults
$event = IncreaseEvent::forTesting();
expect($event->type)->toBe('INCREASE');
// Override specific fields
$event = AddValueEvent::forTesting(['payload' => ['value' => 42]]);
expect($event->payload)->toBe(['value' => 42]);With runWithState
$state = State::forTesting(['count' => 10]);
$event = AddValueEvent::forTesting(['payload' => ['value' => 5]]);
AddValueAction::runWithState($state, eventBehavior: $event);
expect($state->context->count)->toBe(15);With Machine::test()
OrderMachine::test()
->send(PaymentEvent::forTesting()->toArray())
->assertState('paid');Validation Testing
it('validates event payload', function () {
$machine = OrderMachine::create();
expect(fn() => $machine->send([
'type' => 'ADD_ITEM',
'payload' => [
'quantity' => -1, // Invalid
],
]))->toThrow(MachineEventValidationException::class);
});Full Testing Guide
See Isolated Testing for forTesting() details.
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';
}Detailed Guide
For comprehensive design guidelines with Do/Don't examples, see Event Design.