Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Messenger] Add ChainStamp to dispatch messages one after another - #65903

Open
nicolas-grekas wants to merge 1 commit into
symfony:8.2from
nicolas-grekas:messenger-chain
Open

[Messenger] Add ChainStamp to dispatch messages one after another#65903
nicolas-grekas wants to merge 1 commit into
symfony:8.2from
nicolas-grekas:messenger-chain

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Sep 8, 2026

Copy link
Copy Markdown
Member
Q A
Branch? 8.2
Bug fix? no
New feature? yes
Deprecations? no
Issues Fix #50462
License MIT

This is a proposal, open for discussion. It started from Dariusz Gafka's article Symfony Messenger vs Ecotone: The Real Difference, which describes how Ecotone approaches this. What is proposed here is a free interpretation for Symfony, built on Messenger's own model rather than ported from Ecotone, so it departs from the article where the two models differ.

A multi-step flow is usually built by having each handler dispatch the next message class. The sequence is then implied by dispatch calls scattered across handlers, and nothing survives a retry. This PR lets the sequence be declared once, at dispatch time:

$bus->dispatch(new ProcessPodcast($id), [new ChainStamp(new OptimizePodcast($id), new ReleasePodcast($id))]);

Each message of the chain is dispatched once the previous one was handled by all its handlers. A failing step stops the chain. The remaining steps travel in the stamp, so a step that is retried (retry strategy or messenger:failed:retry) resumes the chain from where it stopped.

Public API

  • Symfony\Component\Messenger\Stamp\ChainStamp: __construct(object ...$messages) (messages or Envelope instances, in handling order; an empty chain throws), getMessages(): array.
  • Symfony\Component\Messenger\Middleware\ChainMiddleware: dispatches the next message once the current one is handled. It runs between send_message and handle_message, so a message sent to a transport starts its next step in the worker that handles it, not at send time.
  • FrameworkBundle registers messenger.middleware.chain (shared by all buses, bound to the routable bus) and adds chain to the default middleware stack between send_message and handle_message. Buses configured with default_middleware: false have to list it themselves.

Behavior

  • The next step is dispatched with a DispatchAfterCurrentBusStamp: it runs after the current message completed, after the commit of a doctrine_transaction middleware, and in a worker after the current handlers returned.
  • A step routed to a transport is sent instead of handled; the chain continues in the worker consuming it. A step handled synchronously runs inside the same dispatch() call.
  • An item can be an Envelope, to give that step its own stamps (DelayStamp, TransportNamesStamp, BusNameStamp, ...). An Envelope item carrying its own ChainStamp runs that inner chain before the remaining outer steps. Its non-sendable stamps are dropped, since the chain travels inside another envelope, which would otherwise carry an AckStamp closure or a ReceivedStamp of its own into the next send.
  • A step runs on the bus of the previous step unless it carries its own BusNameStamp.
  • Errors: a failing first step surfaces as HandlerFailedException from dispatch(); a failing later synchronous step surfaces as DelayedMessageHandlingException. In a worker, a synchronous step failing right after an asynchronous one fails that asynchronous message as a whole, so steps that follow an asynchronous one should be idempotent, or be routed to a transport so that each step is retried on its own.
  • A message sent to a transport starts its next step where it is handled, wherever chain sits in the middleware stack.
  • A message handled by a batch handler cannot open a chain: the batch decides when it processes the message, which is after the handler returned, so the combination throws instead of starting the step early and twice.
  • No data flows between steps: handler results are not passed along.
  • The stamp carries message objects. The default PhpSerializer handles it; transports using the Symfony serializer need normalizers for the chained messages, as they do for RedispatchMessage.

Checks

  • ./phpunit src/Symfony/Component/Messenger/Tests and ./phpunit src/Symfony/Bundle/FrameworkBundle/Tests: green (skips are the usual missing-server ones).
  • The new tests fail on the base branch without the two new classes and without the bundle wiring (revert-verified).
  • The wiring now lives in MessengerBundle rather than FrameworkBundle, following the split upstream, so the version guards the branch carried are gone.

Documentation

  • Dispatching a chain, what "handled" means, and that a failing step stops the chain.
  • Envelope items for per-step stamps, bus selection, and the timing given by DispatchAfterCurrentBusStamp.
  • The worker caveat for a synchronous step following an asynchronous one.
  • That a batch handler cannot open a chain, and that a sent message continues where it is handled.
  • The serializer note for the Symfony serializer.

@dgafka

This comment was marked as resolved.

@nicolas-grekas

This comment was marked as resolved.

@nicolas-grekas

This comment was marked as resolved.

@dgafka

This comment was marked as resolved.

@nicolas-grekas

This comment was marked as resolved.

@alexandre-daubois alexandre-daubois left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be nice, I came across a lot of situations in highly async env where that would have been really useful

Comment thread src/Symfony/Component/Messenger/Middleware/ChainMiddleware.php
Comment thread src/Symfony/Component/Messenger/Stamp/ChainStamp.php Outdated
Comment thread src/Symfony/Component/Messenger/Middleware/ChainMiddleware.php Outdated
Comment thread src/Symfony/Component/Messenger/Resources/config/messenger.php
@nicolas-grekas
nicolas-grekas force-pushed the messenger-chain branch 3 times, most recently from ae8a981 to d794296 Compare September 13, 2026 07:25
A ChainStamp lists the messages to handle after the one it is attached to,
in order. The new ChainMiddleware dispatches the next message of the chain
once the current one is handled, with a DispatchAfterCurrentBusStamp, and
passes the remaining messages along in a new ChainStamp. The next message
keeps the bus name of the current one unless it names its own bus.

When a step fails, the rest of the chain is not dispatched. The remaining
steps travel with a message sent to a transport, so the chain continues
from there once a failed step is retried.

The Messenger bundle registers the middleware as messenger.middleware.chain,
bound to the routable message bus, and adds it to the default middleware
stack between send_message and handle_message.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RFC] Chained message handling in Symfony Messenger

4 participants