wisembly/amqp-bundle
A Symfony bundle for integrating AMQP/RabbitMQ messaging into PHP applications. It provides services and configuration for connections, publishing, and consuming messages, wrapping the PHP AMQP extension to support asynchronous queues and background workers within Symfony’s dependency injection setu...
Know what you're adopting first. This bundle is archived, last released in January 2018, and has effectively zero downstream dependents. It is a thin Symfony 2 wrapper around Swarrot (which itself wraps php-amqplib). Treat it as a legacy integration, not a starting point for new work — but if you've inherited a codebase already using it, the steps below get you productive.
Prerequisites
app/console layoutext-amqp or the pure-PHP php-amqplib transportcomposer require wisembly/amqp-bundle — and pin it in composer.json since no further patches will arriveWire it up
app/AppKernel.php:
new Wisembly\Bundle\AMQPBundle\WisemblyAMQPBundle(),
app/config/config.yml (confirm the exact key by reading DependencyInjection/Configuration.php — that file is the authoritative schema).php app/console debug:container | grep -i amqp
php app/console debug:container | grep -i swarrot
Where to look first, in order
| File | Why |
|---|---|
DependencyInjection/Configuration.php |
The real documentation — every config key, default, and required value |
Resources/config/services.xml |
Service IDs you'll inject; this is what you autowire/reference |
README.md |
Usually one example producer + one example consumer |
| Swarrot's own docs | 90% of your questions (processors, middleware, ack/nack) are Swarrot questions, not bundle questions |
First use case — the ten-minute win: publish a JSON message from a controller and consume it with a Swarrot processor. Once that round-trip works, everything else is variations on it.
The bundle's publisher service is the seam you'll use everywhere. Wrap it in your own small service so application code never touches the bundle directly — this is what makes the eventual migration to Symfony Messenger tractable.
class EventPublisher
{
private $publisher;
public function __construct($publisher) // the bundle's publisher service
{
$this->publisher = $publisher;
}
public function publish(string $routingKey, array $payload): void
{
$this->publisher->publish(
$routingKey,
json_encode($payload, JSON_THROW_ON_ERROR),
['content_type' => 'application/json', 'delivery_mode' => 2]
);
}
}
Conventions that pay off: routing key = event name (order.created, user.registered), body = JSON, delivery_mode: 2 for anything you can't afford to lose, and a message_id + occurred_at header on every message for tracing and dedupe.
ProcessorInterface, tag it, run itSwarrot's model is a processor chain. You write a class implementing Swarrot\Processor\ProcessorInterface::process(Message $message, array $options), register it as a service with the processor tag, and reference it from the consumer config.
class OrderCreatedProcessor implements ProcessorInterface
{
public function process(Message $message, array $options): ProcessorInterface
{
$data = json_decode($message->getBody(), true, 512, JSON_THROW_ON_ERROR);
// ... handle ...
return $this; // returning self continues the chain; this is the convention
}
}
Then run it:
php app/console swarrot:consume:order_created -vv
Swarrot ships processors you should compose rather than reimplement. A healthy consumer stack looks like:
do_execute → ack → (retry | exception) → your processor
The ack processor is what turns auto-ack off and makes redelivery work; the retry processor gives you exponential backoff and a dead-letter path. Configure them as tagged processors rather than writing your own retry loop — reimplementing this is the single most common way teams get into trouble with this stack.
Define separate connections for separate vhosts/users rather than sharing a global one, and give each event type its own queue bound to a topic exchange. This makes scaling consumers independently trivial (supervisor with N workers on the high-volume queue) and makes dead-lettering readable.
Swarrot\...\Message built from a raw string body. No broker needed. Assert on the side effects, not the AMQP calls.rabbitmq:3-management), publish, consume one message with --limit=1, assert, tear down. Keep these in a separate suite — they're slow and flaky in CI by nature.Consumers are long-lived processes. Use supervisor or systemd with autorestart=true, stopwaitsecs long enough for in-flight messages, and a rolling restart on deploy. Never run a consumer inside a web request.
It is archived — budget the exit. No security fixes, no PHP 8 support, no Symfony 4/5 compatibility. Pin the exact version, and when you touch this code, wrap the bundle behind your own EventPublisher / ProcessorInterface seams so a migration to symfony/messenger (or raw Swarrot, which is still maintained) is a mechanical swap rather than a rewrite.
Config lives under two keys. It's easy to lose an hour because connection settings are under the bundle's root key but consumer/processor definitions may live under Swarrot's. When something "isn't picked up," run php app/console config:dump-reference <bundle_alias> and compare against what you wrote.
Serialization mismatch is the classic outage. PHP's serialize() and JSON are both plausible defaults depending on how you configure things. Pick JSON explicitly, set it on both publisher and consumer, and version your payloads ({"v":1, ...}). A consumer that unserialize()s a JSON body throws an opaque error deep in the stack.
Ack semantics bite on crashes. With auto-ack, a consumer that dies mid-processing silently drops the message. With manual ack (via the ack processor), it redelivers — which means your processors must be idempotent. Key your side effects on message_id or a business key.
Doctrine's EntityManager goes stale. After any exception in a long-running consumer, the EM is closed and every subsequent message fails with "The EntityManager is closed." Reset it in a custom middleware, or catch and clear inside the processor. This is the #1 cause of "it worked for 200 messages then stopped."
Memory leaks are guaranteed. Long-lived PHP processes accumulate memory in Doctrine's identity map, loggers, and the amqplib connection buffers. Either pass a message limit to the consume command and let the supervisor restart the worker, or call gc_collect_cycles() and $em->clear() every N messages. Watch RSS in monitoring.
Connection heartbeats and idle timeouts. A consumer blocked for longer than the broker's heartbeat window gets disconnected with a cryptic Broken pipe / Channel closed error. Set a heartbeat shorter than your broker's, and handle reconnect in a loop rather than letting the process die.
Debugging tools, in order of usefulness:
rabbitmqctl list_queues name messages consumers # is anything piling up?
rabbitmqctl list_bindings # did the routing key bind correctly?
# RabbitMQ management UI on :15672 — inspect a message's headers/body directly
php app/console swarrot:consume:queue -vv # bundle + Swarrot debug output
Nine times out of ten "the consumer isn't receiving" is a routing-key typo or a queue that was never bound.
Extension points worth knowing: custom Swarrot middleware (tag it and place it in the chain) for cross-cutting concerns like request-id propagation, EM reset, and timing metrics; a custom message provider if you need to pull from something other than AMQP; and service-definition overrides in your app's services.yml to swap the bundle's publisher for a decorated version without forking.
Message size. Default php-amqplib/RabbitMQ limits are generous but the management UI and logging choke on multi-megabyte payloads. Keep bodies small — publish a reference ID and let the consumer fetch the record; it also avoids stale data.
How can I help you explore Laravel packages today?