kuborgh/queue-bundle
A Symfony bundle that adds queue-based background job processing to your application. It provides configuration, services, and console commands for publishing and consuming queued messages, helping you offload time-consuming tasks to workers with minimal setup.
Read this first: it is still not a Laravel package. kuborgh/queue-bundle is a Symfony bundle (registered via AppKernel/bundles.php, configured under a queue: key in config.yml). There is no service provider, no facade, no config/queue.php driver, and nothing that plugs into Laravel's Queue contract. It targets the PHP 5.x / Symfony 2–3 era and has zero dependents. You cannot composer require it into a Laravel app and expect dispatch() to work.
What v1.0.1 changes — and what it doesn't. The release is a patch, not a revival: a bugfix (#1, no detail in the notes), a Logentries logging integration, and getJobByCommand() for looking up the status of a time-critical job. None of that touches the PHP/Symfony version constraint, the service-provider gap, or the serialization format. The headline assessment stands. Two of the three items are still worth reading as patterns, covered below.
So "getting started" here means one of two things:
Evaluating it as a spike (rare, and only if you're maintaining a legacy Symfony app that already depends on it):
composer require kuborgh/queue-bundle
Register the bundle in your kernel, add the minimal config, then look at Command/ in the vendor directory first — that's where the producer command (enqueue a Symfony command) and the worker command (consume and execute) live. That pair is the entire mental model: serialize a console command + its arguments into a store, have a long-running worker pop and run() it. With v1.0.1, add one more stop: wherever getJobByCommand() is defined, because that tells you how jobs are keyed and whether status is derivable from the store at all.
Stealing the pattern for Laravel (the realistic path). The first use case it was built for — "this artisan/symfony command takes 90 seconds, don't block the request or the cron" — maps one-to-one onto:
// app/Jobs/RunReportCommand.php
class RunReportCommand implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function __construct(public array $args = []) {}
public function handle(): void
{
Artisan::call('reports:generate', $this->args);
}
}
RunReportCommand::dispatch(['--month' => '2026-01']);
with php artisan queue:work as your worker. That is the package's value proposition, implemented natively, maintained, and with retries, backoff, failed-job storage, and Horizon metrics included.
Where to look in the repo: Command/ (producer + worker), the queue storage abstraction (DB table vs. file — this determines whether you get concurrency safety at all), the job-status lookup added in v1.0.1, and composer.json for the PHP/Symfony constraints before you waste an afternoon on a dependency conflict.
Producer/consumer split. The bundle's model is: a web request (or cron) enqueues a command name plus argument array; a separate process runs a worker loop that deserializes and executes it. If you're porting this shape to Laravel, keep the split but replace the transport:
| Bundle concept | Laravel equivalent |
|---|---|
| Enqueue a Symfony command | dispatch(new RunConsoleCommand($name, $args)) |
| Worker loop process | php artisan queue:work --queue=commands |
| Queue storage table/file | QUEUE_CONNECTION=redis / database |
| Command arguments serialized | Job constructor properties (serialized via SerializesModels) |
| Failed execution | failed_jobs table + queue:failed / queue:retry |
getJobByCommand() status check (v1.0.1) |
Job status column / Bus::batch() / cache progress keys |
Logentries handler (v1.0.1) |
Monolog channel in config/logging.php |
Dedicated queue for CLI work. If you mix "run an artisan command" jobs with fast jobs, a slow command hogs workers. Use a named queue and a dedicated worker:
public function viaQueue(): string { return 'commands'; }
php artisan queue:work --queue=commands --timeout=600 --tries=1
Status tracking, the one genuinely new idea in v1.0.1. getJobByCommand() answers "did my time-critical job finish?" by looking the job up from the command identity. That's a useful shape if you have a request that dispatches work and then needs to show progress. In Laravel, don't invent a lookup table — use the built-ins:
// Dispatch and remember the batch id
$batch = Bus::batch([new RunReportCommand($args)])->onQueue('commands')->dispatch();
Cache::put("report:{$id}:batch", $batch->id, now()->addDay());
// Poll
$batch = Bus::findBatch(Cache::get("report:{$id}:batch"));
return ['progress' => $batch->progress(), 'done' => $batch->finished()];
For single jobs where you just need a percentage, have handle() write progress into the cache keyed by a job UUID passed into the constructor. Either way, key on a value you control (UUID / batch id), never on a command string — command names collide across concurrent runs, which is exactly the limitation getJobByCommand() inherits.
Long-running command semantics. Symfony commands often aren't idempotent (they may half-write a report before dying). Pattern: wrap the Artisan::call in a job with $tries = 1 and an explicit failure handler that cleans up partial output, rather than relying on blind retries. If retries are safe, set $backoff and retryUntil().
Scheduling. With the bundle you'd cron the worker. In Laravel, keep schedule:run for dispatch and queue:work (or Horizon) for execution — never put slow work directly in Kernel::schedule():
$schedule->job(new RunReportCommand(['--month' => now()->subMonth()->format('Y-m')]))
->monthlyOn(1, '02:00')
->onOneServer();
Logging integration. v1.0.1 wires a Logentries handler so worker output ships off-box. The Laravel analogue is a named channel plus a stack, and it's worth doing for exactly the same reason — a crashed worker's stdout is the only diagnostic you get, and it dies with the container:
// config/logging.php
'queue' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => ['stream' => storage_path('logs/queue.log')],
'tap' => [App\Logging\AddJobContext::class],
],
Integration tip. If you genuinely must bridge to a legacy Symfony command living in the same codebase, don't resurrect this bundle — shell out via Symfony\Component\Process\Process from inside a Laravel job, or expose the legacy command behind an HTTP endpoint the job calls. You keep Laravel's retry/monitoring and avoid a 2016 serializer on PHP 8.
symfony/serializer 5+/6+ support, and no security patches. Treat it as reference reading, not a dependency. Do not read a patch release as evidence of a support commitment.getJobByCommand() is a polling API, so it inherits every polling flaw. Status is only as fresh as the store, there's no "unknown vs. failed vs. never-enqueued" distinction documented, and command-name-keyed lookup breaks with concurrent runs of the same command and different arguments. If you copy the pattern, key on a UUID and model at least three states.queue:work — loads code once. After every deploy you must restart workers (php artisan queue:restart). Symptom: "my fix isn't live" on jobs but is live on web requests.queue:connections.redis.retry_after) or SQS gives you this for free — another reason to port.failed_jobs equivalent, so add logging at the top of your producer. The new status lookup helps confirm "did it run at all" but won't tell you why it failed. For Laravel: php artisan queue:failed, queue:retry all, and Horizon::routeMailNotificationsTo() / horizon:list when you need per-job timing.config/queue.php. Note that the new Logentries wiring adds another key under that root; if you're copying config across, don't carry driver names or the logger block. sync, database, redis, sqs are the only real choices you need.Queue driver via Queue::extend() — useful if you have an internal message broker. Almost everything else in the bundle is now redundant with Bus, Horizon, failed_jobs, and batch progress.ShouldQueue, Bus::batch(), and a dedicated commands queue. Do not add this to composer.json.How can I help you explore Laravel packages today?