Skip to main content

Routing messages

Route messages to publishers​

Dirthara\Messaging\RoutingMessagePublisher is a MessagePublisher that passes each message on to another publisher, chosen by the message's class:

use Dirthara\Messaging\RoutingMessagePublisher;

$publisher = new RoutingMessagePublisher();

$publisher->route(
GenerateInvoice::class,
$queuePublisher,
);

$publisher->route(
SynchroniseCustomer::class,
$crmPublisher,
);

$publisher->publish(
new GenerateInvoice($invoiceId),
);

GenerateInvoice goes to $queuePublisher, and SynchroniseCustomer to $crmPublisher. The code that publishes depends only on MessagePublisher, and does not know which destination each message has.

publish() passes the message it is given, the same instance, to the routed publisher exactly once. It does not copy, change, serialise, or wrap it.

Configure routes through MessageRouter​

Registering routes is described by its own contract, Dirthara\Messaging\Contract\MessageRouter, which RoutingMessagePublisher implements:

namespace Dirthara\Messaging\Contract;

use Dirthara\Messaging\Exception\MessagingException;

interface MessageRouter
{
/**
* @param class-string $message
*
* @throws MessagingException
*/
public function route(string $message, MessagePublisher $publisher): void;
}

A router that cannot accept a route throws a MessagingException, so code that configures routes can catch one type whichever router it is given. RoutingMessagePublisher throws the specific exceptions listed below.

Code that sets routes up, such as a module registering its own messages, can depend on MessageRouter, while code that publishes depends on MessagePublisher. Neither needs the concrete class:

use Dirthara\Messaging\Contract\MessageRouter;
use Dirthara\Messaging\Contract\MessagePublisher;

final readonly class BillingMessages
{
public function __construct(
private MessagePublisher $queue,
) {}

public function register(MessageRouter $router): void
{
$router->route(GenerateInvoice::class, $this->queue);
}
}

The same RoutingMessagePublisher instance is passed as the MessageRouter while the application configures it, and as the MessagePublisher to the code that publishes. The rules below are those of RoutingMessagePublisher.

Routes match the exact class​

A route matches messages whose class is exactly the routed type. It does not match a subclass of that type, or a class that implements it:

$publisher->route(TakePayment::class, $payments);

$publisher->publish(new TakePayment($paymentId)); // routed to $payments
$publisher->publish(new TakeDeposit($paymentId)); // TakeDeposit extends TakePayment: no route

This is deliberately unlike listening for events in Dirthara Events, where a listener for a parent class or interface applies to every event of that type. Many listeners can react to one event, but exactly one publisher takes responsibility for a message. If routes matched parent classes and interfaces, a message implementing two routed interfaces would have two owners, and the router would need rules to pick one.

Routes can be registered for:

TypeAccepted
A concrete classYes
An enumYes; its cases are routed
An anonymous classYes, by its generated class name
An interfaceNo: no object's exact class is an interface
An abstract classNo: no object's exact class is an abstract class
A trait, or a name that does not existNo

A rejected type throws InvalidMessageTypeException from route(), so a mistake in the routes shows up when the application configures them, not when it first publishes. Class names are case-insensitive in PHP; a type written in other letter case or with a leading backslash is routed as the class it names.

There are no wildcard routes, fallback routes, priorities, or routes to several publishers at once.

A type is routed once​

A second route for the same type throws DuplicateMessageRouteException:

$publisher->route(GenerateInvoice::class, $first);
$publisher->route(GenerateInvoice::class, $second); // throws

The first route stays in place. A route cannot be replaced or removed, because two routes for one message would leave it unclear which publisher is responsible for it.

An unrouted message is refused​

Publishing a message whose class has no route throws MessageRouteNotFoundException. The message is not discarded silently, and it is not sent anywhere else.

Failures of the routed publisher​

An exception or error thrown by the routed publisher leaves publish() unchanged; the router does not wrap it. It does not retry the message or try another publisher either. Retrying is up to the publisher that owns the message.

Exceptions​

Every exception implements Dirthara\Messaging\Exception\MessagingException, and carries its context in the context property.

ExceptionExtendsThrown when
InvalidMessageTypeExceptionInvalidArgumentExceptionroute() is given a type that cannot be routed.
DuplicateMessageRouteExceptionInvalidArgumentExceptionroute() is given a type that already has a route.
MessageRouteNotFoundExceptionRuntimeExceptionpublish() is given a message whose class has no route.

Each exception's context holds the message type under message, and never the message itself, whose contents could be sensitive. Control characters in the type, which an anonymous class name contains, are escaped in the context and in the exception message, so the type cannot forge a line in a log.