psyllium/amqp
psyllium/amqp brings AMQP/RabbitMQ messaging to Laravel and PHP applications, offering a simple API for publishing and consuming messages, declaring exchanges and queues, and integrating asynchronous workflows into your app.
Confirm the hard requirements before anything else. This package is not a pure-PHP AMQP client — it wraps psyllium/psyllium, which sits over the native ext-amqp extension. If the extension is missing, nothing installs or runs.
php -m | grep -i amqp # must print "amqp"
pecl install amqp # requires librabbitmq-dev / libssl headers on the host
composer require psyllium/amqp
Watch the install path. The repository is Bitbucket-hosted (bitbucket.org:ebrandpl/amqp). If it is not on Packagist, you must add it as a VCS repository in composer.json and make sure CI and every deploy target can authenticate to Bitbucket — otherwise your builds break the moment a new engineer or pipeline clones the project:
"repositories": [
{ "type": "vcs", "url": "[email protected]:ebrandpl/amqp.git" }
]
Understand what you actually installed. Despite the Laravel-flavored description, the shipped integration is a Symfony bundle (Psyllium\Amqp\Symfony\PsylliumAmqpBundle). It gives you a container-managed Psyllium\Connection/Psyllium\Producer; it does not register a Laravel Queue driver, an Event transport, or a Job payload format. Plan for that gap up front.
Registration (Symfony app inside your Laravel-adjacent stack):
// config/bundles.php
return [
Psyllium\Amqp\Symfony\PsylliumAmqpBundle::class => ['all' => true],
];
Minimal config — the config tree is connection-centric:
# config/packages/psyllium_amqp.yaml
psyllium_amqp:
connections:
default:
host: '%env(AMQP_HOST)%'
port: 5672
login: '%env(AMQP_USER)%'
password: '%env(AMQP_PASS)%'
vhost: '/'
First use case — publish a single message to an exchange:
use Psyllium\Producer;
final class WelcomeMailer
{
public function __construct(private Producer $producer) {}
public function queue(string $email): void
{
$this->producer->publish(
json_encode(['email' => $email]),
'welcome', // routing key
'notifications' // exchange
);
}
}
Where to look first in the repo, in this order:
src/DependencyInjection/Configuration.php — the only trustworthy source of which YAML keys exist and what they default to. Do not trust README snippets.src/Resources/config/services.xml — the service IDs, and whether services are public (injectable) or private (needs aliasing).src/DependencyInjection/*Extension.php — how connections and producers get built from config.psyllium/psyllium README — this bundle is largely a service-definition shim; the real Producer/Consumer API lives upstream.One connection per broker, many producers/consumers per connection. The extension builds one Psyllium\Connection per entry under connections, and your publisher/consumer services receive that connection as a constructor argument. Model your app the same way: register a single default connection and inject producers, rather than opening connections ad hoc inside request handlers.
Publisher wrapper — never call Psyllium\Producer from controllers/jobs directly. The whole risk profile of this package is "single maintainer, unknown upgrade path." Isolate it behind your own contract so you can swap to vladimir-yuldashev/laravel-queue-rabbitmq or php-amqplib later without touching call sites:
interface MessagePublisher
{
public function publish(string $routingKey, array $payload): void;
}
final class AmqpPublisher implements MessagePublisher
{
public function __construct(private \Psyllium\Producer $producer) {}
public function publish(string $routingKey, array $payload): void
{
$this->producer->publish(json_encode($payload), $routingKey, 'events');
}
}
Bind the interface in your container and inject the interface everywhere else. This is the single most important pattern for this package.
Configure topology in config, not in PHP. Exchanges, queues, and bindings should be declared in YAML/config so they match between producers and consumers and can be reasoned about in one place. Keep the exchange name and routing-key conventions centralized (enum class or config constant); ad-hoc string literals are how routing-key mismatches get shipped.
Consumer/worker pattern. Consuming is the harder half and the least visible in the repo. Run consumers as a dedicated long-lived process (a console command or a php artisan-style worker), not inside web requests. Validate the following against a real broker before trusting them in production, because the bundle does not make them explicit:
basic_qos settings and where they come fromSIGTERMIntegration tip — environment separation. Because config is YAML-first, drive every value from env vars and keep per-environment vhosts/exchange names distinct. That lets you point the same code at a local RabbitMQ container and a staged cluster without code changes.
Local dev workflow. Put a RabbitMQ container with the management plugin in docker-compose.yml. Use the management UI to inspect exchanges, queues, bindings, and message rates — this is your primary debugging surface, because the package gives you almost no observability.
Pilot the riskiest integration point first. Do a thin publish test, then a consume test, before wiring it into any real workflow. Confirm the serialization format matches what your consumers expect — this package does not standardize payload shape, so it is on you.
ext-amqp is non-negotiable. No pure-PHP fallback. This alone disqualifies it on shared hosting, many serverless runtimes, and Alpine images without the build toolchain. Bake the extension into your Docker base image.composer install in CI fails. Mirror the package into your own vendor namespace (a fork) and pin a reviewed commit. Assume you are the maintainer.dispatch() on jobs, Queue::push(), failed_jobs integration, Horizon dashboards, or Laravel's job payload format. Any attempt to bridge it into Laravel's queue contracts is bespoke glue you write and own.login/password (not user/password), vhost with a leading /. Copy keys from Configuration.php, not from blog snippets; a typo'd key is silently ignored by Symfony's config tree rather than erroring.services.xml, injection by class name fails unless the bundle declares aliases. When you hit "service not found," check the XML before assuming the extension is broken.publish() takes a string/body; whether that is JSON, MessagePack, or a Symfony-serialized envelope is your decision. Pick one format, document it, and write a contract test — this is where a future transport swap most easily breaks consumers.rabbitmqctl list_queues name messages consumers for stuck queues; (3) broker logs for connection churn; (4) only then the package code. If messages vanish, check the exchange type and that a binding actually exists for your routing key — an exchange with no matching binding silently drops.psyllium/psyllium API underneath it. If you need retry, DLQ, metrics, or correlation-ID propagation, you will either add it in your own wrapper or fork the package. Budget for the fork.How can I help you explore Laravel packages today?