tomatom/jobqueuebundle
A Symfony bundle for running background jobs through a simple queue. Dispatch jobs from your application, process them asynchronously with workers, and keep long-running tasks out of web requests. Useful for deferring emails, exports, and other time-consuming work.
The first thing to internalize: this is not a Laravel package. It is a Symfony 6+ bundle (TomAtomCZ/JobQueueBundle), a re-implementation of JMSJobQueueBundle with a browser UI. There is no service provider, no config/jobqueue.php, no php artisan commands, and no Laravel queue driver. If you composer require it into a Laravel app you will get a bundle class and Doctrine entities that Laravel never boots.
So "getting started" means deciding where it runs:
queue:work.job tables via Doctrine migrations; Laravel only reads/writes rows through a thin repository or, better, through the sidecar's API.If you are actually working inside Symfony (the intended audience), the sequence is: composer require, register the bundle in config/bundles.php, point it at a Doctrine connection, run the bundle's migrations, then hit the browser route. Check the README for the exact bundle class name, route prefix, and worker command — treat those as authoritative, not memory.
First use case: a long-running "generate report / import CSV" job attached to a domain record, where an operator needs to see per-record job history and retry failures from a web page. That is the bundle's sweet spot and the thing Horizon does not give you out of the box.
Sidecar dispatch. From Laravel, keep the hand-off dumb and idempotent:
Http::withToken(config('services.jobqueue.token'))
->post(config('services.jobqueue.url').'/api/jobs', [
'type' => 'report.export',
'payload' => $payload,
'related' => ['type' => 'invoice', 'id' => $invoice->id],
'idempotency_key' => "report:{$invoice->id}:{$month}",
]);
Return the sidecar's job id and store it on your Eloquent model (jobqueue_ref) so you can correlate back.
Mirror the entity-centric model in Eloquent. The bundle's key idea is a Job linked to a related entity (JobRelatedEntity in the JMS lineage). If you stay in Laravel, replicate that shape with a polymorphic relation rather than one flat jobs table:
// jobs: id, type, status, payload, jobable_type, jobable_id, started_at, finished_at, output
$invoice->jobs()->latest()->first()?->status;
This is what makes the UI useful — operators click a record and see its job timeline.
Supervisor, not artisan. The worker is a bin/console long-running process, typically something like bin/console <bundle>:run (confirm the name). Manage it with Supervisor/systemd, exactly as you'd manage queue:work — autorestart=true, stopwaitsecs, one process per queue.
Treat the browser UI as an ops dashboard. It has its own routing and, likely, no authentication of its own. Put it behind a reverse proxy with basic auth, or an IP allowlist / VPN, and never expose it to the internet. Do not try to bolt Laravel's auth middleware onto it — different framework, different session stack.
Bridging events. Have the sidecar emit a webhook or write to a shared job_events table that Laravel polls (or a Redis pub/sub channel it subscribes to) so your app can react to job completion without querying the Doctrine DB directly.
Concept mapping if you're porting from Laravel:
| Laravel | This bundle |
|---|---|
php artisan queue:work |
Bundle's console worker command |
app/Jobs/* |
Doctrine job entities / handlers registered as services |
failed_jobs table |
Job status + retry in the browser UI |
| Horizon dashboard | Browser interface (unauthenticated by default) |
Queue::push() |
Persisting a job entity / service call |
php artisan migrate create or alter the bundle's tables. Keep the bundle's Doctrine migrations in its own migration namespace and run them from the sidecar deploy.--max-jobs-style flag, or a restart every N jobs). Watch RSS in staging.type strings and keeping old handlers around.config/packages/*.yaml with env-var interpolation (%env(...)%). If you're used to config/*.php + env(), this is a mental shift — and cached config (cache:clear) will bite you when env vars change.storage/logs/laravel.log. The browser UI's job detail page is usually the fastest way to see the exception and retry.job_runs table first — you can usually get 90% of the value in an afternoon without a second framework, a second ORM, and a GPL license.How can I help you explore Laravel packages today?