Skip to content

Raised Events ​

Raised events allow actions to queue additional events for processing after the current transition completes. This enables complex workflows where one action triggers subsequent state changes.

Basic Usage ​

Use $this->raise() in an action to queue an event:

php
use Tarfinlabs\EventMachine\Behavior\ActionBehavior;

class ProcessOrderAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->processed = true;

        // Queue an event
        $this->raise(['type' => 'ORDER_PROCESSED']);
    }
}

Event Queue Processing ​

  1. Action executes and calls raise()
  2. Events are queued (not processed immediately)
  3. Current transition completes
  4. Entry actions execute
  5. Queued events process one by one

Raise with Payload ​

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\ContextManager; class CalculateAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $result = $this->calculate($context);

        $this->raise([
            'type' => 'CALCULATION_COMPLETE',
            'payload' => [
                'value' => $result,
                'timestamp' => now()->toIso8601String(),
            ],
        ]);
    }
}

Multiple Raised Events ​

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\ContextManager; class ProcessAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        // Process items
        foreach ($context->items as $item) {
            $this->raise([
                'type' => 'ITEM_PROCESSED',
                'payload' => ['itemId' => $item['id']],
            ]);
        }

        // Final event
        $this->raise(['type' => 'ALL_ITEMS_PROCESSED']);
    }
}

Events are processed in the order they were raised.

Practical Examples ​

Order Processing Chain ​

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\ContextManager; class StartProcessingAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->processingStarted = true;

        // Trigger validation
        $this->raise(['type' => 'VALIDATE']);
    }
}

class ValidateOrderAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        if ($this->isValid($context)) {
            $context->isValid = true;
            $this->raise(['type' => 'VALIDATION_PASSED']);
        } else {
            $context->validationErrors = $this->getErrors($context);
            $this->raise(['type' => 'VALIDATION_FAILED']);
        }
    }
}

Workflow with Notifications ​

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\ContextManager; class ApproveRequestAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->approvedAt = now();
        $context->approvedBy = auth()->id();

        // Trigger notification events
        $this->raise([
            'type' => 'NOTIFY_REQUESTER',
            'payload' => ['message' => 'Your request has been approved'],
        ]);

        $this->raise([
            'type' => 'NOTIFY_TEAM',
            'payload' => ['message' => 'Request #' . $context->requestId . ' approved'],
        ]);
    }
}

Conditional Event Raising ​

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\ContextManager; class ProcessPaymentAction extends ActionBehavior
{
    public function __construct(
        private readonly PaymentGateway $gateway,
    ) {}

    public function __invoke(ContextManager $context): void
    {
        try {
            $result = $this->gateway->charge(
                $context->paymentMethod,
                $context->total,
            );

            $context->paymentId = $result->id;
            $this->raise(['type' => 'PAYMENT_SUCCESS']);

        } catch (PaymentDeclinedException $e) {
            $context->paymentError = $e->getMessage();
            $this->raise(['type' => 'PAYMENT_DECLINED']);

        } catch (PaymentException $e) {
            $context->paymentError = $e->getMessage();
            $this->raise(['type' => 'PAYMENT_ERROR']);
        }
    }
}

State Machine with Raised Events ​

php

use Tarfinlabs\EventMachine\Definition\MachineDefinition; MachineDefinition::define(
    config: [
        'id' => 'order',
        'initial' => 'pending',
        'states' => [
            'pending' => [
                'on' => [
                    'SUBMIT' => [
                        'target' => 'validating',
                        'actions' => 'startValidationAction',
                    ],
                ],
            ],
            'validating' => [
                'entry' => 'validateOrderAction',
                'on' => [
                    'VALIDATION_PASSED' => 'processing',
                    'VALIDATION_FAILED' => 'rejected',
                ],
            ],
            'processing' => [
                'entry' => 'processPaymentAction',
                'on' => [
                    'PAYMENT_SUCCESS' => 'confirmed',
                    'PAYMENT_DECLINED' => 'payment_failed',
                    'PAYMENT_ERROR' => 'error',
                ],
            ],
            'confirmed' => [
                'entry' => 'sendConfirmationAction',
            ],
            'rejected' => [],
            'payment_failed' => [
                'on' => ['RETRY' => 'processing'],
            ],
            'error' => [],
        ],
    ],
    behavior: [
        'actions' => [
            'validateOrderAction' => ValidateOrderAction::class,
            'processPaymentAction' => ProcessPaymentAction::class,
            'sendConfirmationAction' => SendConfirmationAction::class,
        ],
    ],
);

Chained Actions with Events ​

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\ContextManager; class XAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->value .= 'x';
        $this->raise(['type' => '@x']);
    }
}

class YAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->value .= 'y';
        $this->raise(['type' => '@y']);
    }
}

// Machine definition
'states' => [
    '#a' => [
        'entry' => '!x',  // XAction
        'on' => ['@x' => '#x'],
    ],
    '#x' => [
        'entry' => '!y',  // YAction
        'on' => ['@y' => '#y'],
    ],
    '#y' => [],
],

