convenia/pigeon
Convenia Pigeon is a Laravel/PHP package for adding message-based communication to your application. It offers a simple, configurable API for publishing and consuming messages, helping teams build decoupled services and reliable workflows with minimal boilerplate.
Install and wire it up
composer require <vendor>/pigeon
php artisan vendor:publish --tag=pigeon-config
Set the AMQP connection details in .env (host, port, credentials, vhost, queue/exchange names) as documented in docs/INSTALLATION.md, then confirm config/pigeon.php reads them via env(). If your broker topology is provisioned by IaC/Terraform/Helm rather than by the application, also add:
PIGEON_SKIP_AUTO_DECLARATION=true
Leave it false (the default) on a fresh local install so Pigeon creates the queue/exchange for you on first use.
Where to look first
docs/INSTALLATION.md — env keys and the canonical .env example.config/pigeon.php — the single source of truth; every env var maps to a key here.docs/ADVANCED.md — routing/exchange semantics and the new "Skipping Auto Declaration" section.Driver class — the actual execution seam for dispatch(), queue(), exchange(), routing() and events().First use case
Send one message and consume it end to end before building anything abstractions-heavy:
Pigeon::queue('orders')->dispatch(new OrderPlaced($order));
Run your consumer worker in a second terminal, confirm the message round-trips, then delete the auto-created queue in the broker UI and re-run with PIGEON_SKIP_AUTO_DECLARATION=true to see the failure mode you'll get in production when a queue is missing. That five-minute experiment is the fastest way to understand what the new flag actually buys you.
Let the app own topology in dev, IaC own it in prod
The clean split is: PIGEON_SKIP_AUTO_DECLARATION=false locally and in CI integration environments (fast iteration, no provisioning step), true in staging/production where Terraform/Pulumi/Helm charts declare exchanges, queues and bindings. Keep the flag per-environment in your deployment config, never hardcoded in config/pigeon.php, so it stays overridable.
Declare-once, then publish at volume
Auto-declaration is a per-call broker round trip. On a hot path that fans out hundreds of dispatch() calls per request, that's real latency and connection churn. With the flag on, publishing is a single basic_publish against an assumed-existing exchange. This is the highest-leverage reason to enable it — not just IaC purity, but throughput.
Publish/subscribe with explicit topology
Pigeon::exchange('events', 'topic')
->routing('order.*')
->dispatch(new OrderShipped($order));
Use exchange() + routing() for fan-out, queue() for point-to-point work queues. When the flag is on, document the required exchange name, type (topic/direct/fanout), durability and the binding keys in your IaC repo and in a comment next to the call site — the code no longer self-describes its topology.
Consumer workers
Long-running workers are where the flag matters most: a worker that reconnects every N messages won't re-declare on reconnect, so topology drift between deploys becomes visible immediately rather than being papered over. Bind the flag into the same config map that controls worker concurrency and prefetch so a single deploy changes all broker assumptions together.
Testing
Unit tests in the package assert declaration is skipped when the flag is on. Mirror that in your app tests: set the config key in phpunit.xml (<env name="PIGEON_SKIP_AUTO_DECLARATION" value="true"/>) if you use an in-memory or pre-provisioned test broker, and write one test asserting a publish against a missing queue surfaces a broker exception rather than succeeding silently.
The failure mode changes from silent to loud. With the flag on, publishing or consuming against a non-existent queue/exchange raises a broker error (NOT_FOUND / 404) instead of being auto-created. That's the point, but it means a typo in a queue name that previously "worked" now takes down a worker. Grep your codebase for queue/exchange string literals before the first rollout with the flag on, and diff them against your IaC definitions.
Boolean validation is strict. The package validates the config value is an actual boolean and throws otherwise. env() returns strings, so the value must be coerced in config/pigeon.php (e.g. (bool) env('PIGEON_SKIP_AUTO_DECLARATION', false) — note (bool) 'false' is true in PHP, so prefer filter_var(env(...), FILTER_VALIDATE_BOOL) or rely on the package's own cast). A typo like PIGEON_SKIP_AUTO_DECLARATION=yes or =1 will surface as a clear exception at first use — which is good, but it fires at runtime, not at boot, so it can hide until the first dispatch.
php artisan config:cache is mandatory. Changing the env var on a cached-config deployment does nothing until you re-cache. This is the single most common "the flag isn't working" report.
Not every path is guarded. The guard covers queue(), exchange() and routing() (and by extension the dispatch()/events() call sites that use them). If you have custom code touching the underlying AMQP channel directly, it will still declare. Audit any bespoke integration before assuming the flag gives you full read-only topology behavior.
Rolling deploys are safe but briefly inconsistent. Old pods (flag off) will still declare while new pods (flag on) assume existence. Not a correctness problem — declaration is idempotent — but it does mean you won't see missing-topology errors until the old pods drain.
Debugging checklist when a queue "disappears": confirm the flag value actually in effect (config('pigeon.skip_auto_declaration') via tinker), confirm the queue exists in the broker (rabbitmqctl list_queues name or the management UI), confirm the vhost on the connection matches the vhost where IaC created it, then confirm bindings — a queue can exist but be unbound from the exchange, which looks identical from the publisher's side.
Extension point. Driver is the natural seam. If you need finer granularity than a global flag — say, skip declaration only for the high-volume event exchange but keep auto-creating internal work queues — subclass Driver, override the skipAutoDeclaration() guard to consult a per-exchange allowlist, and bind your subclass in a service provider. This keeps you off the package's release cadence for environment-specific policy.
Leave false in local dev. Enabling it everywhere forces every developer to re-provision their Docker broker after a branch switch, which quietly erodes the convenience the default was designed to preserve.
How can I help you explore Laravel packages today?