Skip to content

Data Flow & Context Isolation

Machine delegation uses explicit data flow — no implicit sharing between parent and child contexts.

Data Flow Overview

Parent Context

    ├── 'input' resolves ──→ Child Context (initial)
    │   (MachineInput class,      │
    │    closure, or array)       ├── Child lives its own lifecycle
    │                             │   (entry → events → transitions → actions)
    │                             │
    │                             └── Child reaches final state
    │                                   │
    │                                   ├── 'output' resolves (MachineOutput, OutputBehavior, array)
    │                                   └── Typed or untyped output

    ├── Available in @done event ◄───────┘
    │     {
    │       output:        <MachineOutput DTO, OutputBehavior output, or filtered context>,
    │       output_class:  <MachineOutput FQCN for typed reconstruction>,
    │       machine_id:    <child's root_event_id>,
    │       machine_class: <child's FQCN>,
    │       final_state:   <child's final state key>,
    │     }

    └── @done actions write to parent context
        (typed MachineOutput injected by type-hint)

Parent → Child: The input Key

The input key controls what data the child receives from the parent. Three formats are supported:

MachineInput Class (Typed)

php
'delegating' => [
    'machine' => PaymentMachine::class,
    'input'   => PaymentInput::class,  // auto-resolved from parent context
],

The framework calls PaymentInput::fromContext($parentContext) — constructor param names match camelCase context keys. Missing required params throw MachineInputValidationException.

See Typed Contracts for MachineInput details.

Closure Adapter

php
'input' => function (ContextManager $ctx): PaymentInput {
    return new PaymentInput(
        orderId: $ctx->get('currentOrderId'),   // name mapping
        amount: $ctx->get('totalAmount'),
    );
},

Use closures when parent context key names don't match child's input param names.

Array Format (Untyped)

php
'input' => ['orderId', 'amount'],                  // same-name keys
'input' => ['amount' => 'totalAmount'],             // key rename mapping

Without input, the child starts with its own default context. No parent data is transferred automatically.

Input Lifecycle

  1. CreatedChildMachineJob (async) or handleMachineInvoke() (sync) resolves input
  2. Validated — against child's declared input type (if child config has 'input' => PaymentInput::class)
  3. Merged into context — input properties auto-merged into child's initial context
  4. Consumed — the DTO is gone. Data lives in context from here.

Child → Parent: The output Key

The output key on a state controls which context values are exposed to the parent. Supports four formats:

MachineOutput Class (Typed)

php
'completed' => [
    'type'   => 'final',
    'output' => PaymentOutput::class,  // auto-resolved from child context
],

See Typed Contracts for MachineOutput details.

OutputBehavior Class (Computed)

php
'completed' => [
    'type'   => 'final',
    'output' => ComputedPaymentOutput::class,  // OutputBehavior with __invoke()
],

Array Format

php
'approved' => [
    'type'   => 'final',
    'output' => ['paymentId', 'status'],  // only these keys are exposed
],

Closure Format

php
'approved' => [
    'type'   => 'final',
    'output' => fn(ContextManager $ctx) => [
        'paymentId' => $ctx->get('paymentId'),
        'total'     => $ctx->get('amount') + $ctx->get('tax'),
    ],
],

When no output key is defined, the full child context is returned (default behavior).

Child → Parent: The @done Event

When the child reaches a final state, @done fires with a ChildMachineDoneEvent. With typed contracts, MachineOutput is injected by type-hint:

php
use Tarfinlabs\EventMachine\ContextManager;
use Tarfinlabs\EventMachine\Behavior\ActionBehavior;
use Tarfinlabs\EventMachine\Behavior\ChildMachineDoneEvent;

// Typed injection (when child uses MachineOutput)
class StorePaymentResultAction extends ActionBehavior
{
    public function __invoke(ContextManager $context, PaymentOutput $output): void
    {
        $context->set('paymentId', $output->paymentId);      // IDE autocomplete
        $context->set('transactionRef', $output->transactionRef);
    }
}

