markup/job-queue-bundle
A Symfony bundle for background job processing and queues. Dispatch, process, and manage asynchronous tasks with pluggable queue backends, keeping time-consuming work out of the request cycle so your application stays fast and responsive.
Know what you're adopting first. Despite the composer require-and-go feel, this is a Symfony bundle, not a Laravel package: it registers services in a kernel, ships Symfony Console commands, and depends on videlalvaro/RabbitMqBundle (now php-amqplib/rabbitmq-bundle) for the AMQP connection. Laravel's Artisan is built on Symfony Console, so the commands are conceptually familiar, but you cannot composer require this into a Laravel app and expect php artisan to pick it up. Realistic adoption paths:
Process::run('php artisan ...').Queue + Schedule (usually the better call for a Laravel shop with 0 dependents on this bundle).Minimal setup (path 1):
composer require markup/job-queue-bundle php-amqplib/rabbitmq-bundle
config/bundles.php (or AppKernel).Where to look first: Resources/config/ for the service definitions (that tells you what's swappable), the scheduler command class (that's where cron → delayed-message logic lives), and the README's config sample. Run bin/console list and grep for the bundle's namespace — the command names are the real API surface.
First use case: take one crontab line — say a nightly cleanup command — express it as a job definition, start the consumer, and watch it fire once with RabbitMQ's management UI open. That single loop teaches you the whole model (delay → deliver → execute → re-schedule).
Declarative job registry, one entry per task. The bundle's core value is moving crontab entries into version-controlled config: each job = console command + cron expression + queue/routing. Do this for all recurring work so the schedule is reviewable in a PR rather than on a server someone SSH'd into.
Producer/consumer split. The scheduler publishes a delayed message; the consumer receives it, executes the command, then republishes the next occurrence. Practical consequences:
Deployment workflow (the part people actually stay for). The "commands to help with deployment" are meant to run before app code rolls out: ensure exchanges/queues exist and bindings are correct, so a fresh environment or a renamed queue doesn't silently drop messages. Put them in your CI/CD step after migrations, before the new container starts consuming.
Bridging to Laravel. If you keep Laravel as the system of record, make the job's command a thin Symfony command that shells out:
// conceptually: run an Artisan command, inherit exit code, stream output
$process = new Process(['php', base_path('artisan'), 'reports:rebuild', '--quiet']);
$process->setTimeout(3600);
$process->run();
Keep it to recurring, already-idempotent Artisan commands. Anything needing the full Laravel container (Eloquent models, config, facades) is better dispatched as a Laravel job over the same RabbitMQ exchange, with Laravel's own worker consuming it — RabbitMQ doesn't care who the producer was.
Observability. Log job start/end/duration with the job name as a structured field, and alert on queue depth and unacked count rather than on "command failed" alone. A scheduler that stops publishing looks identical to "nothing to do" otherwise.
Delayed delivery has a prerequisite. RabbitMQ has no native per-message delay. Depending on the version, the bundle relies on either the rabbitmq_delayed_message_exchange plugin or TTL + dead-letter exchanges. If you don't enable the plugin, your "delayed" messages either fail to declare or get delivered immediately — a classic silent misconfiguration. Check the plugin is enabled (rabbitmq-plugins list) and that the exchange type matches what the bundle declares.
At-least-once, always. A consumer crash after execution but before ack means the command runs twice. Every scheduled command must be idempotent or guarded. The bundle will not deduplicate for you.
Overlapping runs. Nothing stops tick N+1 from starting while tick N is still running. Put a lock around the work (Symfony Lock, a DB advisory lock, or a Redis SETNX) rather than assuming the queue serializes it.
Timezone and DST. Cron expressions are evaluated in the process timezone. Pin the timezone explicitly in config/php.ini and be aware that a job scheduled at 02:30 will behave oddly twice a year.
Long-running workers leak. Both processes are daemons; restart them on every deploy (opcache/class changes are invisible otherwise), cap memory_limit, and use --env=prod --no-debug semantics. This is the same discipline as Laravel Horizon's maxTime/maxJobs.
Dependency rot is real. Last release was 2022, 17 stars, 0 dependents. The videlalvaro/RabbitMqBundle it was written against is renamed/superseded — you will likely need php-amqplib/rabbitmq-bundle and may hit config-key or service-id drift. Pin exact versions, keep the bundle behind a small internal adapter, and be ready to fork. Budget for that maintenance cost in the decision.
Debugging checklist:
bin/console debug:container | grep -i job to confirm services and aliases registered.rabbitmqctl list_queues name messages_ready messages_unacknowledged — is the message in the queue, or stuck delayed?-vvv before blaming the schedule math.Extension points worth knowing: the job registry/service that maps job names to commands and the publisher/consumer services can usually be decorated (Symfony decorates:) — that's your hook for logging, metrics, per-job retry policies, or swapping in a Laravel dispatcher without forking.
How can I help you explore Laravel packages today?