mgdigital/busque
Busque is a lightweight command bus for Laravel that maps commands to dedicated handlers, keeping controllers and services thin. It supports middleware pipelines and queued command execution, making it easy to organize complex application logic with a clean, testable CQRS-style approach.
Requirements first, not code. Busque is PHP 7–era, built on ext-pcntl/ext-posix and a Redis store accessed through Predis. It will not run on Windows or PHP 8, and the repo is archived — treat it as a stable black box, not something you can patch.
composer require mgdigital/busque predis/predis
Where to look first (in this order):
README.md — the enqueue + worker example is the whole mental model.src/Busque.php — the facade; every entry point (enqueue, startWorker, startSupervisor, failed-job helpers, scheduler) lives here. Read the method signatures, not the docs, since the README lags the code.src/Job/JobInterface.php — the contract you implement.src/System/Worker.php and src/System/Supervisor.php — how forking, respawning, and failure are actually handled.src/Configuration/QueueConfiguration.php — the knobs you'll tune under load.First use case — a job class plus an enqueue call:
use Busque\Job\JobInterface;
class SendInvoiceEmail implements JobInterface
{
public function perform(array $args)
{
// Throwing = failure. Returning silently = success.
\App\Invoice::findOrFail($args['invoiceId'])->send();
}
}
$redis = new \Predis\Client(['host' => '127.0.0.1', 'port' => 6379]);
$busque = new \Busque\Busque($redis);
$busque->enqueue('emails', new SendInvoiceEmail(), ['invoiceId' => 42]);
First worker. Do not run this from a web request. Put it behind a console command or a shell script, and start with a single process in the foreground so you can see output:
php bin/busque-worker.php # calls $busque->startWorker('emails', 1)
Confirm the job runs, then move to startSupervisor() under systemd/supervisord for multi-process production.
Laravel wiring (there is none built in). Bind Busque as a container singleton around a shared Predis client, and expose console commands:
$this->app->singleton(\Busque\Busque::class, function () {
return new \Busque\Busque(new \Predis\Client(config('database.redis.default')));
});
Then three artisan commands: busque:work {queue}, busque:supervise {queue}, busque:scheduler. Register them in the scheduler or process manager — never invoke them from HTTP.
One queue per work class. This is the single biggest lever. Long-running report jobs, quick email jobs, and webhook retries should never share a queue; a slow job blocks the whole pipeline. Then run a supervisor per queue with a concurrency tuned to that queue's memory profile.
Payload discipline. Jobs are PHP-serialize()d into Redis. Pass scalars and IDs, not Eloquent models or large arrays. A fat job payload is both a Redis memory cost and a slow unserialize on every worker pickup.
Delayed work. Busque supports scheduled jobs via a sorted set drained by Busque\Scheduler\Scheduler. You must run the scheduler process — enqueue-with-delay without a running scheduler means the job sits in Redis forever and looks like a "lost job." Run exactly one scheduler.
Failure handling as a first-class loop. Failed jobs land back in Redis with their exception trace. Build an artisan command over getFailedJobs() / requeueFailedJob($id) / deleteFailedJob($id) so you're not dropping into redis-cli at 2am. Retry the requeue from a dashboard, not by re-enqueuing manually (you lose the original args otherwise).
Deployment. Workers load your code at boot and keep it in memory. After every deploy, gracefully SIGTERM the supervisor, let in-flight jobs finish, then restart. If you skip this, old code keeps running against new DB schemas.
Monitoring. Queue depth is just Redis (LLEN/ZCARD against the queue's keys — inspect with redis-cli KEYS '*<queue>*' once to learn the exact naming). Push depth, failed count, and worker heartbeat into your metrics stack; Busque gives you no UI.
phpredis \RedisInstance (or any ClientInterface mismatch), you'll get type errors deep inside the worker. Match the client the package expects; don't swap drivers for "performance" without reading Busque.php first.perform() semantics are silent. An uncaught exception is a failure; a swallowed exception is a success and the job is gone. Never wrap perform() bodies in try/catch that returns normally unless you truly want the job dropped.QueueConfiguration to retire workers after N jobs, and reconnect to the DB inside perform() if you hold persistent connections — forked children inherit the parent's socket and can corrupt it.echo, or you'll be unable to trace which child failed.redis-cli MONITOR to watch pushes/pops, and inspect the failed set directly. Nine times out of ten the bug is a payload that unserializes differently than it was enqueued.QueueConfiguration for concurrency/retry tuning, subclassing Supervisor for custom respawn policy, and wrapping Busque in your own service to add tracing/job middleware are the practical ones — there's no event system to hook.How can I help you explore Laravel packages today?