Skip to content
docsv0.1.0

HumanGate

Opens and resolves a process instance's gate — the event-sourced counterpart to `milpa/workflow`'s Doctrine-backed `GatePassageService`. This engine is zero-DB (an append-only {@see EventStoreInterface} log), so instead of `GatePassageService` (which requires a real `EntityManagerInterface` and `persist()`/`flush()`s every passage), `HumanGate` represents "the gate is open, awaiting a decision" as a `GateOpened` {@see Event} in the SAME log the process instance itself replays through. It still delegates the D9 anti-self-approval check to `milpa/workflow`'s {@see GateServiceInterface} — by default {@see InMemoryGateService}, the zero-DB implementation built for exactly this seam — rather than reimplementing that check inline: {@see self::resolve()} reconstructs a throwaway `GatePassage` carrying the ORIGINAL requester (read back from the log's `GateOpened` event) and calls `$gateService->approvePassage()` purely to obtain its self-approval guard; the passage's own approval-count/status bookkeeping is discarded, since the actual process advance is the literal decision event this class appends to `$store` regardless of what `approvePassage()` did to the throwaway passage. `$gateService->requestPassage()`/`approvePassage()` need a polymorphic `entityType`/`entityId` pair, but this engine has no such concept (a process instance's id is an opaque string, not an int-keyed entity) and the self-approval check does not depend on either value — so both are fixed, meaningless placeholders here (see {@see self::ENTITY_TYPE}), never read back by any caller of this class.

HumanGate::__construct()

public function __construct(Milpa\Orchestrator\DecisionSurfaceFactoryInterface $artifacts, Milpa\Workflow\Contracts\GateServiceInterface $gateService = new InMemoryGateService()):

Parameters

Parameters of __construct()
NameTypeDescription
$artifactsMilpa\Orchestrator\DecisionSurfaceFactoryInterface
$gateServiceMilpa\Workflow\Contracts\GateServiceInterface

HumanGate::openFor()

public function openFor(Milpa\EventStore\EventStoreInterface $store, Milpa\Orchestrator\ProcessInstance $instance, string $requester): Milpa\Orchestrator\PendingDecision

Opens `$instance`'s current gate for a human decision: appends a `GateOpened` event (payload: `requester` + the gate's `options`), then builds this gate's {@see DecisionSurfaceInterface} via the injected {@see DecisionSurfaceFactoryInterface} and returns the resulting {@see PendingDecision}.

Parameters

Parameters of openFor()
NameTypeDescription
$storeMilpa\EventStore\EventStoreInterface
$instanceMilpa\Orchestrator\ProcessInstance
$requesterstring

Throws

\RuntimeException when `$instance`'s current state has no gate

\InvalidArgumentException when the built artifact's options do not match the gate's transitions 1:1 (see {@see PendingDecision})

HumanGate::resolve()

public function resolve(Milpa\EventStore\EventStoreInterface $store, Milpa\Orchestrator\ProcessInstance $instance, string $gateId, string $decision, string $principal): Milpa\EventStore\Event

Resolves `$gateId` for `$instance` with `$decision`: validates `$decision` is one of the options the matching `GateOpened` event offered, delegates the D9 anti-self-approval check to the injected {@see GateServiceInterface} (see the class docblock), then appends `$decision` itself as the advancing {@see Event} — the SAME mechanism any trigger event uses to move the process forward (see {@see Reducer}).

Parameters

Parameters of resolve()
NameTypeDescription
$storeMilpa\EventStore\EventStoreInterface
$instanceMilpa\Orchestrator\ProcessInstance
$gateIdstring
$decisionstring
$principalstring

Throws

\RuntimeException when `$instance` is not currently awaiting `$gateId` (never opened, or already resolved — its current state no longer carries a gate whose code matches `$gateId`)

\InvalidArgumentException when `$decision` is not one of the options the gate was opened with

\Milpa\Workflow\Exceptions\SelfApprovalException when `$principal` equals the requester that opened `$gateId` (D9)

HumanGate::pendingFor()

public function pendingFor(Milpa\EventStore\EventStoreInterface $store, Milpa\Orchestrator\ProcessInstance $instance): ?Milpa\Orchestrator\PendingDecision

Read-only reconstruction of "what's the current pending decision on `$instance`, if any" — WITHOUT appending a new `GateOpened` event. Finds the latest `GateOpened` event on `$instance`'s own log and checks whether it is still UNRESOLVED (no later event, by `seq`, whose `type` is one of that `GateOpened`'s own `options`), then rebuilds the {@see PendingDecision} around it exactly like {@see self::openFor()} would have, minus the write. A caller wanting to list every pending decision (e.g. across every stream an {@see EventStoreInterface} knows about) must use this instead of {@see self::openFor()}, which would append a redundant `GateOpened` event on every call. {@see ProcessRunner} also uses it to decide whether a gate still needs opening before it calls {@see self::openFor()}. Returns `null` when: no `GateOpened` event exists for `$instance` at all; the latest one has already been resolved; `$instance`'s current state no longer carries a gate matching the latest `GateOpened`'s `gate_id`; or the injected {@see DecisionSurfaceFactoryInterface} throws while building the artifact (e.g. its domain lookup fails) — this method is read-only and reports "nothing pending" rather than propagating that failure.

Parameters

Parameters of pendingFor()
NameTypeDescription
$storeMilpa\EventStore\EventStoreInterface
$instanceMilpa\Orchestrator\ProcessInstance