kamisama/php-resque-ex
Redis-backed background job processing for PHP. Enqueue jobs and run workers to process them asynchronously, with queue management, logging, and failure handling. An enhanced fork of php-resque with extra features and fixes for production use.
kamisama/php-resque-ex is a maintained-fork-lineage port of the Ruby Resque background job system. It is not a Laravel queue driver — there is no ShouldQueue, no auto-discovery, and jobs do not run inside Laravel's lifecycle unless you bootstrap the framework yourself. The minimum viable setup is three pieces: a backend connection, an enqueue call, and a worker process.
1. Install and configure the Redis backend. Put this in a service provider (or a dedicated ResqueServiceProvider):
// composer.json: "kamisama/php-resque-ex": "^1.2"
use Resque;
public function boot()
{
Resque::setBackend(
config('database.redis.default.host') . ':' . config('database.redis.default.port'),
(int) config('database.redis.default.database')
);
if ($password = config('database.redis.default.password')) {
Resque::redis()->auth($password);
}
}
If the phpredis extension is installed, it is used automatically (much faster than the bundled pure-PHP Redisent client). Otherwise you get the slower fallback — acceptable for low volume, but verify which client is in use before benchmarking.
2. Create a job class. Any class with a public perform() method works. $this->args holds the payload:
class SendWelcomeEmail
{
public function perform()
{
$user = User::find($this->args['userId']); // needs Laravel booted — see Gotchas
Mail::to($user->email)->send(new WelcomeMail($user));
}
}
3. Enqueue from normal Laravel code:
Resque::enqueue('default', SendWelcomeEmail::class, ['userId' => $user->id]);
4. Run a worker (under supervisor, systemd, or a container sidecar):
QUEUE=default INTERVAL=5 php vendor/bin/resque
Where to look first: Resque::enqueue() and the bin/resque executable are the entire public surface you will touch daily. The Resque_Job_Status and Resque_Stat classes are the secondary API for observability. Read the job class perform() convention once and the rest is Redis conventions.
First use case: offloading slow mailers, third-party API calls, PDF generation, or image resizing from the HTTP request. It is a workhorse for deferring anything that shouldn't block a response and doesn't need complex retry/backoff semantics.
A bootstrapped base job. Jobs execute in the worker process, which does not share Laravel's container. The cleanest pattern is a custom worker entry point that boots the framework once, so perform() can use facades and the container normally:
// bin/resque-laravel
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$queues = explode(',', getenv('QUEUE') ?: 'default');
$worker = new Resque_Worker($queues);
$worker->work((int) (getenv('INTERVAL') ?: 5));
This is the single most important integration decision — do it up front or every job will silently fail on the first app()/Mail::/DB:: call.
Queue prioritization. Pass a comma-separated list and Resque polls them in order — the leftmost is highest priority:
QUEUE=critical,high,default,low php bin/resque
QUEUE=* processes every queue, but priority order becomes Redis-set dependent. Prefer explicit lists.
Job status tracking. Off by default; enable per dispatch and persist the token yourself (Redis is volatile):
$token = Resque::enqueue('default', ReportJob::class, [$id], true);
// later
$status = (new Resque_Job_Status($token))->get();
// Resque_Job_Status::STATUS_QUEUED | RUNNING | COMPLETE | FAILED
Store $token on a model or in cache to poll from the UI. Note that completed-status records expire (Resque_Job_Status uses a TTL), so don't treat them as a permanent audit log.
Multiple worker processes. Use supervisor (a dozen workers on the same host is typical), each with its own QUEUE if you want isolation. INTERVAL controls Redis polling frequency — the default of 5 seconds is fine; lowering it increases idle load, raising it increases latency.
Failure handling and retries. Resque has a configurable failure backend (Resque_Failure_Redis by default, exposed via the failed queue). There is no built-in retry. The idiomatic pattern is to catch inside perform() and re-enqueue with a counter arg:
public function perform()
{
try {
$this->handle();
} catch (\Throwable $e) {
$attempt = ($this->args['attempt'] ?? 0) + 1;
if ($attempt < 3) {
Resque::enqueue($this->queue, static::class, array_merge($this->args, ['attempt' => $attempt]));
} else {
throw $e; // let Resque record the failure
}
}
}
Logging. This fork improves on upstream logging: env vars LOGGING, LOGLEVEL, LOG control worker logs, and jobs can write through Monolog if you wire it in the worker script. In a container it's usually cleaner to log to stderr and let the platform aggregate.
Testing. Static Resque::enqueue is hard to mock. The pragmatic approach is to point Resque::setBackend at a test Redis database in phpunit.xml and assert against raw Redis keys:
$this->assertSame(1, Resque::size('default'));
Then pop the payload with Resque::redis()->lpop('queue:default') and json_decode it to verify the class and args.
bzirost/php-resque or a private fork is the fallback.app(), facades, Eloquent, and config all fail — usually with a confusing "class not found" or "container not set" error rather than a clear bootstrap warning. Always run your worker via a custom entry point, not raw vendor/bin/resque, in a Laravel app.$this->args is a single array passed verbatim through JSON. No named arguments, no typed value objects, no serialization magic. Keep args scalar and flat; put large payloads in a DB row and pass the ID.Resque::enqueue('default', MyJob::class, $args) resolves MyJob through the worker's autoloader at runtime. If the class is renamed or moved, the enqueue succeeds and the job fails silently at the worker — so add an assertion or a class_exists check on the enqueue side if you refactor often.failed queue is your dead letter and it's just a Redis list. Budget for a small admin tool if reliability matters.QUIT is graceful (finishes the current job), TERM/INT are immediate, USR1 pauses, USR2 resumes. Laravel's queue:restart convention doesn't exist here — you send kill -QUIT <pid> from your deploy script or supervisor. Killing with TERM will corrupt in-flight jobs.resque: (resque:queue:default, resque:worker:*, resque:stat:*, resque:failed). If you share Redis with another app using resque: prefixes, workers will steal each other's jobs. Isolate via a separate database index or a key-prefix convention enforced at the client.phpredis vs pure PHP. The extension is detected at runtime. This matters because the two clients have subtly different behavior on auth/select; if auth seems to work in one place but the worker can't connect, check which client loaded. Pure-PHP is roughly an order of magnitude slower per job dequeue.INTERVAL is in seconds, and workers block nothing else. A single-threaded worker processes one job at a time. Concurrency = process count. Plan the supervisor config accordingly, and remember each worker holds its own Redis connection.Resque_Job_Status records expire; poll status quickly or move completion to your own storage. Failed statuses linger differently from completed ones.Resque_Failure_Interface for custom failure backends (e.g., reporting to Sentry/Bugsnag), Resque_Log subclasses for log routing, and $worker->registerSigHandlers() if you're embedding workers inside another process. Resque::redis() gives you the raw client — useful for health checks (ping) but bypasses library abstractions.perform() uses constructor promotion or enums in signatures, they won't survive the JSON round-trip. Keep the entry point to perform() and plain data.php-resque-ex remains one of the few sane PHP options — otherwise it's a harder sell than it was a decade ago.How can I help you explore Laravel packages today?