// Result after starting:
// 1. Enter #a, run XAction (value='x'), raise @x
// 2. Entry complete, process @x, transition to #x
// 3. Enter #x, run YAction (value='xy'), raise @y
// 4. Entry complete, process @y, transition to #y
// Final: state=#y, context.value='xy'

Event History ​

Raised events appear in the event history:

php
$machine = OrderMachine::create();
$machine->send(['type' => 'SUBMIT']);

$history = $machine->state->history;

// History includes:
// - 'order.event.@validation.raised' (internal event)
// - '@validation' (the raised event)
// - ... subsequent events

Entry Actions and Raised Events ​

Important: Entry actions complete before raised events are processed.

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\ContextManager; class EntryAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->executionOrder[] = 'entry';
    }
}

class TransitionAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        $context->executionOrder[] = 'transition';
        $this->raise(['type' => 'NEXT']);
    }
}

// Order of execution:
// 1. transition action (calls raise())
// 2. target state entry action
// 3. NEXT event processed

Testing Raised Events ​

php

use Tarfinlabs\EventMachine\Definition\MachineDefinition; it('processes raised events in order', function () {
    $machine = MachineDefinition::define(
        config: [
            'initial' => 'a',
            'context' => ['log' => []],
            'states' => [
                'a' => [
                    'entry' => 'logAndRaiseAction',
                    'on' => ['NEXT' => 'b'],
                ],
                'b' => [
                    'entry' => 'logBAction',
                ],
            ],
        ],
        behavior: [
            'actions' => [
                'logAndRaiseAction' => function ($context) {
                    $context->log[] = 'a';
                    // Can't use $this->raise() in inline function
                    // Use class-based action instead
                },
                'logBAction' => fn($ctx) => $ctx->log[] = 'b',
            ],
        ],
    );
});

Full Testing Guide

For transition and raised event testing, see Transitions & Paths.

Best Practices ​

1. Use for Workflow Progression ​

php
// Good - clear workflow progression
$this->raise(['type' => 'VALIDATION_COMPLETE']);
$this->raise(['type' => 'READY_FOR_PAYMENT']);

// Avoid - generic events
$this->raise(['type' => 'NEXT']);
$this->raise(['type' => 'DONE']);

2. Handle Failures Explicitly ​

php
try {
    $result = $this->process();
    $this->raise(['type' => 'SUCCESS', 'payload' => $result]);
} catch (Exception $e) {
    $this->raise([
        'type' => 'FAILURE',
        'payload' => ['error' => $e->getMessage()],
    ]);
}

3. Use Class-Based Actions ​

Raised events require $this->raise(), which is only available in class-based actions:

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; // Works - class-based action
class MyAction extends ActionBehavior
{
    public function __invoke($context): void
    {
        $this->raise(['type' => 'EVENT']);
    }
}

// Doesn't work - inline function
'actions' => [
    'doSomethingAction' => fn($ctx) => $this->raise(['type' => 'EVENT']), // Error! `raise()` not available
],

See the linked guide for more patterns.

Detailed Guide

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

Actor Propagation ​

When an action raises an event, the actor from the triggering event is automatically inherited if not explicitly set. This eliminates the need to manually pass actor: in every raise() call.

Before (v7) ​

php
class ApproveAction extends ActionBehavior
{
    public function __invoke(ContextManager $context, EventBehavior $event): void
    {
        // Had to manually pass actor on every raise
        $this->raise(new NotifyEvent(
            payload: ['message' => 'Approved'],
            actor: $event->actor($context),  // manual boilerplate
        ));
    }
}

After (v8) ​

php
class ApproveAction extends ActionBehavior
{
    public function __invoke(ContextManager $context): void
    {
        // Actor is auto-inherited from the triggering event
        $this->raise(new NotifyEvent(
            payload: ['message' => 'Approved'],
        ));
    }
}

Rules ​

ScenarioResult
raise() without actorInherited from triggering event
raise() with explicit actorExplicit wins
Chained raises (A → B → C)Each inherits from previous
Event class with actor() overrideOverride wins
Timer / lifecycle events → raiseNo propagation (no actor on trigger)

Explicit Always Wins

If you set actor: explicitly on a raised event, it takes precedence over the auto-propagated value. This lets you override the actor for specific events in a chain.

Raised Events with Parallel Dispatch ​

When Parallel Dispatch is enabled, raised events from region entry actions are captured and processed under lock in the same job scope:

  1. Entry action runs (no lock) → calls $this->raise()
  2. Events are captured from the event queue
  3. Job acquires database lock
  4. Raised events are processed under lock via transition()
  5. Each job processes only its own raised events — no cross-contamination

This means raised events work identically whether dispatch is enabled or disabled. The only difference is where they run (queue worker vs. HTTP request).

Released under the MIT License.