kfzteile24/queue-bundle
kfzteile24/queue-bundle is a Symfony bundle that simplifies message queue integration, providing configuration and services for dispatching and consuming asynchronous jobs reliably in PHP applications.
Read this first: the package's own description contradicts itself. The npm-style summary says it abstracts AWS' PHP SDK SQS/SNS clients for DI; the repo's own docs describe a pluggable adapter layer over AMQP/Redis/MongoDB. Either way it is a Symfony bundle, not a Laravel package — there is no QueueServiceProvider, no config/queue.php driver, and no illuminate/* dependency. Before you install anything, open the repo and confirm which of those two worlds you're actually in by reading:
composer.json — the require block tells you whether it pulls aws/aws-sdk-php, php-amqplib/php-amqplib, or both, and which Symfony version it targets.DependencyInjection/Configuration.php — this is the source of truth for every config key. The README is thin; do not trust the README alone.QueueBundle.php + any CompilerPass/ — this is where the service tags for producers/consumers live.Minimal install (Symfony target):
composer require kfzteile24/queue-bundle
// config/bundles.php
return [
// ...
Kfzteile24\QueueBundle\QueueBundle::class => ['all' => true],
];
# config/packages/queue.yaml — keys are illustrative; verify against Configuration.php
queue:
producer:
dsn: '%env(QUEUE_DSN)%'
consumer:
queues: ['orders.created']
For a Laravel developer, the realistic day-one paths are:
composer require this into a Laravel app.First real use case: move a slow side-effect (invoice PDF render, email send, webhook fan-out) off the HTTP request cycle. Define one plain message class with scalar/array properties, call $producer->publish($message) from a controller, and run the consumer once against a non-production broker to confirm the round-trip. That single loop validates: DI wiring, serialization, routing, and the consumer command — everything else is configuration on top.
Producer/consumer split is the unit of work. Keep messages small: an aggregate ID plus a version tag, not a serialized Eloquent/Doctrine entity. The consumer re-fetches the aggregate from the database on the other side. This is the single most important pattern — it keeps messages schema-stable across deploys so a rolling deploy doesn't poison in-flight messages.
// Producer side — from a controller or command
final class PlaceOrderHandler
{
public function __construct(private OrderProducer $producer) {}
public function __invoke(PlaceOrder $command): void
{
$order = $this->orders->create($command);
$this->producer->publish(new OrderCreated($order->id()->toString()));
}
}
// Consumer side — re-hydrate from DB, do NOT trust the message body
final class OrderCreatedConsumer
{
public function __construct(private Orders $orders) {}
public function consume(OrderCreated $msg): void
{
$order = $this->orders->get($msg->orderId); // fresh state
// ...
}
}
Autowire the producer, tag the consumer. In Symfony, producers are typically registered as regular services. Consumers are found at runtime by a tag — check the bundle's CompilerPass for the exact tag name (it varies: queue.consumer, kfzteile24.queue.consumer, etc.). If your consumer doesn't run, a missing tag is the first thing to check.
One queue per concern. orders.created, invoices.render, emails.transactional — separate queues. Do not multiplex. A slow PDF render on the same queue as an order acknowledgement is a head-of-line-blocking outage waiting to happen.
Idempotency by design. Queue transports are at-least-once. Every consumer must be safe to run twice: an INSERT … ON CONFLICT DO NOTHING on a processed_messages(message_id) table, or an idempotency key passed to downstream HTTP calls. This is not optional — the first retry you configure will exercise it.
Retry with backoff, then dead-letter. Retry counts and backoff belong at the broker/consumer configuration level, not inside your try/catch. Once retries exhaust, route to a DLQ that a human actually watches. A DLQ nobody reads is a silent-failure machine.
Testing. Two layers:
publish() was called with the expected message shape. No broker needed.test profile YAML and run one consumer against it. Worth the flakiness for anything touching retry/DLQ — mock transports don't exercise the failure paths you actually care about.Worker supervision. Consumers are long-lived processes. Run them under systemd or supervisord, one numprocs per concern, tuned to I/O concurrency rather than CPU count. A SIGTERM handler that finishes the current message before exiting is worth the five lines — otherwise deploy windows become poison-message windows.
When porting to Laravel. Mirror this shape with ShouldQueue jobs, Illuminate\Queue\Middleware\WithoutOverlapping for the processed-message guard, tries + backoff on the job for retries, and failed_jobs plus a custom failed listener that routes to a dead-letter table. Horizon covers the dashboard; you don't need the Symfony admin commands if you're already on Laravel.
The identity crisis is real. The description says SQS/SNS; the docs say AMQP/Redis/MongoDB. One of those is stale. Read composer.json before you build a mental model — if you assume SQS and it's an AMQP bundle, you have wasted an afternoon.
2 stars, 0 dependents, single-vendor maintainer. This is an extracted internal library from kfzteile24, published for their own convenience. There is no community, no security policy, no SLA. Treat every update as a reviewable diff. Pin the exact version in composer.json ("kfzteile24/queue-bundle": "1.7.*" — minor pin, not ^1.7) and read the changelog yourself.
The 1.7.0 release notes are literally -. Assume releases can be behavior-changing without documentation. Diff the source between versions before bumping.
No Laravel integration layer. Do not attempt to composer require this into a Laravel app and expect autoconfiguration — you'll drag in symfony/framework-bundle and get a container that doesn't autowire your consumers. Use Paths A or B from "Getting Started".
Config keys aren't documented. DependencyInjection/Configuration.php is your API reference. Read it before writing YAML. A typo in a config key silently produces an unconfigured adapter, not an error — Symfony config trees often do this in practice when a node isn't marked cannotBeEmpty().
Service tags must match exactly. If your consumer never fires, the tag name is almost always the culprit. Run bin/console debug:container --tag=<guessed-tag> and then try the variants. Also check whether the compiler pass auto-tags by interface — if your consumer class doesn't implement the expected interface, tag scanning won't find it.
Connection lifecycle is the throughput cliff. Long-lived consumers must reuse connections and channels. If the bundle creates a fresh connection per message (some do, especially simple abstractions), you'll saturate broker connection limits at scale. Verify by counting connections server-side under synthetic load before production.
Poison messages. With aggressive retry settings, a malformed message will cycle through retry → fail → requeue indefinitely, hammering logs and downstream dependencies. Configure a max-retry and a DLQ from day one — do not add it "later."
Silent drops. Misconfigured DLQ routing is the classic failure: the consumer catches an exception, "handles" it, and the message is ack'd and gone. Instrument queue depth, retry counts, and DLQ arrival rate as metrics — not logs. Three counters will tell you more than a stack trace.
Extension points. Because the bundle is a thin abstraction, the underlying producer/consumer classes are usually the extension point — not the bundle config. If you need custom serialization or a bespoke transport, decorating those classes via a Symfony service decorator is the clean path. Forking the bundle should be a last resort.
Version drift. Zero dependents means zero tested configurations besides the vendor's own app. Verify the Symfony major, PHP minor, and any aws/aws-sdk-php / php-amqplib version constraints against your platform baseline before commiting. A half-day compatibility spike is cheap insurance versus a two-week debugging detour.
Escape hatch. If the bundle goes stale, the fork cost is low (MIT, small surface) but the internal-ownership cost is not. Decide up front who owns the queue subsystem if you adopt this — that person will be the de facto maintainer.
How can I help you explore Laravel packages today?