Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Stamp Decider Pipeline
Stamp Decider Pipeline
somework/cqrs-bundle
cursor
symfony
ddd
event-bus
Messenger
cqrs
command-bus
query-bus

StampsDecider aggregates StampDecider implementations into a per-message-class pipeline. When AbstractMessengerBus::dispatchMessage() calls decide(), the aggregator runs all applicable deciders in priority order, threading the stamp array through each stage. The pipeline is cached per message...

Install
php artisan boost:add-skill somework/cqrs-bundle

Save this content to: .cursor/rules/stamp-decider-pipeline.md

# Stamp Decider Pipeline

## How It Works

`StampsDecider` aggregates `StampDecider` implementations into a per-message-class pipeline. When `AbstractMessengerBus::dispatchMessage()` calls `decide()`, the aggregator runs all applicable deciders in priority order, threading the stamp array through each stage. The pipeline is cached per message class on first invocation.

## Implementing a New Decider

Choose the right interface:
- **`MessageTypeAwareStampDecider`** — when the decider only applies to specific message types (Command, Query, Event). Implement `messageTypes()` returning the applicable contract classes. This is called once at construction, not per-dispatch. Most deciders use this.
- **`StampDecider`** — when the decider applies to all messages regardless of type (`CausationIdStampDecider`, `IdempotencyStampDecider`, `DispatchAfterCurrentBusStampDecider`).

The `decide(object $message, DispatchMode $mode, array $stamps): array` method receives the current stamp list and MUST return the updated list. Four patterns exist in the codebase:

