swarrot/swarrot-bundle
SwarrotBundle integrates Swarrot message consumers into Symfony apps. Configure RabbitMQ connections, then define consumers with a processor service and a customizable middleware stack for signal handling, message limits, execution time, memory limits, and Doctrine integration.
Know what it is first. SwarrotBundle is a thin Symfony DI/console layer over the core swarrot/swarrot library. The bundle gives you three things: a publisher service backed by named message_types, one console command per configured consumer, and a configurable middleware stack wrapped around your processor. It deliberately does not provision exchanges, queues or bindings — that is out of scope by design (the README points at odolbeau/rabbit-mq-admin-toolkit). If your team expects a bundle that also declares topology, this is not that bundle.
Requirements. As of 2.7.0 (2026-07-29): PHP >= 8.2 and Symfony 6.4 – 8.0. Older lines supported older Symfony, so pin deliberately.
composer require swarrot/swarrot-bundle
Register it the modern way — the README still shows app/AppKernel.php, which is stale:
// config/bundles.php
return [
// ...
Swarrot\SwarrotBundle\SwarrotBundle::class => ['all' => true],
];
Pick your provider up front. provider: pecl requires ext-amqp; provider: amqp_lib requires php-amqplib/php-amqplib. This is a one-line config choice, but it changes the whole transport stack underneath, so decide before you write consumers.
Minimal viable config — one connection, one consumer, one publishable type:
# config/packages/swarrot.yaml
swarrot:
provider: amqp_lib
default_connection: rabbitmq
connections:
rabbitmq:
url: "%env(RABBITMQ_URL)%"
consumers:
order_consumer:
processor: app.order_processor
middleware_stack:
- configurator: swarrot.processor.signal_handler
- configurator: swarrot.processor.max_messages
- configurator: swarrot.processor.exception_catcher
- configurator: swarrot.processor.ack
extras:
poll_interval: 500000
messages_types:
order_publisher:
connection: rabbitmq
exchange: orders
routing_key: order.created
First use case — publish from a controller/service, consume from a CLI worker:
$this->get('swarrot.publisher')->publish(
'order_publisher',
new \Swarrot\Broker\Message('{"id":42}')
);
bin/console swarrot:consume:order_consumer orders_queue rabbitmq
Where to look first when the README runs out: (1) the configuration reference block above — it is the de-facto reference for this bundle; (2) vendor/swarrot/swarrot/src/Swarrot/Processor/ for what each configurator service actually does and which extras keys it reads; (3) vendor/swarrot/swarrot-bundle/ for the *ProcessorConfigurator classes — they validate and translate your extras. With ~90 stars and effectively no dependents, the source is the documentation.
Publisher with config overrides. message_types are reusable publish presets; override per call rather than duplicating entries:
$publisher->publish('order_publisher', $message, [
'exchange' => 'orders_dead',
'routing_key' => 'order.failed',
'connection' => 'secondary',
]);
Inject swarrot.publisher (it is an explicitly public service) or add an alias in services.yaml for autowiring.
Processor = pure business logic. Implement Swarrot\Processor\ProcessorInterface::process(Message $message, array $options) and keep it free of broker concerns — acking, retrying, error catching and lifetimes belong in the middleware stack. This keeps processors unit-testable with a plain Message and an empty options array.
Compose the stack by intent, not by copy-paste. Read a consumer config as an ordered list of cross-cutting concerns, outermost first:
| Position | Typical configurator | Why it sits there |
|---|---|---|
| outer | signal_handler |
Must own the outermost loop to shut down gracefully between messages |
max_messages / max_execution_time / memory_limit |
Process recycling for long-running workers | |
doctrine_connection, doctrine_object_manager |
Ping/reconnect and clear the EM between messages | |
exception_catcher |
Turns thrown exceptions into nacks instead of fatal errors | |
ack |
Commits the message after everything below it succeeded | |
| inner | retry |
Sees the raw exception first, republishes to the retry exchange |
| innermost | your processor |
Long-running workers. The pattern that works in production: signal_handler + max_messages + max_execution_time + doctrine_object_manager + services_resetter, run under Supervisor/systemd. services_resetter is the one people forget — it resets container services between messages so stateful services (mailers, request stacks, Doctrine) don't leak.
Retry with a dedicated exchange instead of requeueing on the same queue:
- configurator: swarrot.processor.retry
extras:
retry_exchange: retry
retry_attempts: 3
retry_routing_key_pattern: 'retry_%%attempt%%'
Provision the retry exchange and TTL/DLQ wiring outside the bundle.
One consumer per queue, many consumers per processor. Consumers are cheap; define several consumers reusing the same processor service but with different stacks (e.g. a fast one with max_messages: 100 and a bulk one with max_messages: 1) and give them human-friendly names via command_alias.
Custom middleware — implement ProcessorConfiguratorInterface, register it as abstract: true, reference the service id in middleware_stack. Same extension shape applies to providers: implement Broker\FactoryInterface, tag swarrot.provider_factory (optionally with alias: redis) and set swarrot.provider: redis.
Testing. Override the publisher class in the test env so publishes are no-ops:
# config/packages/test/swarrot.yaml
parameters:
swarrot.publisher.class: Swarrot\SwarrotBundle\Broker\BlackholePublisher
For consumption tests, invoke swarrot:consume:<name> via CommandTester against a throwaway queue rather than trying to mock the internal loop.
Observability. Point swarrot.logger at a dedicated Monolog channel (monolog.logger.swarrot) so broker noise doesn't drown your app logs. Note the bundle ships a data collector, so the Symfony profiler shows publish activity in dev.
middleware_stack order is not cosmetic. Because each configurator decorates the next, the first entry is the outermost. Putting ack outside retry means a retried message is still acked; swapping them changes semantics entirely. Always reason about the stack outside-in.--no-catch, --no-retry, --max-messages, --max-execution-time only affect consumers that include the matching configurator. Easy hour to lose.poll_interval is in microseconds (default 500000 ≈ 0.5 s), not milliseconds. --poll-interval on the command line overrides the config.% must be escaped as %% in YAML, which is why the README writes retry_%%attempt%%. Single % gets parsed as a container parameter and blows up at compile time.doctrine_object_manager, you accumulate a bloated identity map and stale entities. Pair it with doctrine_connection (doctrine_ping: true) when workers idle long enough for the broker to drop the connection.AppKernel, processors_stack, publisher_logger) were removed in 2.0; blog posts and older StackOverflow answers will mislead you. Trust the changelog: 2.0 dropped processors_stack, 2.7.0 converted internal config files to PHP and added Symfony 8.BlackholePublisher only silences publishing. It does not stub consumption, and overriding a container parameter is a blunt instrument — it also affects any code that reads swarrot.publisher.class. Prefer it in test only, or decorate the publisher service in a dedicated test container.bin/console debug:container swarrot to see generated consumer commands and services; bin/console list swarrot to confirm command names (derived from consumer keys, so order_consumer → swarrot:consume:order_consumer); run with -vvv --no-catch to surface the real exception instead of a caught-and-logged one.default_command is the escape hatch for anything the bundle won't let you configure — swap in a subclass of Swarrot\SwarrotBundle\Command\SwarrotCommand and extend the worker loop without forking the bundle.vendor/ source, and pin the version — minor releases here have historically raised PHP/Symfony floors (2.5.0 → Symfony 7, 2.7.0 → PHP 8.2).How can I help you explore Laravel packages today?