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
| Name | Type | Description |
|---|---|---|
| $artifacts | Milpa\Orchestrator\DecisionSurfaceFactoryInterface | |
| $gateService | Milpa\Workflow\Contracts\GateServiceInterface |
HumanGate::openFor()
public function openFor(Milpa\EventStore\EventStoreInterface $store, Milpa\Orchestrator\ProcessInstance $instance, string $requester): Milpa\Orchestrator\PendingDecisionOpens `$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
| Name | Type | Description |
|---|---|---|
| $store | Milpa\EventStore\EventStoreInterface | |
| $instance | Milpa\Orchestrator\ProcessInstance | |
| $requester | string |
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\EventResolves `$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
| Name | Type | Description |
|---|---|---|
| $store | Milpa\EventStore\EventStoreInterface | |
| $instance | Milpa\Orchestrator\ProcessInstance | |
| $gateId | string | |
| $decision | string | |
| $principal | string |
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\PendingDecisionRead-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
| Name | Type | Description |
|---|---|---|
| $store | Milpa\EventStore\EventStoreInterface | |
| $instance | Milpa\Orchestrator\ProcessInstance |