Messenger transports often connect your Symfony application to other applications, which may not use Symfony or even PHP. For example, a shop publishes its orders to RabbitMQ and an invoicing service written in Go consumes them. Symfony 8.2 adds a new serializer for these shared transports and lets all applications keep the identity of each message.

Exchanging Messages with Other Applications

Nicolas Philippe
Contributed by Nicolas Philippe in #66574

Messenger's Symfony serializer includes envelope stamps alongside the message. This works well when the sending and receiving applications share the same configuration, but can cause problems when they don't.

For example, the receiving application can't decode a stamp whose class isn't installed. A BusNameStamp can also tell its worker to use a bus that doesn't exist there, causing an error.

Symfony 8.2 adds the messenger.transport.interop_serializer service. It sends messages without their stamps and ignores the stamps of the messages it receives. Use it in all the Symfony applications that share the transport:

1
2
3
4
5
6
7
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            orders:
                dsn: '%env(ORDERS_TRANSPORT_DSN)%'
                serializer: messenger.transport.interop_serializer

Messages sent back to the transport for retries, or sent to the failure transport, keep their stamps so Messenger can count retries. The messages sent by this serializer only contain a JSON body and a few plain headers, so applications written in any language can read them.

By default, the type header contains the PHP class of the message, which means nothing to the other application. Give that class a serialized type name such as #[AsMessage(serializedTypeName: 'shop.order_placed')]. If the other application also uses Messenger, give its class the same name.

If the application sending the messages doesn't use Messenger (e.g. a Python or Node.js service), its messages may have no type header. When a transport carries only one kind of message, you can configure its class (or serialized type name) explicitly:

1
2
3
4
5
6
7
# config/services.yaml
services:
    app.orders_serializer:
        class: Symfony\Component\Messenger\Transport\Serialization\InteropSerializer
        arguments:
            - '@messenger.transport.symfony_serializer'
            - 'App\Message\OrderPlaced'

Then configure the transport to use this serializer:

1
2
3
4
5
6
7
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            orders:
                dsn: '%env(ORDERS_TRANSPORT_DSN)%'
                serializer: app.orders_serializer

If the transport carries several kinds of messages, pass a closure instead of a class name. The closure receives the message body and headers and returns the message type, or null if it can't determine the type.

Sharing Stamps as Plain Headers

Nicolas Grekas
Contributed by Nicolas Grekas in #66873

Removing all stamps also removes some useful information. The original message ID, for example, lets the receiving application recognize the same message when the broker delivers it again. Without that ID, each delivery gets a new one.

Symfony 8.2 adds stamp header mappers to share this information as plain headers that any application can read and set. This preserves useful metadata without sending the PHP stamp objects themselves.

By default, the MessageIdStamp, CorrelationStamp and CausationStamp are sent as the message_id, correlation_id and causation_id headers. Messenger adds these stamps when you enable the identity_stamps option.

You can also share information from your own stamps. For example, to include a tenant ID in each message, implement StampHeaderMapperInterface. With autoconfiguration enabled, the messenger.transport.interop_serializer service picks up your mapper automatically:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Messenger/TenantStampHeaderMapper.php
namespace App\Messenger;

use App\Messenger\Stamp\TenantStamp;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Transport\Serialization\StampHeaderMapperInterface;

class TenantStampHeaderMapper implements StampHeaderMapperInterface
{
    public function toHeaders(Envelope $envelope): array
    {
        return ($stamp = $envelope->last(TenantStamp::class)) ? ['tenant_id' => $stamp->tenantId] : [];
    }

    public function toStamps(array $headers): array
    {
        // these headers come from another application, so check their values
        return \is_string($tenantId = $headers['tenant_id'] ?? null) ? [new TenantStamp($tenantId)] : [];
    }
}

Signing Messages with Custom Headers

Nicolas Grekas
Contributed by Nicolas Grekas in #66875

If your transport signs its messages, Symfony 8.2 includes custom headers in the signature too. The signature covers every header written by the serializer, except Content-Type, including headers added by stamp header mappers. Headers added after serialization are ignored.

Caution

Symfony 7.4.18+ and 8.1.6+ refuse signed messages with headers other than type, Content-Type and X-Message-Stamp-*. If your serializer writes such headers, upgrade the applications that consume a signing transport before the ones that send messages to it.

Published in #Living on the edge