Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Job Queue Bundle Laravel Package

levuro/job-queue-bundle

levuro/job-queue-bundle provides a lightweight, database-backed job queue for Symfony apps. Dispatch background jobs, process them with worker commands, and manage retries, failures, and delayed execution without adding external queue infrastructure.

View on GitHub
Deep Wiki
Context7

Getting Started

First, verify you should be here. This is a Symfony bundle (JMSJobQueueBundle, redistributed by levuro) built on Doctrine ORM and the Symfony console. There is no ServiceProvider, no facade, no publishable config/jms_job_queue.php, and no dispatch() helper. In a Laravel app you are committing to: doctrine/orm (e.g. via laravel-doctrine/orm), a second entity manager and its migrations, a Symfony console kernel or a bespoke bridge to run the worker, and hand-wired DI for the bundle's services. If your jobs are ordinary Laravel work, Horizon + Bus::batch() covers the same ground for a fraction of the glue. Adopt this only when you specifically want jobs as durable, queryable Doctrine rows dispatchable inside the same transaction as business data, and an operator UI to inspect them.

Minimal setup

  1. composer require levuro/job-queue-bundle:1.10.9 — pin explicitly. 1.10.9 is a maintenance patch that swaps symfony/debug for symfony/error-handler; it is safe on Symfony ≥ 4.4 and hard-breaks EOL Symfony < 4.4.
  2. Add the bundle to the Symfony kernel (if you have one) or register its Doctrine mappings manually with a dedicated entity-manager connection. Point it at the same database as your Laravel app if you want transactional enqueue (the whole point).
  3. Create the schema: jms_job_queue_job and the related-job / cron tables are generated from the bundle's entity mappings. If you don't have a Symfony kernel, generate the SQL once offline and commit it as a Laravel migration.
  4. Start the worker: bin/console jms-job-queue:run (or a Symfony Application you bootstrap yourself). Nothing runs until this process is alive.

Where to look first, in order

  • JMS\JobQueueBundle\Entity\Job — the state machine, retries, exit code, output, and getRelatedEntities() all live here. Read it before anything else.
  • The jms-job-queue:run command — it is the entire runtime. Understand its concurrency and polling options.
  • The recurring_commands config block — this is the cron scheduler; you do not get cron for free from anything else.

First use case to prove out

Enqueue one Symfony console command, run the worker, read the state back:

use JMS\JobQueueBundle\Entity\Job;

$job = new Job('app:sync-orders', ['--since' => '2024-01-01']);
$em->persist($job);
$em->flush();          // enqueue

// later
$job->getState();      // new / running / finished / failed / canceled
$job->getOutput();     // captured console output
$job->getExitCode();

The mental model shift: you write console commands, not Job classes. The unit of work is an argv array, not a handle() method. That is the single biggest adjustment coming from Laravel.

Implementation Patterns

Transactional enqueue (the reason to choose this). Jobs are rows in the same database as your domain data, so you can enqueue atomically with the write that caused the work. From a Laravel codebase, wrap both sides:

DB::transaction(function () use ($em, $order) {
    $order->save();                               // Eloquent
    $job = new Job('app:send-receipt', [(string) $order->id]);
    $em->persist($job);
    $em->flush();                                 // Doctrine, same connection
});

This is the property you cannot get from Redis without an outbox table. Guard against enqueuing before commit if Eloquent and Doctrine ever end up on different connections — then the ordering breaks and you must fall back to DB::afterCommit().

Pass IDs, not payloads. The job row stores argv, which is indexed and duplicated in the admin UI. Serialize ['--order-id' => 42] and reload the model inside the command. Large JSON blobs in argv will bloat the table and slow the poller.

Idempotency + dedupe. JobManager::addJob() accepts a $duplicate flag; the repository can tell you whether a matching non-finished job already exists. Use it for "one sync per account at a time" flows rather than hand-rolling a lock table.

Retry policy per job, not per worker. Set it on the instance: setMaxRetries(), setMaxRuntime(), and retry-aware backoff where exposed by your bundle version. A poison job should exhaust retries and land in failed, not spin forever — assert that in a test.

Recurring work via config, not cron. Define scheduled commands in the bundle's recurring_commands config. These become tracked job rows with history, which is the actual value: "did last night's report run, and why did it fail?" For a Laravel-only team, expose a thin Artisan wrapper so operators don't need bin/console:

// app/Console/Commands/RunJobQueue.php
public function handle() {
    return \Symfony\Component\Console\Application::class // boot a minimal kernel
        ?: $this->call(...); // whatever bridge you build
}

Honestly: writing the bridge is the work. Budget for it.

Supervise the worker like a daemon. One process, --max-concurrent-jobs tuned to your DB connection pool, restart on exit, log to stdout. Under Laravel you'd use Supervisor with a command= pointing at your Symfony console entrypoint, not queue:work. Run a second process only if you've sized the connection pool for it.

Observability without the UI. The bundle ships an admin web UI (a Symfony route), which is inconvenient in a Laravel app. The pragmatic alternative is two Eloquent-ish queries against the job table: failure count in the last hour, and age of the oldest new job. Those two numbers catch almost every real incident.

Hook job state changes with a Symfony event subscriber on the bundle's state-change event to push metrics or notifications — this is the cleanest extension point and avoids polling.

Gotchas and Tips

  • It is not a queue. No dispatch(), no queue:work, no retry command you'd recognise. Everything is "insert a row" and "a long-lived process polls the table." Latency is poll-interval-bound, not sub-second.
  • Throughput is your database's throughput. Enqueue inside business transactions means lock contention between writers and the poller. Keep transactions short; at sustained churn beyond a few hundred jobs/second, use a broker.
  • The worker dying is silent. There is no heartbeat and no broker to hold the work. Add a queue-depth alert and an oldest-pending-job alert; without them, a dead worker looks exactly like a quiet night.
  • Stuck jobs after a crash. A job left running when the process is killed needs manual intervention or a reaper. Know where that lever is before you need it at 3am.
  • Cron rows accumulate forever. Schedule the bundle's cleanup command. Also watch index health on the job table once it's in the millions.
  • Argv is strings only. Booleans, ints, nulls, and nested arrays do not round-trip cleanly through a Symfony command invocation. Cast at enqueue time and validate at the top of the command.
  • Command names must resolve at runtime. The worker looks up your command in the console application; a typo in the command name fails at run time inside the worker, not at enqueue time. Add a smoke test that asserts every string you enqueue is a registered command.
  • 1.10.9 specifically. Dropping symfony/debug will surface as a runtime class-not-found, not an install error, if any of your code (or a bootstrap in your console entrypoint) still references Symfony\Component\Debug\*. Grep for it before upgrading. The new symfony/error-handler requirement is a no-op on Symfony ≥ 4.4 and unsatisfiable below it — if a service is stuck on Symfony 3.4/4.0–4.3, pin 1.10.8 and schedule the framework upgrade separately. Also note the maintainer ships requirement changes as patches: pin your version, don't float.
  • Test the error path, not just the happy path. Deliberately throw inside a job and confirm it lands failed with the right retry count and captured output. That single test covers the release-specific risk and most of the operational risk.
  • Extension points worth knowing: the job state-change event (metrics/notifications), JobManager (custom enqueue semantics), the recurring-commands config (scheduling), and getRelatedEntities() (link a job to your Eloquent records via the related-job table for support triage).
  • Maintenance posture. Feature-frozen, single-maintainer, patch-only releases. That's acceptable for free job auditability — just don't plan roadmap capability on top of it, and keep the bridge layer thin enough that swapping in Horizon + a small audit table later is a week, not a quarter.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings