kunicmarko/jms-messenger-adapter
A Symfony Messenger adapter that lets JMS Serializer handle your message serialization, replacing the default PHP serializer so messages can travel as JSON, XML, or other JMS-supported formats with full metadata and type handling support.
This package is a transport-level adapter: it makes Symfony Messenger serialize message bodies with JMS Serializer instead of Symfony Serializer. It is archived and unmaintained (9 stars, zero dependents), so treat it as a small reference implementation or a fork candidate rather than a long-term dependency. The only recent activity is release 1.1.0, which adds PHP 8 compatibility — a maintenance-only change with no new features, no stamp handling, and no API changes. Practically, that means you can install it on a PHP 8 runtime without syntax-level breakage, but nothing about the adapter's behaviour has improved.
Start by reading vendor/kunicmarko/jms-messenger-adapter/src/Serializer.php. The package has no Symfony bundle and no Flex recipe; you wire it manually. The important surface is one class implementing Symfony\Component\Messenger\Transport\Serialization\SerializerInterface, with encode(Envelope): array and decode(array): Envelope. Confirm two things in that source before designing around it:
encode() write anything besides a type header?decode() wrap JMS exceptions in MessageDecodingFailedException and restore any stamps?Minimal setup:
composer require kunicmarko/jms-messenger-adapter jms/serializer-bundle
# config/services.yaml
services:
app.messenger.jms_serializer:
# Class name from vendor/kunicmarko/jms-messenger-adapter/src
class: Kunicmarko\JMSMessengerAdapter\Serializer
arguments:
- '@jms_serializer'
# config/packages/messenger.yaml
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
serializer: app.messenger.jms_serializer
failed:
dsn: 'doctrine://default?queue_name=failed'
serializer: app.messenger.jms_serializer
A first message:
namespace App\Message;
use JMS\Serializer\Annotation as JMS;
final class SendWelcomeEmail
{
/** @JMS\Type("string") */
private string $userId;
public function __construct(string $userId)
{
$this->userId = $userId;
}
public function getUserId(): string
{
return $this->userId;
}
}
$bus->dispatch(new SendWelcomeEmail($user->getId()));
bin/console messenger:consume async -vv
The first useful validation is to inspect the raw transport payload. For AMQP/Redis/Doctrine you should see a JSON body such as {"userId":"123"} and a header type: App\Message\SendWelcomeEmail. If that is all you see, you have seen the whole adapter: it is a thin class-name header plus JMS body serializer.
Use it first for simple, self-contained JSON messages on an async transport. Do not start by migrating a queue full of existing Symfony-serialized messages.
Configure the serializer per transport. This is not a bus middleware or a global serializer replacement. Set serializer: on each transport that needs JMS. Set it on the failure transport too; otherwise failed messages written by one serializer cannot be read by the other.
Keep message DTOs explicit. Messenger messages should be small DTOs, not Doctrine entities. Add JMS metadata for every non-scalar property:
use JMS\Serializer\Annotation as JMS;
final class ReindexProduct
{
/** @JMS\Type("string") */
private string $productId;
/** @JMS\Type("array<App\Message\ReindexLocale>") */
private array $locales = [];
/** @JMS\Type("DateTimeImmutable<'Y-m-d H:i:s'>") */
private \DateTimeImmutable $requestedAt;
}
Without @Type, JMS may decode nested objects as arrays or fail to hydrate collections. Use @SerializedName when the wire name must stay stable even if the PHP property is renamed.
Stabilize the type header. The adapter likely uses get_class($message) as the type header. That means renaming or moving a message class breaks messages already in the queue. Decorate the serializer with a class-to-alias map:
final class StableNameSerializer implements SerializerInterface
{
private const ALIASES = [
SendWelcomeEmail::class => 'send_welcome_email',
];
public function encode(Envelope $envelope): array
{
$encoded = $this->inner->encode($envelope);
$encoded['headers']['type'] = self::ALIASES[get_class($envelope->getMessage())];
return $encoded;
}
public function decode(array $encodedEnvelope): Envelope
{
$class = array_search($encodedEnvelope['headers']['type'], self::ALIASES, true);
if ($class === false) {
throw new MessageDecodingFailedException('Unknown message type.');
}
$encodedEnvelope['headers']['type'] = $class;
return $this->inner->decode($encodedEnvelope);
}
}
An allow-list like this is also the right place to prevent arbitrary class instantiation from a type header.
Add stamp handling if you need retries or bus routing. Symfony’s own serializer stores sendable stamps in X-Message-Stamp-* headers (BusNameStamp, DelayStamp, RedeliveryStamp, etc.). The JMS adapter is focused on body serialization and may not preserve them. Symptoms include retries resetting or looping, delays being ignored, and messages always going to the default bus after consume. If your source confirms stamps are dropped, wrap encode()/decode() and add a stamp codec. Be careful: stamp classes often do not have JMS metadata, so using JMS for stamps as well can be brittle. A small explicit codec for the stamps you actually use is usually safer. Note that 1.1.0 did not alter the encoding logic — it is still a body-only serializer.
Wrap decoding failures. Messenger expects decode() to throw Symfony\Component\Messenger\Exception\MessageDecodingFailedException for bad payloads. If JMS exceptions bubble out, the worker can crash instead of rejecting the message. A decorator is the safest place:
try {
return $this->inner->decode($encodedEnvelope);
} catch (\Throwable $e) {
throw new MessageDecodingFailedException($e->getMessage(), 0, $e);
}
Validate after deserialization. JMS commonly bypasses the constructor when hydrating objects, so constructor validation will not run. Use @JMS\PostDeserialize for invariant checks, or run validation in a middleware after the message is decoded. Keep post-deserialize hooks free of side effects: they run on every decode, including failure-transport retries.
Plan rolling deployments carefully. Existing messages encoded by Symfony’s serializer have headers like Content-Type and X-Message-Stamp-*; JMS-adapter messages have type. During migration, either drain queues before switching, or use a dispatching serializer that detects the header shape and delegates to the old or new serializer:
if (isset($encodedEnvelope['headers']['type'])) {
return $this->jmsSerializer->decode($encodedEnvelope);
}
return $this->symfonySerializer->decode($encodedEnvelope);
Test with a real serializing transport. in-memory:// passes envelopes directly and does not exercise encode()/decode(). Use Doctrine, AMQP, or Redis for integration tests. A useful test suite is: encode a message with stamps, assert the body and headers, decode it again, and assert the message and required stamps survive.
readonly properties) or newer Symfony Messenger stamp formats. Verify your JMS version specifically, not just the PHP version.SerializerInterface, global autowiring aliases can accidentally route every transport through JMS. Use an explicit service id and set it only where intended.type header is executable surface. A queue producer that can set type may be able to make JMS instantiate arbitrary classes. Always allow-list message class names if the queue is not fully trusted.BusNameStamp, DelayStamp, and RedeliveryStamp, dump the headers, and decode it. If those stamps are absent, implement a stamp codec or do not use this adapter for transports that depend on retry/delay metadata. Re-test on 1.1.0 — but expect the same result.async uses JMS but failed uses Symfony Serializer, messenger:failed:show/retry cannot decode the message. Configure both.Error: Typed property ... must not be accessed before initialization. Test with the real JSON payload, not just PHP objects.readonly properties depend on your JMS version, not on this adapter; 1.1.0 does not add support for them.SerializationContext::create()->setGroups([...]) explicitly if needed.composer diff or a git diff in a fork). Confirm the PHP 8 change is limited to compatibility and does not touch encode()/decode() behaviour your decorators depend on.bin/console debug:container app.messenger.jms_serializer confirms wiring; a temporary decorator that dumps $encodedEnvelope shows exactly what goes on the wire; messenger:failed:show -vv shows failure-transport decoding; raw AMQP/Redis inspection confirms the type header and JSON body. Use messenger:consume --limit=1 -vv while iterating.How can I help you explore Laravel packages today?