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

Qless Php Laravel Package

pdffiller/qless-php

qless-php is a PHP client for Qless, a Redis-backed job queue. It lets Laravel/PHP apps enqueue, schedule, and process background jobs with priorities, recurring jobs, retries, and real-time monitoring.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bindings with Composer and point them at a Redis instance — Qless has no server daemon of its own; Redis is the queue.

composer require pdffiller/qless-php

There is no migration, no schema, no config file. The only prerequisite is a reachable Redis (Qless keys are namespaced under ql:), so you can be enqueuing in a single script:

<?php
require 'vendor/autoload.php';

$client = new Qless\Client('tcp://127.0.0.1:6379');
$queue  = $client->queues->get('default');

$jid = $queue->put('Acme\\Jobs\\SendEmail', ['user_id' => 42], ['priority' => 10]);

$job = $queue->pop();          // non-blocking reserve
$payload = $job->data;         // decoded JSON payload
// ...do work...
$job->complete();              // removes the job and records it

Where to look first, in order:

  1. Qless\Client — the connection and the entry point to queues, workers, jobs. Almost everything hangs off this object, so bind it once in your container.
  2. Qless\Queue — put, pop, recur, stats, length. This is 80% of producer-side code.
  3. Qless\Job — jid, data, complete, fail, heartbeat, cancel, retry. This is 80% of worker-side code.
  4. The Lua scripts shipped with the package (in the src/lua directory). They are the real contract with Redis and the best place to look when behaviour surprises you — everything Qless does atomically is defined there.

The natural first use case is a small out-of-band worker: an HTTP request pushes a job, a CLI process pops it, does the work, and calls complete() or fail(). Get that loop working end-to-end before adding scheduling, dependencies, or recurring jobs.

Implementation Patterns

Separate producer and consumer processes. Producers are usually inside your web app: a controller, a model observer, or a queued command injects Qless\Client, grabs a queue by name, and calls put(). Workers are long-lived CLI scripts — an Artisan command, a Symfony console command, or a plain PHP file — that loop. Never pop a job from inside a web request; the lock and heartbeat semantics assume a worker owns the job for a while.

Use a callable/class-name dispatch table, not serialized objects. The payload that goes into Redis is JSON, so pass scalars and arrays only, and let the worker rehydrate:

// producer
$queue->put('SendEmail', ['user_id' => 42, 'template' => 'welcome']);

// worker
$handlers = [
    'SendEmail' => function (Qless\Job $job) {
        $user = User::find($job->data['user_id']);
        Mailer::send($user, $job->data['template']);
    },
];

while (true) {
    $job = $queue->pop();
    if (!$job) { usleep(200000); continue; }

    try {
        $handlers[$job->data['_class'] ?? $job->klass]($job);
        $job->complete();
    } catch (Throwable $e) {
        $job->fail(get_class($e), $e->getMessage());
    }
}

The klass string you pass to put() is stored verbatim and is not validated, so a typo silently produces a job nobody can handle — pair the names with a constant or an enum.

Heartbeat long jobs. A popped job is locked for a TTL (default on the order of a minute). If your work takes longer and you never heartbeat, Qless assumes the worker died, returns the job to the queue, and another worker starts the same job. Any job that can exceed the lock TTL needs $job->heartbeat() on a timer — typically interleaved with the work itself, or in a pcntl_alarm/SIGALRM handler.

Scheduling and retries go through options on put(). delay (seconds) defers a job, and the same job-creation entry point accepts priority, tags, and depends for dependencies on other jids. For retry, prefer re-enqueueing from the failure path with an explicit backoff over relying on implicit re-pop, and keep an attempt counter in the payload — Qless tracks failures per job, but once you re-put you get a fresh jid and a fresh history.

Recurring jobs use recur() on the queue, not an external cron. Register them once (it is idempotent per class+args+interval in Qless's Lua) and let Qless materialise each occurrence. This is the cleanest win over a hand-rolled cron + table approach: it is server-clock based, so worker/host skew does not matter.

Bind the client once. In a Laravel service provider, register Qless\Client as a singleton with the DSN from config and inject Qless\Queue instances via a small factory. The client is stateless enough to share, and creating a new one per job wastes connections. For test isolation point the DSN at a dedicated Redis database (.../15) and flush the ql:* keys in setUp.

Scale out with processes, not threads. PHP has no threads, so run N copies of the worker script under Supervisor/systemd, each with a distinct worker name (Qless tracks workers for visibility and for unfail/track tooling). Inside the worker, cap memory by exiting after N jobs (or pcntl_fork per job) — long-running PHP daemons leak.

Gotchas and Tips

  • Duplicate execution is the default failure mode, not an edge case. Lock expiry, worker OOM, deploy restarts, and kill -9 all cause re-delivery. Assume every job runs at least once and make handlers idempotent (unique keys, INSERT ... ON CONFLICT, conditional updates).
  • Never let an exception escape the loop without complete() or fail(). An uncaught error leaves the job locked until the TTL expires, then it re-pops and fails again — you get a slow-motion infinite retry with no error surfaced anywhere. Always wrap in try/catch/finally.
  • The payload is JSON. Associative arrays survive; PHP objects, resources, and DateTime objects do not. Format dates to strings and store IDs rather than models. Very large payloads bloat Redis memory and every pop copies them — keep them small.
  • Accessor style varies. Depending on the version, job fields are public properties ($job->jid, $job->data) or getters ($job->getJid(), $job->getData()). If a property read returns null, read Qless\Job before assuming the job is empty.
  • Priority ordering is a numeric sort key, with enqueue time as the tiebreaker. Don't guess the direction — enqueue two jobs with different priorities and pop them once. Then document your convention; mixed conventions across teams are a classic source of "why did the big job jump the queue".
  • Use a dedicated Redis DB or key prefix. Qless owns ql:* keys and FLUSHDB will destroy every outstanding job. Sharing a Redis instance with cache data is fine; sharing a database with something that flushes is not.
  • Debug with redis-cli. SCAN --pattern 'ql:*' shows the layout: ql:queues (set), ql:queue:<name> (sorted set), ql:job:<jid> (hash), ql:workers, ql:locks, ql:tags. When a job "vanishes", HGETALL ql:job:<jid> tells you whether it is complete, failed, or waiting on a dependency in seconds.
  • Stale code in long-running workers. A daemonised worker loads your classes once. After a deploy it will happily run old handler code against new payloads. Restart workers on deploy (Supervisor autorestart + a restart hook) or exit-on-drain.
  • pop() is non-blocking in most ports. A tight while (true) { pop(); } will hammer Redis with thousands of calls a second and show up as CPU on the Redis box. Sleep a few hundred milliseconds when the pop is empty.
  • Server time vs. PHP time. Qless uses Redis TIME inside its Lua for scheduling decisions. Compute delays as offsets (delay => 300), not absolute timestamps from time() on a worker, or a skewed worker host will schedule jobs into the past or the far future.
  • Interop caveat. If you also run qless-python/qless-ruby against the same Redis, all clients must agree on the Lua scripts and key layout. This package is a small community binding, so pin its version in composer.json and upgrade deliberately rather than floating.
  • Extension points worth knowing: $job->cancel() (discard), $job->retry() (re-enqueue from the failure path), $client->jobs->get($jid) (inspect by id), tag queries via $client->jobs->tagged(...), and $queue->stats() for backlog/latency dashboards. Building a tiny health endpoint over stats() on day one pays for itself the first time a queue backs up.
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.
jinas/dhivatar
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