rabbitevents / symfony-bundle
RabbitEvents for Symfony โ publish and listen to events across microservices via RabbitMQ
Package info
github.com/rabbitevents/symfony-bundle
Type:symfony-bundle
pkg:composer/rabbitevents/symfony-bundle
Requires
- php: ^8.2
- ext-json: *
- enqueue/amqp-lib: ^0.10
- psr/log: ^3.0
- symfony/config: ^7.0|^8.0
- symfony/console: ^7.0|^8.0
- symfony/dependency-injection: ^7.0|^8.0
- symfony/event-dispatcher: ^7.0|^8.0
- symfony/framework-bundle: ^7.0|^8.0
Requires (Dev)
- google/protobuf: ^4.33
- mockery/mockery: ^1.6.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5|^11.0
Suggests
- ext-amqp: Using this extension makes your app faster. If you're using it you need to install enqueue/amqp-ext.
- ext-pcntl: Required to use all features of the worker.
- enqueue/amqp-ext: This package is necessary if you want to use `ext-amqp`.
- google/protobuf: Required for Protobuf serialization support (auto-registered when installed).
This package is auto-updated.
Last update: 2026-08-25 13:19:43 UTC
README
Publish and listen to events across microservices via RabbitMQ โ for Symfony.
This bundle provides inter-application event communication using RabbitMQ topic exchanges. Events published by any microservice are routed across services based on routing keys, with full support for wildcard routing.
Features
- ๐ท๏ธ Modern PHP 8+ Attributes: Register listeners via
#[AsRabbitListener], on whole classes or individual methods. - โก Zero-Overhead Publisher: Lazy connection initialization โ RabbitMQ connections are only established when messages are published.
- ๐ Wildcard Routing: Full AMQP topic routing key support (
*and#). - ๐ฆ Pluggable Serialization: Built-in JSON serializer, optional Protobuf serializer, or custom serializers via Symfony DI tags.
- ๐ Resilience & Retries: Built-in retry handling (
--tries,--sleep), dead-letter support, andfailed()listener callbacks. - ๐งช Testing Support:
PublishableEventTestingtrait for asserting event publication without a live RabbitMQ broker. - ๐ Modern PHP & Symfony: Compatible with PHP 8.2+ and Symfony 6.4, 7.x, and 8.x.
Table of Contents
- Installation
- Quick Start
- Publishing Events
- Listening for Events
- Console Commands
- Configuration
- Serialization
- Testing
- Examples
- License
Installation
Install the bundle via Composer:
composer require rabbitevents/symfony-bundle
If you don't use Symfony Flex, enable the bundle in config/bundles.php:
return [ // ... RabbitEvents\Bundle\RabbitEventsBundle::class => ['all' => true], ];
Quick Start
1. Configure Connection
Create config/packages/rabbitevents.yaml:
rabbitevents: connection: host: '%env(RABBITMQ_HOST)%' port: '%env(int:RABBITMQ_PORT)%' user: '%env(RABBITMQ_USER)%' pass: '%env(RABBITMQ_PASSWORD)%' vhost: '/' exchange: 'events'
Add environment variables to your .env:
RABBITMQ_HOST=127.0.0.1 RABBITMQ_PORT=5672 RABBITMQ_USER=guest RABBITMQ_PASSWORD=guest
2. Create and Publish an Event
namespace App\Event; use RabbitEvents\Bundle\Publisher\AbstractPublishableEvent; class OrderCreatedEvent extends AbstractPublishableEvent { public function __construct(public readonly int $orderId) { } public function publishEventKey(): string { return 'order.created'; } public function toPublish(): array { return [ 'order_id' => $this->orderId, 'timestamp' => time(), ]; } } // Publish from anywhere: (new OrderCreatedEvent(12345))->publish();
3. Create a Listener
namespace App\Listener; use RabbitEvents\Bundle\Listener\Attribute\AsRabbitListener; #[AsRabbitListener(event: 'order.created')] class OrderCreatedListener { public function handle(array $payload): void { // Handle payload: ['order_id' => 12345, 'timestamp' => ...] echo sprintf("Order #%d created!\n", $payload['order_id']); } }
4. Start Listening
bin/console rabbitevents:listen order.created --service=my-service
Publishing Events
1. Event Classes (AbstractPublishableEvent)
Extending AbstractPublishableEvent gives your event a static-style $event->publish() helper:
namespace App\Event; use RabbitEvents\Bundle\Publisher\AbstractPublishableEvent; class UserRegisteredEvent extends AbstractPublishableEvent { public function __construct( public readonly int $userId, public readonly string $email ) { } public function publishEventKey(): string { return 'user.registered'; } public function toPublish(): array { return [ 'user_id' => $this->userId, 'email' => $this->email, ]; } } // Publish: $event = new UserRegisteredEvent(1, 'user@example.com'); $event->publish();
2. Publishing via Dependency Injection
For clean architecture and easier unit testing, inject RabbitEvents\Bundle\Publisher directly into your services:
namespace App\Service; use App\Event\UserRegisteredEvent; use RabbitEvents\Bundle\Publisher; class UserService { public function __construct( private readonly Publisher $publisher ) { } public function register(string $email): void { // ... save user ... $this->publisher->publish(new UserRegisteredEvent($user->getId(), $email)); } }
3. Implementing ShouldPublish Directly
If you prefer not to extend AbstractPublishableEvent, implement ShouldPublish:
namespace App\Event; use RabbitEvents\Bundle\Publisher\ShouldPublish; class InvoiceGeneratedEvent implements ShouldPublish { public function __construct(public readonly string $invoiceNumber) {} public function publishEventKey(): string { return 'invoice.generated'; } public function toPublish(): array { return ['invoice_number' => $this->invoiceNumber]; } }
Listening for Events
Listeners are automatically discovered and registered via the #[AsRabbitListener] attribute.
Class-Level Listener Attribute
Place #[AsRabbitListener] on the class. The handle() method (or __invoke()) will be called when the event arrives:
namespace App\Listener; use Psr\Log\LoggerInterface; use RabbitEvents\Bundle\Listener\Attribute\AsRabbitListener; #[AsRabbitListener(event: 'order.created')] class OrderCreatedListener { public function __construct( private readonly LoggerInterface $logger ) { } public function handle(array $payload): void { $this->logger->info('Order received', ['order_id' => $payload['order_id']]); } }
Method-Level Listener Attributes
You can group multiple event handlers in a single service class by annotating individual methods:
namespace App\Listener; use Psr\Log\LoggerInterface; use RabbitEvents\Bundle\Listener\Attribute\AsRabbitListener; class OrderLifecycleListener { public function __construct(private readonly LoggerInterface $logger) {} #[AsRabbitListener(event: 'order.created')] public function onCreated(array $payload): void { $this->logger->info('Order created', $payload); } #[AsRabbitListener(event: 'order.updated')] public function onUpdated(array $payload): void { $this->logger->info('Order updated', $payload); } #[AsRabbitListener(event: 'order.cancelled')] public function onCancelled(array $payload): void { $this->logger->warning('Order cancelled', $payload); } }
Wildcard Listeners
RabbitEvents uses RabbitMQ topic exchanges, supporting wildcard matching in routing keys:
*(asterisk) matches exactly one word:order.*matchesorder.created,order.cancelled.#(hash) matches zero or more words:order.#matchesorder.created,order.item.added.
#[AsRabbitListener(event: 'order.*')] class AllOrderEventsListener { public function handle(array $payload): void { // Receives all order.* events } }
Failure Handling (failed() callback)
If an exception occurs during processing and retry attempts are exhausted, the worker calls the failed(\Throwable $e) method on your listener:
#[AsRabbitListener(event: 'payment.processed')] class PaymentListener { public function handle(array $payload): void { // May throw an exception if payment gateway is down } public function failed(\Throwable $exception): void { // Cleanup, send alerts to Slack / Sentry, etc. } }
Console Commands
rabbitevents:listen
Start a background worker to consume events from RabbitMQ:
# Listen for specific events: bin/console rabbitevents:listen order.created order.updated --service=my-service # Listen with wildcards: bin/console rabbitevents:listen "order.*" --service=my-service
Available Options
| Option | Description | Default |
|---|---|---|
--service, -s |
Service name used to isolate queue names per service | app |
--memory |
Memory limit in MB before worker gracefully restarts | 128 |
--timeout |
Max processing timeout in seconds per message | 60 |
--tries |
Max retry attempts for failed messages | 0 (no retries) |
--sleep |
Sleep delay in seconds between retries | 1 |
rabbitevents:list
List all discovered RabbitEvents listeners and their routing keys:
bin/console rabbitevents:list
+-----------------+-------------------------------+
| Event | Listeners |
+-----------------+-------------------------------+
| order.created | App\Listener\OrderCreated... |
| order.cancelled | App\Listener\OrderCreated... |
| user.* | App\Listener\UserWildcard... |
+-----------------+-------------------------------+
Configuration
Full options reference for config/packages/rabbitevents.yaml:
rabbitevents: connection: host: '%env(RABBITMQ_HOST)%' # Default: 127.0.0.1 port: '%env(int:RABBITMQ_PORT)%' # Default: 5672 user: '%env(RABBITMQ_USER)%' # Default: guest pass: '%env(RABBITMQ_PASSWORD)%' # Default: guest vhost: '/' # Default: / exchange: 'events' # Topic exchange name (default: events) read_timeout: 3.0 # Socket read timeout (seconds) write_timeout: 3.0 # Socket write timeout (seconds) connection_timeout: 3.0 # Connection timeout (seconds) heartbeat: 0 # Heartbeat (seconds, 0 = disabled) lazy: true # Defer connection until first publish/consume qos: prefetch_count: 1 # AMQP prefetch count prefetch_size: 0 global: false ssl: enabled: false verify_peer: true cafile: null local_cert: null local_key: null passphrase: '' # Default serializer class (implements Serializer interface) default_serializer: RabbitEvents\Bundle\Message\Serializer\Json\Serializer logging: enabled: false level: 'info'
Serialization
JSON (Default)
The default serializer encodes arrays and objects as JSON and decodes them to associative arrays in your listener's handle(array $payload) method.
Protobuf Support
If google/protobuf is installed in your project, the Protobuf serializer is automatically registered:
composer require google/protobuf
Custom Serializers
To register a custom serializer, implement RabbitEvents\Bundle\Contract\Serializer and tag it:
# config/services.yaml services: App\Serializer\XmlSerializer: tags: - { name: 'rabbitevents.serializer' }
Testing
Use the PublishableEventTesting trait in PHPUnit tests to fake event publication without connecting to RabbitMQ:
namespace App\Tests; use App\Event\OrderCreatedEvent; use App\Service\OrderService; use PHPUnit\Framework\TestCase; use RabbitEvents\Bundle\Publisher\PublishableEventTesting; class OrderServiceTest extends TestCase { use PublishableEventTesting; protected function setUp(): void { parent::setUp(); self::fake(); // Replaces the real Publisher with a testing spy } public function testOrderCreatesEvent(): void { $service = new OrderService(); $service->createOrder(orderId: 100); OrderCreatedEvent::assertPublished('order.created', [ 'order_id' => 100, ]); } public function testEventNotPublishedOnFailure(): void { $service = new OrderService(); // ... action that should not publish ... OrderCreatedEvent::assertNotPublished(); } }
Examples
Check the examples/ directory for complete, ready-to-use examples:
OrderCreatedEvent.phpโ Standard publishable event.UserRegisteredEvent.phpโ Event usingShouldPublishand DI injection.OrderCreatedListener.phpโ Class-level#[AsRabbitListener].OrderLifecycleListener.phpโ Multi-method#[AsRabbitListener].UserWildcardListener.phpโ Wildcarduser.*pattern matching.PaymentProcessedListener.phpโ Custom error and failure handling (failed()).
License
This package is open-sourced software licensed under the MIT license.