AXIAM PHP SDK

ReactorServer
in package

FinalYes

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:

  1. It MUST NOT declare topology (§22.1) — enforced by the shape of ReactorTransport, which has no declare or bind method at all.
  2. 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_policy decide. An SDK that answered allow on behalf of a handler that crashed would have overridden a fail_closed setting from inside the library.
  3. It MUST NOT filter a patch to the allowed subset (§22.4 rule 1) — see ReactorAnswer::mutate().
  4. It SHOULD honour timeout_ms by 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_policy applies — 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).

On this page

Search results