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. Created — ChildMachineJob (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.