nuwber/rabbitevents
RabbitEvents is a Laravel package for event-driven microservices over RabbitMQ. Define Laravel-style event classes, publish them from one service, and listen in others with configurable queues and routing.
Prerequisites before the first line of code
ext-amqp extension (the package wraps php-amqp, so a missing extension is the #1 install failure, and it typically surfaces as a confusing connection error rather than a clear "extension missing" message).docker-compose service with the management UI (-p 15672:15672) is worth the five minutes — you will spend more time than you expect staring at exchange bindings.Minimal steps
composer require nuwber/rabbitevents
php artisan vendor:publish --provider="Nuwber\RabbitEvents\ServiceProvider"
Then open config/rabbitevents.php. That file is the first place to look and the last place you should stop reading — it holds the AMQP connection block (host/port/user/password/vhost), the exchange declaration, the event→exchange/routing-key mapping, the middleware stack, and the logging configuration. Everything else in the package is a thin layer over those six things.
First use case: publish one event, consume it in a second process
Step 1 — emit from the producer (idiomatic Laravel, nothing RabbitMQ-specific in your controllers):
event(new OrderPlaced($order->id, $order->total));
Declare the route on the event class with attributes, or in the config map — check src/Attributes in the installed vendor directory for the exact attribute names in your pinned version rather than trusting a blog post:
#[Exchange('orders')]
#[RoutingKey('order.placed')]
class OrderPlaced
{
public function __construct(public int $orderId, public int $total) {}
}
Step 2 — consume in the second service:
php artisan rabbitevents:list # confirm the package sees your event and where it routes
php artisan rabbitevents:listen --event="App\Events\OrderPlaced"
Verification loop for day one
php artisan tinker.Budget a day for a working publisher/consumer pair, and a week or two more before it is production-shaped (retry policy, DLQ, idempotency, observability). The library wiring is the easy part; the topology is the work.
Producer patterns
SerializesModels will happily put a model class name plus an ID into the payload, which is useless to a consumer written in another service or another language. Convert to a DTO/array at the boundary:event(new OrderPlaced($order->id, (int) $order->total_cents, $order->currency));
version field or version the routing key (order.placed.v2). Producers and consumers deploy independently; without a version marker, a payload change is an undetectable breaking change.Consumer patterns
match on event type inside a single handler — that is how you get a queue carrying fast and slow work with head-of-line blocking.order.*).{service}.{entity}.{action} with topic exchanges and wildcard bindings. Write this down in the repo, because it is the thing that drifts.Middleware pipeline
Middleware is where day-to-day behaviour actually lives. Register it in config and think about ordering:
'middleware' => [
\App\RabbitEvents\CorrelationIdMiddleware::class, // attach trace id first
\App\RabbitEvents\DeduplicateMiddleware::class, // drop replays/idempotency hits
\App\RabbitEvents\LoggingMiddleware::class,
\Nuwber\RabbitEvents\Middleware\RetryMiddleware::class, // last: retries re-run the stack
],
Each middleware takes the message and a $next closure — the same shape as Laravel HTTP middleware, so onboarding cost is near zero. Use it for:
Local development and testing
rabbitevents:listen --event=... with a single event keeps the feedback loop short while you iterate.Deployment shape
queue:work, you should not assume it picks up new code.rabbitevents:listen workers out of the same container/pod as web traffic; they have completely different restart and scaling semantics.The silent-drop trap. A message published to an exchange with no matching binding is discarded with no error. When a consumer "sees nothing", check the RabbitMQ UI bindings first — nine times out of ten it's a routing-key typo or a queue that was never declared. Producers get no feedback for this.
Handler exceptions default to something bad. If you have not configured retries and a dead-letter target, an exception in a handler very plausibly means requeue-forever. Configure a max-attempt policy and a DLQ before the first production message, not after the first incident. This is the single highest-value hour you will spend.
Log-writer changes are consumer-path changes. The v9.1.3 fix (read handler payload via payload()) is a one-line correctness patch in the General log writer — previously logged payloads were wrong. Two practical consequences: (a) if you have overridden the log writer, use the payload() accessor rather than reading the raw handler payload, or you will reintroduce the bug in your fork; (b) logging runs inside the consume loop, so anything that throws there is a delivery incident, not a cosmetic bug. Keep general logging conservative in production for that reason.
Pin to ~9.1.3, not ^9. A patch series that is still shipping internals fixes indicates the 9.x surface is settling. A tilde constraint plus a staging consumer soak is proportionate verification for patch-level upgrades; there's no data migration or dual-write window involved.
Publish failures are your problem, not the package's. If the broker is unavailable when the domain event fires, you get either an exception in the user's request path or a lost event — the package does not provide an outbox. Decide explicitly: fail the request, or write to a transactional outbox/laravel-queue fallback and republish. Silent loss is the default you get by not deciding.
Prefetch and long handlers. Scale is governed by prefetch count × handler latency, not by the package. A high prefetch with slow handlers means one worker holds a pile of unacked messages while another sits idle. Tune prefetch down for slow handlers, and split queues by latency class instead of blindly adding workers.
Don't mix paradigms casually. Adding RabbitMQ eventing alongside an existing Redis/SQS queue:work setup means two messaging mental models, two sets of failure modes, and two things to teach on-call. Adopt it for cross-service integration events only, and keep request-scoped background jobs on the native queue.
Topology drift is the real maintenance cost. Exchange/queue/binding declarations that live half in config and half in the RabbitMQ UI will diverge. Keep the event↔routing-key map version-controlled and reviewed like code, and treat broker-side manual changes as suspect.
Extension points worth knowing. Custom middleware (filtering, retries, dedupe), custom log writers, and the message/payload factory are the intended seams. Prefer them over forking; the project is effectively single-maintainer, so any fork is a long-term commitment — keeping your customisation in middleware and config means you can take upstream patches for free.
Debugging checklist. rabbitevents:list for introspection → rabbitevents:listen --help for the exact filter flags in your version → RabbitMQ management UI for bindings and ready/unacked counts → application logs for handler exceptions. If messages are "stuck", the unacked count tells you whether it's a consumer that died mid-handler or a handler that is simply slow.
How can I help you explore Laravel packages today?