1. **Append, skipping caller classes** — Append each new stamp unless a stamp of the same class is already in the array (Messenger reads the last stamp of a class, so an appended one would override the caller's; see `RetryPolicyStampDecider`)
2. **Conditional append** — Check if the resolver returns a stamp, append only if non-null (see `MessageSerializerStampDecider`)
3. **Early-exit-if-present** — Check if a stamp type already exists in the array, return unchanged if so (see `MessageTransportStampDecider`, `MessageMetadataStampDecider`, `DispatchAfterCurrentBusStampDecider`)
4. **Enrich the last stamp** — Replace the last stamp of a type with an enriched copy (see `CausationIdStampDecider`)

Stamps passed by the caller always win: never remove, replace or duplicate a stamp the caller supplied.

Never mutate the input array in place — always return a new array. `StampsDecider` calls `array_values()` on the final result to ensure sequential keys.

## Priority Bands

Deciders are sorted by priority via `TaggedIteratorArgument` (higher = earlier). Established bands:

| Priority | Purpose | Example |
|----------|---------|---------|
| 225 | Rate limiting (fails fast) | `RateLimitStampDecider` |
| 200 | Retry policies | `RetryPolicyStampDecider` |
| 175 | Transport routing, including the `#[Asynchronous]` transport | `MessageTransportStampDecider` |
| 150 | Serialization | `MessageSerializerStampDecider` |
| 125 | Metadata | `MessageMetadataStampDecider` |
| 110 | Event sequence | `SequenceStampDecider` |
| 100 | Causation id (needs the metadata stamp) | `CausationIdStampDecider` |
| 50 | Idempotency | `IdempotencyStampDecider` |
| 0 | Default priority of custom (autoconfigured) deciders | — |
| -10 | Cross-cutting (runs last) | `DispatchAfterCurrentBusStampDecider` |

Place new deciders in the appropriate band. If a decider depends on stamps from an earlier stage, give it a lower priority. Cross-cutting deciders that must run after custom deciders use a negative priority (`DispatchAfterCurrentBusStampDecider` uses -10).

## Registration

Register new deciders in `StampsDeciderRegistrar` following the existing pattern:
1. Create a `Definition` with the decider class and its constructor arguments (typically a resolver reference from an earlier registrar)
2. Tag it with `somework_cqrs.dispatch_stamp_decider` and a `priority` attribute (message types are declared by `MessageTypeAwareStampDecider::messageTypes()`, not by the tag)
3. The `StampsDecider` aggregator collects all tagged services automatically via `TaggedIteratorArgument`

Service ID convention: `somework_cqrs.stamp_decider.{type}_{concern}` for type-aware deciders, `somework_cqrs.{concern}_stamp_decider` for generic ones.

## Third-Party Extension

Users can add custom deciders by tagging their own services with `somework_cqrs.dispatch_stamp_decider` and implementing `StampDecider`. They don't need to modify `StampsDeciderRegistrar`. The tagged iterator collects all services with this tag automatically.

Stamp Decider Pipeline

How It Works

StampsDecider aggregates StampDecider implementations into a per-message-class pipeline. When AbstractMessengerBus::dispatchMessage() calls decide(), the aggregator runs all applicable deciders in priority order, threading the stamp array through each stage. The pipeline is cached per message class on first invocation.

Implementing a New Decider

Choose the right interface:

  • MessageTypeAwareStampDecider — when the decider only applies to specific message types (Command, Query, Event). Implement messageTypes() returning the applicable contract classes. This is called once at construction, not per-dispatch. Most deciders use this.
  • StampDecider — when the decider applies to all messages regardless of type (CausationIdStampDecider, IdempotencyStampDecider, DispatchAfterCurrentBusStampDecider).

The decide(object $message, DispatchMode $mode, array $stamps): array method receives the current stamp list and MUST return the updated list. Four patterns exist in the codebase:

  1. Append, skipping caller classes — Append each new stamp unless a stamp of the same class is already in the array (Messenger reads the last stamp of a class, so an appended one would override the caller's; see RetryPolicyStampDecider)
  2. Conditional append — Check if the resolver returns a stamp, append only if non-null (see MessageSerializerStampDecider)
  3. Early-exit-if-present — Check if a stamp type already exists in the array, return unchanged if so (see MessageTransportStampDecider, MessageMetadataStampDecider, DispatchAfterCurrentBusStampDecider)
  4. Enrich the last stamp — Replace the last stamp of a type with an enriched copy (see CausationIdStampDecider)

Stamps passed by the caller always win: never remove, replace or duplicate a stamp the caller supplied.

Never mutate the input array in place — always return a new array. StampsDecider calls array_values() on the final result to ensure sequential keys.

Priority Bands

Deciders are sorted by priority via TaggedIteratorArgument (higher = earlier). Established bands:

Priority Purpose Example
225 Rate limiting (fails fast) RateLimitStampDecider
200 Retry policies RetryPolicyStampDecider
175 Transport routing, including the #[Asynchronous] transport MessageTransportStampDecider
150 Serialization MessageSerializerStampDecider
125 Metadata MessageMetadataStampDecider
110 Event sequence SequenceStampDecider
100 Causation id (needs the metadata stamp) CausationIdStampDecider
50 Idempotency IdempotencyStampDecider
0 Default priority of custom (autoconfigured) deciders —
-10 Cross-cutting (runs last) DispatchAfterCurrentBusStampDecider

Place new deciders in the appropriate band. If a decider depends on stamps from an earlier stage, give it a lower priority. Cross-cutting deciders that must run after custom deciders use a negative priority (DispatchAfterCurrentBusStampDecider uses -10).

Registration

Register new deciders in StampsDeciderRegistrar following the existing pattern:

  1. Create a Definition with the decider class and its constructor arguments (typically a resolver reference from an earlier registrar)
  2. Tag it with somework_cqrs.dispatch_stamp_decider and a priority attribute (message types are declared by MessageTypeAwareStampDecider::messageTypes(), not by the tag)
  3. The StampsDecider aggregator collects all tagged services automatically via TaggedIteratorArgument

Service ID convention: somework_cqrs.stamp_decider.{type}_{concern} for type-aware deciders, somework_cqrs.{concern}_stamp_decider for generic ones.

Third-Party Extension

Users can add custom deciders by tagging their own services with somework_cqrs.dispatch_stamp_decider and implementing StampDecider. They don't need to modify StampsDeciderRegistrar. The tagged iterator collects all services with this tag automatically.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib