elandlord/nats-php-bundle
A Symfony bundle that integrates NATS with Symfony Messenger using a NatsConnection, helping you publish and consume messages through NATS in Symfony applications.
Know what the bundle does and doesn't do. Despite the name, this is not a NATS client. It is a thin Symfony Messenger transport that reuses an existing NatsConnection service and hands publish/consume to Messenger's bus, routing, retry, and middleware stack. First mental model: you own the connection and the broker topology; the bundle owns the transport glue.
Minimal setup:
NatsConnection service in your container, from the companion elandlord/nats-php package. This is the step people miss: if no NatsConnection is registered/aliased, the transport can't be built. Wire it explicitly in config/services.yaml rather than hoping for autowiring.composer require elandlord/nats-php-bundle) and let Flex register it, or add it to config/bundles.php.# config/packages/messenger.yaml
framework:
messenger:
transports:
nats_events:
dsn: '%env(MESSENGER_NATS_DSN)%'
routing:
App\Message\UserCreated: nats_events
The exact DSN scheme and available transport options are whatever NatsTransportFactory::supports() and the bundle's Configuration.php accept — there is no meaningful config reference in the README, so read those two files first, then confirm with bin/console config:dump-reference / debug:config.
Where to look first, in order:
DependencyInjection/Configuration.php — the real documentation.Transport/NatsTransportFactory.php and Transport/NatsTransport.php — DSN parsing, subject/stream/consumer option names, ack behaviour.Resources/config/services.* — which service ids/aliases exist and what the transport is injected with.First day-one use case: take one non-critical, fire-and-forget event (analytics ping, notification, audit log), dispatch it from a controller via MessageBusInterface, and run bin/console messenger:consume nats_events -vv in a terminal. Watch the message land, the handler fire, and the ack settle. That single loop exercises routing, serialization, and acking — the three things that break in production.
Message DTO + handler, routed by class. Stay inside Messenger conventions instead of touching the transport directly:
final class UserCreated
{
public function __construct(
public readonly string $userId,
public readonly \DateTimeImmutable $occurredAt,
) {}
}
Dispatch with $bus->dispatch(new UserCreated($id, new \DateTimeImmutable())) and let framework.messenger.routing decide the transport. This keeps NATS swappable — moving to AMQP later is a config change, not a refactor.
Serialization is Symfony Serializer, not raw JSON. Since v1.3.0 the bundle hydrates messages through the Symfony Serializer, and v1.4.1 added BackedEnumNormalizer. In practice: type your constructor properties (including backed enums and DateTimeImmutable), keep message classes immutable, and make sure the normalizer set you need is enabled on the serializer the bundle picks up. Avoid untyped array $payload constructors — you lose the benefit and inherit all the fragility.
Durable consumer config as code. With v1.4.3, consumer configuration changes are applied to existing durable consumers instead of being silently ignored. Treat the transport's consumer block in messenger.yaml as production configuration: review diffs, and evolve filter subjects/durables deliberately rather than assuming a config edit is inert.
Middleware is where the operational behaviour lives. Because this is a Messenger transport, you get for free:
doctrine_transaction around handlers that write to the DB,failed transport for poison messages (messenger:failed:show / :retry),Add a logging middleware before you need it — NATS failures are otherwise quiet.
Worker supervision. Long-lived messenger:consume processes should run under Supervisor/systemd with --time-limit and --memory-limit so connections are periodically recycled, and messenger:stop-workers should be part of your deploy so code changes take effect.
Multiple transports for isolation. One transport per stream/subject family (e.g. nats_events, nats_jobs) maps cleanly onto separate worker pools with different concurrency and retry policies, and keeps a noisy consumer from starving a critical one.
Extension points worth knowing: a custom SerializerInterface if you need Protobuf/Avro, a decorating service around NatsConnection for metrics and connection health, and custom Messenger middleware for cross-cutting policy.
NatsConnection service ⇒ transport construction fails, usually with a DI error that doesn't mention NATS at all. If autowiring fails, define the service explicitly and check the alias the transport factory expects.>=1.4.0 for ack correctness. v1.4.0 is literally "fix to ack nats message." On earlier versions you can expect redelivery storms under load. Similarly, v1.2.0 fixed consumer unsubscribe and performance, and v1.1.0 fixed messages simply not being picked up — this library's history is a list of "silently does nothing" bugs, so pin deliberately and read the changelog before upgrading.nats consumer info <stream> <durable> and nats stream view tell you whether anything arrived and whether the consumer is bound. Then check debug:messenger on the PHP side.nats consumer info output before deploys, and know that some durable fields are immutable — attempting to change them fails or leaves broker state diverged from your YAML. Keep a delete-and-recreate runbook.failed transports won't save a message that was never persisted. Decide which guarantee you're buying before you promise anyone "it's queued."messenger:failed:show -vv before blaming your handler.How can I help you explore Laravel packages today?