Symfony Messenger transport for NATS JetStream. PHP 8.2+, Symfony ^7.2 / ^8.0. Uses idct/php-nats-jetstream-client (amphp-based coroutines). Ships IDCTNatsMessengerBundle (Flex auto-enables it), which registers the transport factory and the igbinary serializer - no manual wiring needed. Each...
php artisan boost:add-skill idct/symfony-nats-messenger
Save this content to: AGENTS.md
# Project Guide
## Overview
Symfony Messenger transport for NATS JetStream. PHP 8.2+, Symfony ^7.2 / ^8.0.
Uses `idct/php-nats-jetstream-client` (amphp-based coroutines).
Ships `IDCTNatsMessengerBundle` (Flex auto-enables it), which registers the transport factory
and the igbinary serializer - no manual wiring needed. Each service has one id, its class name:
Symfony wraps a transport's serializer for signing once per id, so a second id would wrap it twice.
## Project Memory
Read [docs/PROJECT_MEMORY.md](docs/PROJECT_MEMORY.md) when starting work for maintainer preferences,
the local client-library relationship, behavioral pitfalls, and a dated issue/environment snapshot.
Refresh changeable facts from source and GitHub before relying on them; update the memory when new
findings affect future work. This guide remains the project instruction source.
## Installation
```bash
composer require idct/symfony-nats-messenger
```
Development setup:
```bash
composer install
composer test # PHPStan + fast unit tests (run after every change)
```
## Application Structure
```
src/
├── IDCTNatsMessengerBundle.php # Symfony bundle - auto-registers factory + serializer services
├── NatsTransport.php # TransportInterface, MessageCountAwareInterface, SetupableTransportInterface
├── NatsTransportFactory.php # TransportFactoryInterface - DSN parsing, transport creation
├── TypeCoercion.php # Safe scalar type coercion helpers (final, static)
├── Options/
│ ├── NatsTransportConfiguration.php # Immutable readonly config value object
│ ├── NatsTransportConfigurationBuilder.php # DSN + options parsing, validation, builds Configuration
│ ├── RetryHandler.php # Enum: SYMFONY (TERM) | NATS (NAK)
│ └── TransportOption.php # Enum: all configurable option keys
└── Serializer/
├── AbstractEnveloperSerializer.php # Base serializer with encode/decode envelope wrapping
└── IgbinarySerializer.php # Default serializer (falls back to PhpSerializer)
```
```
tests/
├── bootstrap.php
├── unit/ # PHPUnit 11 to 13 - fast, no NATS required
│ ├── NatsTransportTest.php # Transport core: setup, send, get, ack, reject, retry
│ ├── NatsTransportFactoryTest.php # Factory: DSN scheme detection, transport creation
│ ├── Options/
│ │ ├── NatsTransportConfigurationBuilderTest.php # Builder validation, DSN parsing, option merging
│ │ └── NatsTransportConfigurationTest.php # Config accessors, type coercion
│ └── Serializer/
│ ├── AbstractEnveloperSerializerTest.php # Encode/decode round-trips, error cases
│ └── IgbinarySerializerTest.php # Igbinary serialize/deserialize
├── functional/ # Behat - requires running NATS server
│ ├── features/
│ │ ├── nats_setup.feature # Stream creation, update, multi-subject, message flow
│ │ ├── nats_stream_limits.feature # max_bytes, max_msgs, max_msgs_per_subject
│ │ ├── nats_consumer.feature # Custom consumer names
│ │ ├── nats_batching.feature # Batch consumption
│ │ ├── nats_nak.feature # NAK retry handler
│ │ ├── nats_term.feature # TERM failure handler
│ │ ├── nats_delayed.feature # Scheduled/delayed messages
│ │ ├── nats_tls.feature # TLS connections
│ │ └── nats_mtls.feature # mTLS with client certificates
│ └── tests/Behat/NatsSetupContext.php # All step definitions
└── nats/ # Docker Compose + NATS configs for testing
├── docker-compose.yaml
├── nats.conf / nats-tls.conf / nats-mtls.conf
└── certs/ # generate.sh for the test TLS certificates (generated, not committed)
```
## Features
- **DSN schemes**: `nats-jetstream://` and `nats-jetstream+tls://`
- **Stream management**: create, update existing (subject merging, config preservation)
- **Retention policies**: `stream_max_age`, `stream_max_bytes`, `stream_max_messages`, `stream_max_messages_per_subject`
- **Storage backends**: `file` or `memory` via `stream_storage`
- **Replication**: `stream_replicas` for HA
- **Consumer strategies**: shared (batching=1) or independent consumers
- **Batching**: configurable `batching` count and `max_batch_timeout`
- **Retry handling**: `retry_handler` - `symfony` (TERM, default) or `nats` (NAK)
- **Scheduled messages**: `scheduled_messages` flag, `DelayStamp` → NATS schedule headers
- **TLS/mTLS**: full TLS config options including client certificates
- **Authentication**: username/password, token, JWT, NKey
- **Serialization**: igbinary (default with fallback) or custom via `AbstractEnveloperSerializer`
- **Publish validation**: JetStream publish responses are parsed and validated
- **Connection checks**: a lost connection is dialled again; `ping_after_idle` PINGs a connection that sat idle, or that an operation failed on, before reuse, and before the NAK or TERM of a failed delivery
- **Timeouts**: `connection_timeout` (the dial), `request_timeout` (replies from the server), `max_batch_timeout` (pulls)
- **Duplicate protection**: the `deduplicate` option and `DeduplicationIdStamp`, sent as `Nats-Msg-Id`; with `reconnect: true`, `resend_on_reconnect` sends such a publish again on the recovered connection
- **Keepalive**: `messenger:consume --keepalive`, an in-progress acknowledgement queued from the signal handler
## Testing
### After every code change
```bash
composer test # PHPStan (level max) + fast unit tests
```
### Unit tests with coverage
```bash
composer test:unit # Coverage report → clover.xml + coverage/
composer coverage:check # Verify ≥ 90% statement coverage
```
### Functional tests (require Docker)
```bash
composer test:functional:setup # Install Behat dependencies (first time)
composer nats:start # Generate the test TLS certs, then start NATS in Docker
composer test:functional # Run Behat scenarios
composer examples # Run examples/*.php against the test NATS (needs nats:start)
composer nats:stop # Stop NATS
```
### Key testing patterns
- Unit tests use `TestableNatsTransport` (overrides `connect()` to skip live NATS)
- `RuntimeTestableNatsTransport` allows injecting `JetStreamContext` and `NatsClient` test doubles via setters
- The unit suite runs on PHPUnit 11 to 13: PHP 8.2 installs 11, PHP 8.3 installs 12 and PHP 8.4 or later
installs 13 (CI runs 11 and 13). Make a test double that sets no expectations with `createStub()`, not
`createMock()`: PHPUnit 12 and later report a notice for a mock without expectations, and CI's PHP 8.5 job
fails on it. Keep `createMock()` where the test checks calls, for example `expects(self::never())`.
- Behat step definitions and hooks are **attributes** (`#[Given]`, `#[When]`, `#[Then]`, `#[AfterScenario]`), and the
Behat configuration is `tests/functional/behat.php`: Behat 4 reads neither doc-comment annotations nor YAML. The
configuration is strict, so a scenario with an undefined step fails the run instead of being skipped.
- Mock `Future::complete()` / `Future::error()` for async return values
- `self::callback()` for complex assertion on method arguments (e.g., stream options arrays)
## Documentation Maintenance
### Test Coverage Map (`docs/TESTS.md`)
- Keep `docs/TESTS.md` up to date whenever tests are added, removed, or renamed.
- When a new feature is implemented, add it to the appropriate table with the tests that cover it.
- When a test method is renamed or deleted, update the corresponding entry.
### Changelog (`docs/CHANGELOG.md`)
- Every release must have an entry in `docs/CHANGELOG.md`.
- Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
- Add new items under `## [Unreleased]` as work progresses; move them to a versioned heading at release time.
- Categorize changes as Added, Changed, Fixed, Deprecated, Removed, or Security.
### PR Merge Documentation (`docs/PRs/`)
- When a PR is merged or its features are adapted, add a file to `docs/PRs/` named `PR-NNN-short-description.md`.
- Include: what the PR proposed, how the features were implemented, and which tests cover them.
Symfony Messenger transport for NATS JetStream. PHP 8.2+, Symfony ^7.2 / ^8.0.
Uses idct/php-nats-jetstream-client (amphp-based coroutines).
Ships IDCTNatsMessengerBundle (Flex auto-enables it), which registers the transport factory
and the igbinary serializer - no manual wiring needed. Each service has one id, its class name:
Symfony wraps a transport's serializer for signing once per id, so a second id would wrap it twice.
Read docs/PROJECT_MEMORY.md when starting work for maintainer preferences, the local client-library relationship, behavioral pitfalls, and a dated issue/environment snapshot. Refresh changeable facts from source and GitHub before relying on them; update the memory when new findings affect future work. This guide remains the project instruction source.
composer require idct/symfony-nats-messenger
Development setup:
composer install
composer test # PHPStan + fast unit tests (run after every change)
src/
├── IDCTNatsMessengerBundle.php # Symfony bundle - auto-registers factory + serializer services
├── NatsTransport.php # TransportInterface, MessageCountAwareInterface, SetupableTransportInterface
├── NatsTransportFactory.php # TransportFactoryInterface - DSN parsing, transport creation
├── TypeCoercion.php # Safe scalar type coercion helpers (final, static)
├── Options/
│ ├── NatsTransportConfiguration.php # Immutable readonly config value object
│ ├── NatsTransportConfigurationBuilder.php # DSN + options parsing, validation, builds Configuration
│ ├── RetryHandler.php # Enum: SYMFONY (TERM) | NATS (NAK)
│ └── TransportOption.php # Enum: all configurable option keys
└── Serializer/
├── AbstractEnveloperSerializer.php # Base serializer with encode/decode envelope wrapping
└── IgbinarySerializer.php # Default serializer (falls back to PhpSerializer)
tests/
├── bootstrap.php
├── unit/ # PHPUnit 11 to 13 - fast, no NATS required
│ ├── NatsTransportTest.php # Transport core: setup, send, get, ack, reject, retry
│ ├── NatsTransportFactoryTest.php # Factory: DSN scheme detection, transport creation
│ ├── Options/
│ │ ├── NatsTransportConfigurationBuilderTest.php # Builder validation, DSN parsing, option merging
│ │ └── NatsTransportConfigurationTest.php # Config accessors, type coercion
│ └── Serializer/
│ ├── AbstractEnveloperSerializerTest.php # Encode/decode round-trips, error cases
│ └── IgbinarySerializerTest.php # Igbinary serialize/deserialize
├── functional/ # Behat - requires running NATS server
│ ├── features/
│ │ ├── nats_setup.feature # Stream creation, update, multi-subject, message flow
│ │ ├── nats_stream_limits.feature # max_bytes, max_msgs, max_msgs_per_subject
│ │ ├── nats_consumer.feature # Custom consumer names
│ │ ├── nats_batching.feature # Batch consumption
│ │ ├── nats_nak.feature # NAK retry handler
│ │ ├── nats_term.feature # TERM failure handler
│ │ ├── nats_delayed.feature # Scheduled/delayed messages
│ │ ├── nats_tls.feature # TLS connections
│ │ └── nats_mtls.feature # mTLS with client certificates
│ └── tests/Behat/NatsSetupContext.php # All step definitions
└── nats/ # Docker Compose + NATS configs for testing
├── docker-compose.yaml
├── nats.conf / nats-tls.conf / nats-mtls.conf
└── certs/ # generate.sh for the test TLS certificates (generated, not committed)
nats-jetstream:// and nats-jetstream+tls://stream_max_age, stream_max_bytes, stream_max_messages, stream_max_messages_per_subjectfile or memory via stream_storagestream_replicas for HAbatching count and max_batch_timeoutretry_handler - symfony (TERM, default) or nats (NAK)scheduled_messages flag, DelayStamp → NATS schedule headersAbstractEnveloperSerializerping_after_idle PINGs a connection that sat idle, or that an operation failed on, before reuse, and before the NAK or TERM of a failed deliveryconnection_timeout (the dial), request_timeout (replies from the server), max_batch_timeout (pulls)deduplicate option and DeduplicationIdStamp, sent as Nats-Msg-Id; with reconnect: true, resend_on_reconnect sends such a publish again on the recovered connectionmessenger:consume --keepalive, an in-progress acknowledgement queued from the signal handlercomposer test # PHPStan (level max) + fast unit tests
composer test:unit # Coverage report → clover.xml + coverage/
composer coverage:check # Verify ≥ 90% statement coverage
composer test:functional:setup # Install Behat dependencies (first time)
composer nats:start # Generate the test TLS certs, then start NATS in Docker
composer test:functional # Run Behat scenarios
composer examples # Run examples/*.php against the test NATS (needs nats:start)
composer nats:stop # Stop NATS
TestableNatsTransport (overrides connect() to skip live NATS)RuntimeTestableNatsTransport allows injecting JetStreamContext and NatsClient test doubles via setterscreateStub(), not
createMock(): PHPUnit 12 and later report a notice for a mock without expectations, and CI's PHP 8.5 job
fails on it. Keep createMock() where the test checks calls, for example expects(self::never()).#[Given], #[When], #[Then], #[AfterScenario]), and the
Behat configuration is tests/functional/behat.php: Behat 4 reads neither doc-comment annotations nor YAML. The
configuration is strict, so a scenario with an undefined step fails the run instead of being skipped.Future::complete() / Future::error() for async return valuesself::callback() for complex assertion on method arguments (e.g., stream options arrays)docs/TESTS.md)docs/TESTS.md up to date whenever tests are added, removed, or renamed.docs/CHANGELOG.md)docs/CHANGELOG.md.## [Unreleased] as work progresses; move them to a versioned heading at release time.docs/PRs/)docs/PRs/ named PR-NNN-short-description.md.How can I help you explore Laravel packages today?