Skip to content

Event Behaviors ​

Event behaviors define the structure, validation, and metadata for events sent to your machine.

Basic Event Class ​

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; 
class SubmitOrderEvent extends EventBehavior
{
    // getType() auto-generates 'SUBMIT_ORDER' from class name — no override needed!
}

Event Registration ​

Register event classes in the behavior configuration:

php
MachineDefinition::define(
    config: [
        'states' => [
            'cart' => [
                'on' => [
                    'SUBMIT_ORDER' => 'processing',
                    // Or use class directly
                    SubmitOrderEvent::class => 'processing',
                ],
            ],
        ],
    ],
    behavior: [
        'events' => [
            'SUBMIT_ORDER' => SubmitOrderEvent::class,
        ],
    ],
);

Sending Events ​

php
// 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 ​

php
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 ​

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; 
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 ​

php
try {
    $machine->send([
        'type' => 'SUBMIT_ORDER',
        'payload' => [
            'items' => [],  // Invalid
        ],
    ]);
} catch (MachineEventValidationException $e) {
    // 'Your cart is empty'
    $errors = $e->errors();
}

Using Spatie Data Attributes ​

php
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:

php
// 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:

php
$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:

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; 
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:

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; use Tarfinlabs\EventMachine\ContextManager; 
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):

php
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 triggers
  • internal: System-generated events like state entry/exit, machine initialization, action completion, raised events from actions

You can filter events by source when querying history:

php
$userEvents = $machine->state->history->filter(
    fn($event) => $event->source === SourceType::EXTERNAL
);

version ​

Event versioning for schema evolution:

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; 
class SubmitEventV2 extends EventBehavior
{
    public int $version = 2;

    public static function getType(): string
    {
        return 'SUBMIT';
    }
}

Practical Examples ​

E-commerce Events ​

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; 
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 ​

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; use Tarfinlabs\EventMachine\ContextManager; 
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 ​

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; use Tarfinlabs\EventMachine\ContextManager; 
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:

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\Behavior\EventBehavior; use Tarfinlabs\EventMachine\ContextManager; 
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:

php
// 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 ​

php
$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() ​

php
OrderMachine::test()
    ->send(PaymentEvent::forTesting()->toArray())
    ->assertState('paid');

Validation Testing ​

php
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 ​

php
// Good
'ORDER_SUBMITTED'
'PAYMENT_PROCESSED'
'INVENTORY_RESERVED'

// Avoid
'SUBMIT'
'PROCESS'
'UPDATE'

2. Validate at Event Level ​

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; 
class SubmitEvent extends EventBehavior
{
    public static function rules(): array
    {
        return [
            'payload.items' => 'required|array|min:1',
        ];
    }
}

3. Use Classes for Complex Events ​

php
// 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 ​

php
public function actor(ContextManager $context): mixed
{
    return auth()->user() ?? 'anonymous';
}

Detailed Guide

For comprehensive design guidelines with Do/Don't examples, see Event Design.

Released under the MIT License.