From 2618d474d9d8d824792693d0e73176699f5dfe6c Mon Sep 17 00:00:00 2001
From: Kerian Montes-Morin <kerianmontes@gmail.com>
Date: Wed, 28 Jun 2023 13:41:21 +0200
Subject: [PATCH 1/2] [Messenger] Add auto stamp middleware

---
 .../Resources/config/messenger.php            |  3 ++
 src/Symfony/Component/Messenger/CHANGELOG.md  |  1 +
 .../Middleware/AutoStampMiddleware.php        | 35 +++++++++++++
 .../Component/Messenger/Stamp/DelayStamp.php  |  1 +
 .../Messenger/Stamp/ValidationStamp.php       |  1 +
 .../Tests/Fixtures/AutoStampedMessage.php     | 12 +++++
 .../Middleware/AutoStampMiddlewareTest.php    | 49 +++++++++++++++++++
 7 files changed, 102 insertions(+)
 create mode 100644 src/Symfony/Component/Messenger/Middleware/AutoStampMiddleware.php
 create mode 100644 src/Symfony/Component/Messenger/Tests/Fixtures/AutoStampedMessage.php
 create mode 100644 src/Symfony/Component/Messenger/Tests/Middleware/AutoStampMiddlewareTest.php

diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php
index 3fe593ac673ff..02b8ec307d4e3 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php
@@ -26,6 +26,7 @@
 use Symfony\Component\Messenger\EventListener\StopWorkerOnSignalsListener;
 use Symfony\Component\Messenger\Handler\RedispatchMessageHandler;
 use Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware;
+use Symfony\Component\Messenger\Middleware\AutoStampMiddleware;
 use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware;
 use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware;
 use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
@@ -112,6 +113,8 @@
                 service('router'),
             ])
 
