ReactorServer
in package
The CONTRACT.md §22 reactor runtime — §22.10's `reactor_serve`, spelled {@see self::reactorServe()} in PHP by that subsection's per-language table.
A Reactor is an external process that subscribes to named hook events on the AXIAM AMQP bus and answers back — allow, deny, or a field-allow-listed mutation — inside a timeout the server declared. Zitadel Actions and Keycloak SPIs solve the same problem by loading third-party code into the authorization server; a reactor stays outside it, reachable only through a signed reply schema the server validates before it believes a word of it.
Per delivery, in this order and no other (§22.3): reject key_version < 2,
verify the MAC, check freshness, check the nonce, decode the payload, dispatch
to the handler, sign and publish the reply. A runtime that hands an unverified
payload to user code has already lost.
THE FOUR RULES §22.10 PUTS ON THIS HELPER, AND WHERE EACH ONE LIVES:
- It MUST NOT declare topology (§22.1) — enforced by the shape of ReactorTransport, which has no declare or bind method at all.
- It MUST fail closed on its own errors. A handler that throws, an answer
this SDK refuses to send, a reply that will not serialize: every one of them
results in no reply, letting the operator's
failure_policydecide. An SDK that answeredallowon behalf of a handler that crashed would have overridden afail_closedsetting from inside the library. - It MUST NOT filter a patch to the allowed subset (§22.4 rule 1) — see ReactorAnswer::mutate().
- It SHOULD honour
timeout_msby abandoning work whose window has closed rather than replying late (§22.3) — see the deadline check in self::dispatchDelivery().
THIS IS NOT A WEB-REQUEST PATH. self::reactorServe() blocks for the
lifetime of the process and must run on a long-running runtime — a dedicated CLI
worker, never an FPM request. php-amqplib has no built-in automatic
reconnection, so as with this SDK's §8 consumer the loop returns when the
session ends and a process supervisor (systemd Restart=on-failure, a
Kubernetes restart policy, supervisord) brings it back. That is a deliberate,
documented deviation from the Go/Java runtimes' in-process reconnect loop, and
the same one this SDK already makes for §8.
Table of Contents
Constants
- NO_REPLY_LISTEN_MODE : mixed = 'listen_mode'
- No-reply category for a {@see ReactorEvents::MODE_LISTEN} registration: the handler ran and no reply was published because the server never reads one on this path (§22.5). Unlike every other no-reply category, it is not a failure and no `failure_policy` is consulted for it.
- NO_REPLY_PUBLISH_FAILED : mixed = 'publish_failed'
- No-reply category for a reply that could not be handed to the broker.
- WAIT_TICK_SECONDS : mixed = 1.0
- How long one {@see ReactorTransport::wait()} call blocks before the loop re-checks whether it has been asked to stop.
Methods
- __construct() : mixed
- dispatchDelivery() : void
- Verifies, dispatches and answers ONE delivery.
- reactorServe() : void
- §22.10's `reactor_serve`: consume the server-declared queue and answer every delivery until the session ends or {@see self::stop()} is called.
- stop() : void
- Asks {@see self::reactorServe()} to return after the delivery it is currently handling, if any.
Constants
NO_REPLY_LISTEN_MODE
No-reply category for a {@see ReactorEvents::MODE_LISTEN} registration: the handler ran and no reply was published because the server never reads one on this path (§22.5). Unlike every other no-reply category, it is not a failure and no `failure_policy` is consulted for it.
public
mixed
NO_REPLY_LISTEN_MODE
= 'listen_mode'
NO_REPLY_PUBLISH_FAILED
No-reply category for a reply that could not be handed to the broker.
public
mixed
NO_REPLY_PUBLISH_FAILED
= 'publish_failed'
WAIT_TICK_SECONDS
How long one {@see ReactorTransport::wait()} call blocks before the loop re-checks whether it has been asked to stop.
public
mixed
WAIT_TICK_SECONDS
= 1.0
A tick rather than an indefinite block, so self::stop() from a signal handler is honoured promptly and the deterministic-shutdown obligation of §18 is met without a second thread.
Methods
__construct()
public
__construct(ReactorConfig $config, ReactorTransport $transport, callable(ReactorEvent): ReactorAnswer $handler[, LoggerInterface $logger = new NullLogger() ][, callable(ReactorTelemetryEvent): void|null $telemetryHook = null ][, callable(): int|null $clock = null ][, callable(): string|null $nonceFactory = null ]) : mixed
Parameters
- $config : ReactorConfig
-
Identity, queue and signing key.
- $transport : ReactorTransport
-
The broker session. It has no way to declare topology (§22.1).
- $handler : callable(ReactorEvent): ReactorAnswer
-
Decides one event. Throwing means "I could not decide": no reply is published and the registration's
failure_policyapplies — which is the honest outcome, and the one an operator configured.In ReactorEvents::MODE_LISTEN the return value is IGNORED and no reply is ever published (§22.5). Write a listener handler IDEMPOTENTLY: a redelivery after a broker hiccup is normal, and a listener that double-counts is one that assumed an exactly-once delivery it was never promised.
- $logger : LoggerInterface = new NullLogger()
-
Security events only: the fact and category of a refusal, never the MAC, the key or the payload.
- $telemetryHook : callable(ReactorTelemetryEvent): void|null = null
-
§19 sink. Invoked on the dispatch path, so it must not block.
- $clock : callable(): int|null = null
-
Overridable "now" source (Unix seconds) for deterministic tests.
- $nonceFactory : callable(): string|null = null
-
Overridable reply-nonce source. Defaults to a fresh UUIDv4 per reply (§22.2).
dispatchDelivery()
Verifies, dispatches and answers ONE delivery.
public
dispatchDelivery(ReactorDelivery $delivery) : void
Public because it is the load-bearing, separately-testable unit — the same
reason this SDK's §8 consumer exposes verifyAndDispatch. Tests drive it
with a fake delivery and a fake transport; no broker is involved.
Parameters
- $delivery : ReactorDelivery
reactorServe()
§22.10's `reactor_serve`: consume the server-declared queue and answer every delivery until the session ends or {@see self::stop()} is called.
public
reactorServe() : void
It never declares an exchange, a queue or a binding (§22.1). It returns normally on a clean stop and on a lost session alike — there is no in-SDK reconnect loop, by the same design decision this SDK's §8 consumer records.
In-flight work is drained by construction rather than by a timer: deliveries are dispatched one at a time on the calling thread, so when this returns there is no half-finished dispatch left behind (§18).
stop()
Asks {@see self::reactorServe()} to return after the delivery it is currently handling, if any.
public
stop() : void
Safe to call from a pcntl signal handler: it sets a flag and nothing else,
so a dispatch already in progress finishes and answers rather than being
abandoned mid-decision (§18).