// Untyped access (when child uses array output)
class StorePaymentResultLegacy extends ActionBehavior
{
    public function __invoke(ContextManager $context, ChildMachineDoneEvent $event): void
    {
        $context->set('paymentId', $event->output('paymentId'));
        $context->set('status', $event->output('status'));
    }
}
AccessorReturn TypeDescription
output(?$key)mixedOutput data (filtered context, OutputBehavior output, or full context)
typedOutput()?MachineOutputTyped MachineOutput instance (null if untyped)
childMachineId()stringChild's root_event_id
childMachineClass()stringChild's FQCN
finalState()?stringThe child's final state key name

Child → Parent: The @fail Event

When the child throws an exception, @fail fires with a ChildMachineFailEvent. With typed contracts, MachineFailure is injected:

php
use Tarfinlabs\EventMachine\ContextManager;
use Tarfinlabs\EventMachine\Behavior\ActionBehavior;

// Typed injection (when child declares 'failure' config key)
class HandlePaymentFailureAction extends ActionBehavior
{
    public function __invoke(ContextManager $context, PaymentFailure $failure): void
    {
        $context->set('errorCode', $failure->errorCode);
        $context->set('errorDetail', $failure->gatewayResponse);
    }
}
AccessorReturn TypeDescription
errorMessage()?stringError message from exception
errorCode()int|string|nullError code from exception
typedFailure()?MachineFailureTyped MachineFailure instance (null if untyped)
childMachineId()stringChild's root_event_id
childMachineClass()stringChild's FQCN
output(?$key)mixedChild's context at failure time

input/output Symmetry

DirectionConfig KeyFormatsPurpose
Parent → ChildinputMachineInput class, closure, arrayControls what data child receives
Child → ParentoutputMachineOutput class, OutputBehavior, array, closureControls what data parent receives
Child → Parent (error)failure (machine config)MachineFailure classMaps exceptions to structured errors

Auto-Injected Context Keys

When a child machine is created via delegation, special keys are auto-injected into the child context:

KeyValuePurpose
_machine_idChild's own root_event_idSelf-identification (e.g., webhook URLs)
_parent_root_event_idParent's root_event_idEnables sendToParent()

Access via typed methods:

php
$context->machineId();           // child's own root_event_id
$context->parentMachineId();     // parent's root_event_id
$context->parentMachineClass();  // parent's FQCN

These are stored as separate properties on ContextManager, not in the data array.

Forward Response Data Flow

Forward events go directly to the HTTP response. The parent context is NOT modified.

Forward Event (HTTP request)
    ├── Validated by child's EventBehavior
    ├── Routed: parent.send() → tryForwardEventToChild() → child.send()
    ├── Child transitions
    ├── Child output resolved via $machine->output()
    └── Response built
          ├── Default: { id, state, output: <child's output> }
          ├── output (array): filtered child context
          └── output (class): parent's OutputBehavior (child MachineOutput injected)

When a forward entry specifies an output class, the parent's OutputBehavior runs. The child's typed MachineOutput is injected by type-hint:

php
use Tarfinlabs\EventMachine\ContextManager;
use Tarfinlabs\EventMachine\Behavior\OutputBehavior;

class PaymentStepOutput extends OutputBehavior
{
    public function __invoke(ContextManager $context, VerifyingOutput $childOutput): array
    {
        return [
            'orderId'   => $context->get('orderId'),        // Parent context
            'cardLast4' => $childOutput->cardLast4,          // Child typed output
            'step'      => $childOutput->step,
        ];
    }
}

Testing Data Flow

php
PaymentMachine::fake(output: new PaymentOutput(paymentId: 'pay_123', status: 'settled'));

OrderMachine::test()
    ->send('START_PAYMENT')
    ->assertContext('paymentId', 'pay_123');

Full Testing Guide

See Delegation Testing for more examples.

Released under the MIT License.