Durable, identifier-mapped, broker-agnostic workflows declared in plain PHP — on the database and broker you already operate. Sagas, orchestrators, chained handlers, timeouts, outbox, and distributed routing in one attribute-driven model.
Composer package · Laravel or Symfony · PostgreSQL or MySQL · RabbitMQ, Kafka, SQS, Redis, or DBAL outbox
Sagas persist state per identifier in your DB. The outbox commits business state and message dispatch in one DBAL transaction. Broker redelivery resumes work after crashes, restarts, and rolling deploys — no separate workflow runtime to deploy or operate.
#[Saga] for stateful processes that react to events arriving over time. #[Orchestrator] for declarative routing-slip workflows whose step list is visible at one glance. Chained #[InternalHandler] for stateless durable flows where the message carries the state.
#[Delayed] timeouts, #[Priority], #[TimeToLive], #[Deduplicated], per-handler failure isolation, retry/DLQ, CombinedMessageChannel outbox, #[Distributed] cross-service routing, multi-tenant dynamic channels — one declarative attribute model across every primitive.
Workflows survive crashes, restarts, and rolling deploys because state and dispatch live in infrastructure you already operate — no external runtime to deploy, no engine-shaped event history to maintain.
Step 1
A #[Saga] records its state in your own database, keyed by the correlation identifier the events carry.
Step 2
CombinedMessageChannel writes the business state change and the outgoing message into one DBAL transaction — no dual-write window.
Step 3
Worker dies mid-step? The broker redelivers; the saga reloads from the DB by identifier; built-in deduplication tolerates the duplicate; the work resumes.
Worker killed mid-step? The broker redelivers; the saga reloads from its identifier; the handler runs again. Built-in deduplication tolerates the duplicate. State + dispatch commit together via CombinedMessageChannel, so there is no dual-write window to leak inconsistent state.
#[Delayed] resumes a handler after a TimeSpan, an exact DateTime, or an expression — no cron, no separate timer service. The broker's underlying delayed-message primitive does the waiting; the attribute model hides the broker specifics from your code.
Each subscriber receives its own copy of the message. A failing handler retries on its own envelope; sibling handlers are unaffected. Retry strategy is per-handler, not per-transport — the failure of one step never replays a side effect on another.
#[Saga] binds events to instances by payload field, header, or expression via identifierMapping. State is loaded by identifier on every event arrival; the saga remembers where it is across hours, days, or months of events.
#[Distributed] handlers and the Distributed Bus extend the same workflow primitives across bounded contexts. Service Map carries the topology; multi-broker single-topology is first-class. The retry, DLQ, idempotency, and outbox semantics that protect in-process steps apply uniformly across service boundaries.
Each shape below maps to one or more Ecotone primitives applied through PHP attributes on plain classes.
#[Orchestrator] declares the step list as channel names; each step is an independently testable #[InternalHandler]. The outbox commits business state and next-step dispatch atomically. Compensation steps are channels you append on failure — declarative, not a manual try/catch chain.
#[Saga] holds state across days or weeks of events. #[Delayed] handlers fire after a TimeSpan or DateTime — no cron job, no timer service. The same saga handles the human approval event, the timeout event, and the resume event with one identifier.
#[Distributed] handlers exchange commands and events between PHP services over the broker you already operate. Service Map declares which service consumes which routing keys on which broker. Adding a service is a config change, not code in every caller.
Per-handler failure isolation: each subscriber consumes its own copy. A slow or failing subscriber never blocks siblings. CombinedMessageChannel keeps the outbox in the database and pushes execution to the broker — one poller drains the outbox, many consumers scale the work.
Dynamic channels route per-tenant at runtime via headers — no redeploy. The same workflow code serves every tenant; isolation is operational, not duplicated. Tenant-scoped channels carry their own retry, priority, and DLQ policies.
Stateful process manager. #[Identifier] maps incoming events to the right instance by payload, header, or expression. State persisted in your DB; reloaded per event.
Routing-slip workflow. Returns the channel list for the next steps — including dynamic step lists computed from input data. Each step is reusable.
Stateless durable flow. The message carries the state; outputChannelName advances it. Combined with the outbox, each step commits atomically with its business write.
Saga timeouts as attribute — TimeSpan, exact DateTime, or expression. The broker's delayed-message primitive handles the wait. No cron, no timer service.
One DBAL transaction commits business state and the outgoing message together. One poller drains the database; the broker carries execution. No dual-write window.
Cross-service commands and events on the brokers you already operate. Service Map carries the topology; multi-broker single-topology is first-class.
Per-tenant message routing at runtime via headers. Multi-tenant workflows in one deployment, with per-channel retry and priority.
Gateway-level deduplication absorbs redelivered messages, double-clicks, and webhook retries. Every handler behind the bus is covered without per-handler code.
Unleash the power of Messaging in PHP
and push productivity to the higher level
