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
{
    public static function getType(): string
    {
        return 'SUBMIT_ORDER';
    }
}

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:

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

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';
    }
}

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 [
            'user_id' => 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 Support

Events can specify scenarios:

php
$machine->send([
    'type' => 'SUBMIT',
    'payload' => [
        'scenarioType' => 'test',  // Use 'test' scenario
        'data' => [...],
    ],
]);

See Scenarios 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

php
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

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';
}

Released under the MIT License.