+        ->set('messenger.middleware.auto_stamp_middleware', AutoStampMiddleware::class)
+
         // Discovery
         ->set('messenger.receiver_locator', ServiceLocator::class)
             ->args([
diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md
index aabf140f6ce73..a45b37d99e3ee 100644
--- a/src/Symfony/Component/Messenger/CHANGELOG.md
+++ b/src/Symfony/Component/Messenger/CHANGELOG.md
@@ -16,6 +16,7 @@ CHANGELOG
    SIGTERM by default
  * Add `RedispatchMessage` and `RedispatchMessageHandler`
  * Add optional parameter `$isSameDatabase` to `DoctrineTransport::configureSchema()`
+ * Add `AutoStampMiddleware` that register envelope stamps from message attributes
 
 6.2
 ---
diff --git a/src/Symfony/Component/Messenger/Middleware/AutoStampMiddleware.php b/src/Symfony/Component/Messenger/Middleware/AutoStampMiddleware.php
new file mode 100644
index 0000000000000..6628fe6dd9ce7
--- /dev/null
+++ b/src/Symfony/Component/Messenger/Middleware/AutoStampMiddleware.php
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Messenger\Middleware;
+
+use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\Stamp\StampInterface;
+
+/**
+ * Middleware that add stamps configured on message.
+ *
+ * @author Kerian Montes <kerianmontes@gmail.com>
+ */
+class AutoStampMiddleware implements MiddlewareInterface
+{
+    public function handle(Envelope $envelope, StackInterface $stack): Envelope
+    {
+        $class = new \ReflectionClass($envelope->getMessage());
+        do {
+            foreach ($class->getAttributes(StampInterface::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+                $envelope = $envelope->with($attribute->newInstance());
+            }
+        } while ($class = $class->getParentClass());
+
+        return $stack->next()->handle($envelope, $stack);
+    }
+}
diff --git a/src/Symfony/Component/Messenger/Stamp/DelayStamp.php b/src/Symfony/Component/Messenger/Stamp/DelayStamp.php
index 8b44239380fb4..141a13ca5c5ac 100644
--- a/src/Symfony/Component/Messenger/Stamp/DelayStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/DelayStamp.php
@@ -14,6 +14,7 @@
 /**
  * Apply this stamp to delay delivery of your message on a transport.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class DelayStamp implements StampInterface
 {
     private int $delay;
diff --git a/src/Symfony/Component/Messenger/Stamp/ValidationStamp.php b/src/Symfony/Component/Messenger/Stamp/ValidationStamp.php
index e9cc9de5f9a25..c6f094158a888 100644
--- a/src/Symfony/Component/Messenger/Stamp/ValidationStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/ValidationStamp.php
@@ -16,6 +16,7 @@
 /**
  * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class ValidationStamp implements StampInterface
 {
     private array|GroupSequence $groups;
diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/AutoStampedMessage.php b/src/Symfony/Component/Messenger/Tests/Fixtures/AutoStampedMessage.php
new file mode 100644
index 0000000000000..7e8fd73a04bff
--- /dev/null
+++ b/src/Symfony/Component/Messenger/Tests/Fixtures/AutoStampedMessage.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Symfony\Component\Messenger\Tests\Fixtures;
+
+use Symfony\Component\Messenger\Stamp\DelayStamp;
+use Symfony\Component\Messenger\Stamp\ValidationStamp;
+
+#[DelayStamp(123)]
+#[ValidationStamp(['Default', 'Extra'])]
+class AutoStampedMessage
+{
+}
diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/AutoStampMiddlewareTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/AutoStampMiddlewareTest.php
new file mode 100644
index 0000000000000..d84592fa7c29e
--- /dev/null
+++ b/src/Symfony/Component/Messenger/Tests/Middleware/AutoStampMiddlewareTest.php
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Messenger\Tests\Middleware;
+
+use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\Middleware\AutoStampMiddleware;
+use Symfony\Component\Messenger\Stamp\DelayStamp;
+use Symfony\Component\Messenger\Stamp\ValidationStamp;
+use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase;
+use Symfony\Component\Messenger\Tests\Fixtures\AutoStampedMessage;
+use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
+
+class AutoStampMiddlewareTest extends MiddlewareTestCase
+{
+    public function testHandleWithoutAutoStampAndNextMiddleware()
+    {
+        $message = new DummyMessage('Hey');
+        $envelope = new Envelope($message);
+
+        (new AutoStampMiddleware())->handle($envelope, $this->getStackMock());
+        $this->assertCount(0, $envelope->all());
+    }
+
+    public function testHandleWithAutoStampAndNextMiddleware()
+    {
+        $message = new AutoStampedMessage();
+        $envelope = new Envelope($message);
+
+        $handledEnvelope = (new AutoStampMiddleware())->handle($envelope, $this->getStackMock());
+        $this->assertCount(2, $handledEnvelope->all());
+
+        $delayStamp = $handledEnvelope->last(DelayStamp::class);
+        $this->assertInstanceOf(DelayStamp::class, $delayStamp);
+        $this->assertSame(123, $delayStamp->getDelay());
+
+        $validationStamp = $handledEnvelope->last(ValidationStamp::class);
+        $this->assertInstanceOf(ValidationStamp::class, $validationStamp);
+        $this->assertSame(['Default', 'Extra'], $validationStamp->getGroups());
+    }
+}

From 89fb4e5f57db9f254a53978c80d278064305d2f8 Mon Sep 17 00:00:00 2001
From: Kerian Montes-Morin <kerianmontes@gmail.com>
Date: Wed, 28 Jun 2023 17:30:22 +0200
Subject: [PATCH 2/2] [Messenger] Stamps as class attributes

---
 .../Messenger/Bridge/AmazonSqs/Transport/AmazonSqsFifoStamp.php  | 1 +
 .../Bridge/AmazonSqs/Transport/AmazonSqsReceivedStamp.php        | 1 +
 .../Bridge/AmazonSqs/Transport/AmazonSqsXrayTraceHeaderStamp.php | 1 +
 .../Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php        | 1 +
 .../Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php      | 1 +
 .../Bridge/Beanstalkd/Transport/BeanstalkdReceivedStamp.php      | 1 +
 .../Bridge/Doctrine/Transport/DoctrineReceivedStamp.php          | 1 +
 .../Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php      | 1 +
 src/Symfony/Component/Messenger/Stamp/AckStamp.php               | 1 +
 src/Symfony/Component/Messenger/Stamp/BusNameStamp.php           | 1 +
 src/Symfony/Component/Messenger/Stamp/ConsumedByWorkerStamp.php  | 1 +
 .../Component/Messenger/Stamp/DispatchAfterCurrentBusStamp.php   | 1 +
 src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php      | 1 +
 .../Component/Messenger/Stamp/FlushBatchHandlersStamp.php        | 1 +
 src/Symfony/Component/Messenger/Stamp/HandledStamp.php           | 1 +
 src/Symfony/Component/Messenger/Stamp/HandlerArgumentsStamp.php  | 1 +
 .../Component/Messenger/Stamp/MessageDecodingFailedStamp.php     | 1 +
 src/Symfony/Component/Messenger/Stamp/NoAutoAckStamp.php         | 1 +
 src/Symfony/Component/Messenger/Stamp/ReceivedStamp.php          | 1 +
 src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php        | 1 +
 src/Symfony/Component/Messenger/Stamp/RouterContextStamp.php     | 1 +
 src/Symfony/Component/Messenger/Stamp/SentStamp.php              | 1 +
 .../Component/Messenger/Stamp/SentToFailureTransportStamp.php    | 1 +
 src/Symfony/Component/Messenger/Stamp/SerializedMessageStamp.php | 1 +
 src/Symfony/Component/Messenger/Stamp/SerializerStamp.php        | 1 +
 .../Component/Messenger/Stamp/TransportMessageIdStamp.php        | 1 +
 src/Symfony/Component/Messenger/Stamp/TransportNamesStamp.php    | 1 +
 src/Symfony/Component/Scheduler/Messenger/ScheduledStamp.php     | 1 +
 28 files changed, 28 insertions(+)

diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsFifoStamp.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsFifoStamp.php
index f6cb568c5a9d5..8b5bb5fe2045d 100644
--- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsFifoStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsFifoStamp.php
@@ -13,6 +13,7 @@
 
 use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
 
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class AmazonSqsFifoStamp implements NonSendableStampInterface
 {
     private ?string $messageGroupId;
diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsReceivedStamp.php
index 8243bf7fea2a1..dd96f1f190566 100644
--- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsReceivedStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsReceivedStamp.php
@@ -16,6 +16,7 @@
 /**
  * @author Jérémy Derussé <jeremy@derusse.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class AmazonSqsReceivedStamp implements NonSendableStampInterface
 {
     private string $id;
diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsXrayTraceHeaderStamp.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsXrayTraceHeaderStamp.php
index b023c31da6f54..dc3b0f797617d 100644
--- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsXrayTraceHeaderStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsXrayTraceHeaderStamp.php
@@ -13,6 +13,7 @@
 
 use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
 
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class AmazonSqsXrayTraceHeaderStamp implements NonSendableStampInterface
 {
     private string $traceId;
diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php
index 1735f0f79c2a5..f69ac3a5e9b20 100644
--- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpReceivedStamp.php
@@ -16,6 +16,7 @@
 /**
  * Stamp applied when a message is received from Amqp.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class AmqpReceivedStamp implements NonSendableStampInterface
 {
     private \AMQPEnvelope $amqpEnvelope;
diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php
index 127593d38b66d..d5d7049f4fe19 100644
--- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php
@@ -17,6 +17,7 @@
  * @author Guillaume Gammelin <ggammelin@gmail.com>
  * @author Samuel Roze <samuel.roze@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class AmqpStamp implements NonSendableStampInterface
 {
     private ?string $routingKey;
diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/BeanstalkdReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/BeanstalkdReceivedStamp.php
index 54511e8d90f28..6d368a36803bf 100644
--- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/BeanstalkdReceivedStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/BeanstalkdReceivedStamp.php
@@ -16,6 +16,7 @@
 /**
  * @author Antonio Pauletich <antonio.pauletich95@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class BeanstalkdReceivedStamp implements NonSendableStampInterface
 {
     private string $id;
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceivedStamp.php
index 513f4b9f01c4c..1ce9a249a6b8f 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceivedStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/DoctrineReceivedStamp.php
@@ -16,6 +16,7 @@
 /**
  * @author Vincent Touzet <vincent.touzet@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class DoctrineReceivedStamp implements NonSendableStampInterface
 {
     private string $id;
diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php
index 4773d821f91f7..aa3911371aca5 100644
--- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php
+++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceivedStamp.php
@@ -16,6 +16,7 @@
 /**
  * @author Alexander Schranz <alexander@sulu.io>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class RedisReceivedStamp implements NonSendableStampInterface
 {
     private string $id;
diff --git a/src/Symfony/Component/Messenger/Stamp/AckStamp.php b/src/Symfony/Component/Messenger/Stamp/AckStamp.php
index b94c2c98e395c..cc855cdc759b8 100644
--- a/src/Symfony/Component/Messenger/Stamp/AckStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/AckStamp.php
@@ -16,6 +16,7 @@
 /**
  * Marker stamp for messages that can be ack/nack'ed.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class AckStamp implements NonSendableStampInterface
 {
     private $ack;
diff --git a/src/Symfony/Component/Messenger/Stamp/BusNameStamp.php b/src/Symfony/Component/Messenger/Stamp/BusNameStamp.php
index ad6116ff35bed..10863a020a6d7 100644
--- a/src/Symfony/Component/Messenger/Stamp/BusNameStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/BusNameStamp.php
@@ -16,6 +16,7 @@
  *
  * @author Ryan Weaver <ryan@symfonycasts.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class BusNameStamp implements StampInterface
 {
     private string $busName;
diff --git a/src/Symfony/Component/Messenger/Stamp/ConsumedByWorkerStamp.php b/src/Symfony/Component/Messenger/Stamp/ConsumedByWorkerStamp.php
index 3ae37ba6ad230..feffda644b4e6 100644
--- a/src/Symfony/Component/Messenger/Stamp/ConsumedByWorkerStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/ConsumedByWorkerStamp.php
@@ -14,6 +14,7 @@
 /**
  * A marker that this message was consumed by a worker process.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class ConsumedByWorkerStamp implements NonSendableStampInterface
 {
 }
diff --git a/src/Symfony/Component/Messenger/Stamp/DispatchAfterCurrentBusStamp.php b/src/Symfony/Component/Messenger/Stamp/DispatchAfterCurrentBusStamp.php
index 0ee31f05c361d..092fcf87213fe 100644
--- a/src/Symfony/Component/Messenger/Stamp/DispatchAfterCurrentBusStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/DispatchAfterCurrentBusStamp.php
@@ -18,6 +18,7 @@
  *
  * @author Tobias Nyholm <tobias.nyholm@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class DispatchAfterCurrentBusStamp implements NonSendableStampInterface
 {
 }
diff --git a/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php b/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php
index 1bf4f9d30d542..e46a1fb3557bc 100644
--- a/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php
@@ -17,6 +17,7 @@
 /**
  * Stamp applied when a messages fails due to an exception in the handler.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class ErrorDetailsStamp implements StampInterface
 {
     private string $exceptionClass;
diff --git a/src/Symfony/Component/Messenger/Stamp/FlushBatchHandlersStamp.php b/src/Symfony/Component/Messenger/Stamp/FlushBatchHandlersStamp.php
index 5dfbe2281efe3..cf9ef46969dee 100644
--- a/src/Symfony/Component/Messenger/Stamp/FlushBatchHandlersStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/FlushBatchHandlersStamp.php
@@ -14,6 +14,7 @@
 /**
  * Marker telling that any batch handlers bound to the envelope should be flushed.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class FlushBatchHandlersStamp implements NonSendableStampInterface
 {
     private $force;
diff --git a/src/Symfony/Component/Messenger/Stamp/HandledStamp.php b/src/Symfony/Component/Messenger/Stamp/HandledStamp.php
index 00327a8d75bd2..a93703729f4c6 100644
--- a/src/Symfony/Component/Messenger/Stamp/HandledStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/HandledStamp.php
@@ -25,6 +25,7 @@
  *
  * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class HandledStamp implements StampInterface
 {
     private mixed $result;
diff --git a/src/Symfony/Component/Messenger/Stamp/HandlerArgumentsStamp.php b/src/Symfony/Component/Messenger/Stamp/HandlerArgumentsStamp.php
index 3dcdacf88f580..2226afda591f1 100644
--- a/src/Symfony/Component/Messenger/Stamp/HandlerArgumentsStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/HandlerArgumentsStamp.php
@@ -14,6 +14,7 @@
 /**
  * @author Jáchym Toušek <enumag@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class HandlerArgumentsStamp implements NonSendableStampInterface
 {
     public function __construct(
diff --git a/src/Symfony/Component/Messenger/Stamp/MessageDecodingFailedStamp.php b/src/Symfony/Component/Messenger/Stamp/MessageDecodingFailedStamp.php
index 66e4778fbd47a..c42ccd00d66f1 100644
--- a/src/Symfony/Component/Messenger/Stamp/MessageDecodingFailedStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/MessageDecodingFailedStamp.php
@@ -14,6 +14,7 @@
 /**
  * @author Grégoire Pineau <lyrixx@lyrixx.info>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class MessageDecodingFailedStamp implements StampInterface
 {
 }
diff --git a/src/Symfony/Component/Messenger/Stamp/NoAutoAckStamp.php b/src/Symfony/Component/Messenger/Stamp/NoAutoAckStamp.php
index 15ba383b79c9c..cbf5cf46d1d43 100644
--- a/src/Symfony/Component/Messenger/Stamp/NoAutoAckStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/NoAutoAckStamp.php
@@ -16,6 +16,7 @@
 /**
  * Marker telling that ack should not be done automatically for this message.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class NoAutoAckStamp implements NonSendableStampInterface
 {
     private $handlerDescriptor;
diff --git a/src/Symfony/Component/Messenger/Stamp/ReceivedStamp.php b/src/Symfony/Component/Messenger/Stamp/ReceivedStamp.php
index 0a8d00a73bbaa..7a81a6eb3402e 100644
--- a/src/Symfony/Component/Messenger/Stamp/ReceivedStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/ReceivedStamp.php
@@ -23,6 +23,7 @@
  *
  * @author Samuel Roze <samuel.roze@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class ReceivedStamp implements NonSendableStampInterface
 {
     private string $transportName;
diff --git a/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php b/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php
index 5df028cf7c984..1b80589274435 100644
--- a/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php
@@ -16,6 +16,7 @@
 /**
  * Stamp applied when a messages needs to be redelivered.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class RedeliveryStamp implements StampInterface
 {
     private int $retryCount;
diff --git a/src/Symfony/Component/Messenger/Stamp/RouterContextStamp.php b/src/Symfony/Component/Messenger/Stamp/RouterContextStamp.php
index bf66c15f395ca..7cd55f772ffc8 100644
--- a/src/Symfony/Component/Messenger/Stamp/RouterContextStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/RouterContextStamp.php
@@ -14,6 +14,7 @@
 /**
  * @author Jérémy Derussé <jeremy@derusse.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 class RouterContextStamp implements StampInterface
 {
     private string $baseUrl;
diff --git a/src/Symfony/Component/Messenger/Stamp/SentStamp.php b/src/Symfony/Component/Messenger/Stamp/SentStamp.php
index d1092db90e99b..7ef94aad4f2d2 100644
--- a/src/Symfony/Component/Messenger/Stamp/SentStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/SentStamp.php
@@ -18,6 +18,7 @@
  *
  * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class SentStamp implements NonSendableStampInterface
 {
     private string $senderClass;
diff --git a/src/Symfony/Component/Messenger/Stamp/SentToFailureTransportStamp.php b/src/Symfony/Component/Messenger/Stamp/SentToFailureTransportStamp.php
index 10952c9638216..053bd6ce62371 100644
--- a/src/Symfony/Component/Messenger/Stamp/SentToFailureTransportStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/SentToFailureTransportStamp.php
@@ -16,6 +16,7 @@
  *
  * @author Ryan Weaver <ryan@symfonycasts.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class SentToFailureTransportStamp implements StampInterface
 {
     private string $originalReceiverName;
diff --git a/src/Symfony/Component/Messenger/Stamp/SerializedMessageStamp.php b/src/Symfony/Component/Messenger/Stamp/SerializedMessageStamp.php
index 3feffbc131861..d430c8aaf011b 100644
--- a/src/Symfony/Component/Messenger/Stamp/SerializedMessageStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/SerializedMessageStamp.php
@@ -11,6 +11,7 @@
 
 namespace Symfony\Component\Messenger\Stamp;
 
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class SerializedMessageStamp implements NonSendableStampInterface
 {
     public function __construct(private string $serializedMessage)
diff --git a/src/Symfony/Component/Messenger/Stamp/SerializerStamp.php b/src/Symfony/Component/Messenger/Stamp/SerializerStamp.php
index e3660301f7c33..796342c6886bb 100644
--- a/src/Symfony/Component/Messenger/Stamp/SerializerStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/SerializerStamp.php
@@ -14,6 +14,7 @@
 /**
  * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class SerializerStamp implements StampInterface
 {
     private array $context;
diff --git a/src/Symfony/Component/Messenger/Stamp/TransportMessageIdStamp.php b/src/Symfony/Component/Messenger/Stamp/TransportMessageIdStamp.php
index a1846eda2ea82..5caf24a189a8e 100644
--- a/src/Symfony/Component/Messenger/Stamp/TransportMessageIdStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/TransportMessageIdStamp.php
@@ -16,6 +16,7 @@
  *
  * @author Ryan Weaver <ryan@symfonycasts.com>
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class TransportMessageIdStamp implements StampInterface
 {
     private mixed $id;
diff --git a/src/Symfony/Component/Messenger/Stamp/TransportNamesStamp.php b/src/Symfony/Component/Messenger/Stamp/TransportNamesStamp.php
index a7b9a8f284e9f..9b09aa268d507 100644
--- a/src/Symfony/Component/Messenger/Stamp/TransportNamesStamp.php
+++ b/src/Symfony/Component/Messenger/Stamp/TransportNamesStamp.php
@@ -14,6 +14,7 @@
 /**
  * Stamp used to override the transport names specified in the Messenger routing configuration file.
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class TransportNamesStamp implements StampInterface
 {
     private array $transportNames;
diff --git a/src/Symfony/Component/Scheduler/Messenger/ScheduledStamp.php b/src/Symfony/Component/Scheduler/Messenger/ScheduledStamp.php
index 79efc1121e581..a23994224c8f8 100644
--- a/src/Symfony/Component/Scheduler/Messenger/ScheduledStamp.php
+++ b/src/Symfony/Component/Scheduler/Messenger/ScheduledStamp.php
@@ -17,6 +17,7 @@
 /**
  * @experimental
  */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
 final class ScheduledStamp implements NonSendableStampInterface
 {
     public function __construct(public readonly MessageContext $messageContext)