Skip to main content

MessageRouter

Pluggable inbound routing interface used by ReceiverActor to resolve each incoming Messenger envelope to the Nexus ActorRef that should receive its message.

What it does

MessageRouter is a single-method interface. ReceiverActor calls route() for every envelope it drains from the transport. Returning null marks the message unroutable and triggers the UnroutablePolicy configured on ReceiverActorConfig (reject or dead-letters).

Two concrete implementations ship with the package:

MapMessageRouter

Exact PHP message class → ActorRef lookup. This is the right choice for most applications where message types are known at startup.

/**
* @param Route<object> ...$routes
*/
public function __construct(Route ...$routes)

Routes are built through the typed Route::to() boundary, which checks at analysis time that each target ref handles its routed message class. route() returns the registered ref for $message::class or null if the class is not in the map. The lookup is an O(1) array fetch.

StampMessageRouter

Cluster seam: resolves the TargetActorPathStamp on the envelope against a path-keyed registry. Use this when a remote producer stamps the target actor path (e.g., when routing across cluster nodes). Messages without the stamp, or with a path not in the registry, are unroutable.

/**
* @param array<string, ActorRef<object>> $registry keyed by actor-path string
* @param TargetAuthorizer|null $authorizer authorizes producer → target routing per envelope
*/
public function __construct(private array $registry, private ?TargetAuthorizer $authorizer = null)

route() reads $envelope->last(TargetActorPathStamp::class) and looks up $registry[$stamp->path].

Authorizing producer → target routes

Because the target is selected by a producer-controlled stamp, a producer with publish rights could otherwise invoke any registered target and consume its capacity (SEC-012). Pass a TargetAuthorizer to gate this: a resolved target is only returned if the authorizer permits the envelope's producer to reach it. A denied envelope is unroutable — the ReceiverActor rejects or dead-letters it per its policy — so an unauthorized producer never reaches the target actor.

MapTargetAuthorizer is a static allowlist mapping a producer identity (read from the ProducerIdentityStamp, wire header X-Nexus-Producer-Identity) to the exact target paths it may reach. It fails closed: no identity stamp, an unknown identity, or a target outside that identity's list is denied (and logged when a PSR-3 logger is supplied).

$router = new StampMessageRouter(
['/user/orders' => $ordersRef, '/user/payments' => $paymentsRef],
new MapTargetAuthorizer([
'orders-svc' => ['/user/orders'],
'billing-svc' => ['/user/payments'],
]),
);
Trust boundary

A ProducerIdentityStamp proves origin only as far as the producer is trusted. Across mutually untrusted producers the identity must be established or validated at a trusted boundary — an authenticated transport, a broker ACL that stamps identity, or a signed envelope — otherwise a producer can assert any identity. The authorizer enforces the ACL; the trust in the identity comes from the boundary that set it. Pair it with broker-side ACLs when producers are not mutually trusted.

Omitting the authorizer preserves the previous behavior (any producer may reach any registered target), so this is a backward-compatible, opt-in tightening.

Interface

interface MessageRouter
{
/**
* @return ActorRef<object>|null null means unroutable
*/
public function route(object $message, Envelope $envelope): ?ActorRef;
}

Example

src/bootstrap.php
use Monadial\Nexus\Messenger\Routing\MapMessageRouter;
use Monadial\Nexus\Messenger\Routing\Route;
use Monadial\Nexus\Messenger\Routing\StampMessageRouter;

// Type-based routing (most common)
$router = new MapMessageRouter(
Route::to(OrderPlaced::class, $ordersActor),
Route::to(PaymentMade::class, $paymentsActor),
);

// Path-stamp routing (cluster seam)
$router = new StampMessageRouter([
'/user/orders' => $ordersActor,
'/user/payments' => $paymentsActor,
]);

// Custom router: route on any envelope property
$router = new class implements MessageRouter {
public function route(object $message, Envelope $envelope): ?ActorRef {
// inspect stamps, message fields, etc.
return $message instanceof PriorityMessage
? $this->priorityRef
: $this->defaultRef;
}
};

Full API reference

MessageRouter interface · MapMessageRouter · StampMessageRouter

See also