Stamp 是 Symfony Messenger 中独立于消息体的元数据容器,用于解耦上下文与业务逻辑;直接往消息类加字段会导致序列化膨胀、处理器耦合、中间件无法统一处理及跨传输丢失类型信息。

Stamp 是什么,为什么不能直接往消息里塞字段?
Stamp 不是附加在消息对象属性上的普通字段,而是独立于消息体的元数据容器。直接往 SendEmailMessage 类里加 $traceId 或 $userId 属性,会导致:消息序列化体积膨胀、处理器必须感知这些字段、无法被中间件统一处理、跨传输(如从 Redis 到 SQS)时可能丢失类型信息。
Stamp 的设计目标就是解耦上下文与业务逻辑——它由 Envelope 携带,随消息流转,但不参与业务处理逻辑本身。比如你用 TraceStamp 记录调用链路,所有中间件和处理器都能读取,却不需要修改任何消息类定义。
怎么创建和注入自定义 Stamp?
自定义 Stamp 必须实现 Symfony\Component\Messenger\Stamp\StampInterface,通常只需一个简单类:
namespace App\Stamp;
use Symfony\Component\Messenger\Stamp\StampInterface;
class UserIdStamp implements StampInterface
{
public function __construct(public int $userId) {}
}
注入方式取决于场景:
- 控制器中 dispatch 前手动包装:
$envelope = new Envelope($message, [new UserIdStamp($user->getId())]); $this->bus->dispatch($envelope); - 用中间件自动添加(推荐):在
handle()中检查消息是否已有该 Stamp,没有就补上,比如从安全令牌或请求头提取$userId - 避免在 Handler 里 new Stamp 再 dispatch 新消息——这会丢失原始上下文,应复用当前
$envelope->withStamps(...)
如何在 Handler 或中间件里安全读取 Stamp?
读取 Stamp 不能靠 try-catch 或 isset,必须用 getStamps() 并做类型判断:
// 正确:明确获取指定类型 Stamp
$userIdStamp = $envelope->last(UserIdStamp::class);
if ($userIdStamp instanceof UserIdStamp) {
$userId = $userIdStamp->userId;
}
// 错误:直接访问数组键或假设存在
$stamps = $envelope->getStamps(UserIdStamp::class);
$userId = $stamps[0]->userId ?? null; // 可能越界或 null
注意三点:
-
last()返回最后一个同类型 Stamp,适合覆盖语义(如多次中间件叠加);getStamps()返回全部,适合收集多个值 - Stamp 实例是不可变的,修改字段不会影响原对象,必须用
withStamps()生成新Envelope - 若用 Doctrine 传输,Stamp 必须可序列化;含 Closure 或 resource 的 Stamp 会报错
跨进程时 Stamp 会丢吗?哪些 Stamp 能持久化?
不是所有 Stamp 都能跨进程存活。只有被 SerializerInterface 支持序列化的 Stamp 才能存入传输层(Redis/Doctrine/SQS)。内置 Stamp 如 DelayStamp、PriorityStamp 默认支持;自定义 Stamp 必须满足:
- 不含私有属性(或提供
__serialize()/__unserialize()) - 不依赖运行时资源(如数据库连接、文件句柄)
- 构造参数全是标量或可序列化对象(如
DateTimeImmutable可以,DateTime不推荐)
最容易踩的坑是:本地测试时 Stamp 正常传递,一上生产用 Redis 就消失——往往是因为用了 DateTime 或未声明 public 属性。用 php bin/console debug:messenger 查看消息进队列前的 Stamp 列表,再对比消费时的列表,能快速定位丢失点。


















