hautelook/gearman-bundle
A Symfony bundle that integrates the Gearman job server, letting you define and run background workers, dispatch asynchronous jobs, and manage queues through Symfony services and configuration.
Blunt first read: this is a Symfony2-era bundle (Kernel + DI container + symfony/framework-bundle ~2.x), with no stars and no dependents. Laravel 9+ ships Symfony 6/7 components, so composer require hautelook/gearman-bundle will almost certainly fail resolution. Treat it as reference material for how to wire a Gearman client, not as a dependency you'll actually install. You're a Laravel developer; what you want out of this package is the 100-line wrapper around ext-gearman, and you'll write that yourself in a service provider.
Minimal steps if you're evaluating or on a legacy Symfony 2 app:
pecl install gearman — the bundle is a thin wrapper over ext-gearman, nothing works without the extension and a running gearmand (gearmand -d -L 0.0.0.0).composer require hautelook/gearman-bundle (legacy apps only). Register new Hautelook\GearmanBundle\HautelookGearmanBundle() in AppKernel.php.hautelook_gearman config key — typically a servers: map of host/port plus a default_server.DependencyInjection/Configuration.php (the config tree — the only real API surface), Resources/config/services.xml (grep for gearman to get the exact service id), then Service/ for the client wrapper. Those three files are the whole package.First use case — fire-and-forget a background job (image resize, email fan-out, webhook delivery):
// Symfony 2 style
$client = $container->get('hautelook_gearman.service.gearman'); // confirm id in services.xml
$client->doBackground('image.resize', json_encode(['id' => 42, 'w' => 800]));
The equivalent Laravel-native call, skipping the bundle entirely:
$client = new \GearmanClient();
$client->addServer(config('gearman.host'), config('gearman.port'));
$client->doBackground('image.resize', json_encode(['id' => 42, 'w' => 800]));
That second snippet is the whole value proposition. Notice how little the bundle adds.
Wrap it in a service provider, don't import the bundle. Bind a singleton behind an interface so you can fake it in tests and swap drivers later:
// app/Providers/GearmanServiceProvider.php
$this->app->singleton(GearmanDispatcher::class, fn () => new GearmanDispatcher(
config('gearman.host'), config('gearman.port')
));
final class GearmanDispatcher
{
private \GearmanClient $client;
public function __construct(string $host, int $port) {
$this->client = new \GearmanClient();
$this->client->addServer($host, $port);
}
public function background(string $fn, array $payload, ?string $unique = null): string
{
return $this->client->doBackground($fn, json_encode($payload), $unique);
}
}
Background vs. normal is the one decision that matters. doBackground() returns a job handle and never blocks — use it for anything user-facing. doNormal() blocks until a worker replies — use it only for request/response RPC (e.g., a fast price lookup), and always setTimeout() first or you'll hang a PHP-FPM worker.
Payloads are a contract, not an object. ext-gearman ships bytes. json_encode on the producer, json_decode($job->workload(), true) on the consumer. Never serialize() — that couples you to PHP on both ends and breaks the day you add a Python or Go worker.
The worker is a separate long-running process. Make it an Artisan command so it shares your app's container, config, and Eloquent:
class GearmanWork extends Command
{
protected $signature = 'gearman:work {--max=500}';
public function handle() {
$worker = new \GearmanWorker();
$worker->addServer(config('gearman.host'), config('gearman.port'));
$worker->addFunction('image.resize', [$this, 'resize']);
$n = 0;
while ($worker->work() && ++$n < $this->option('max')) { /* ... */ }
}
}
Supervise it with Supervisor, autorestart=true, and a --max recycle to avoid the classic PHP worker memory leak.
Dedupe with the unique key, not your own locking. Both the bundle and raw ext-gearman let you pass a unique string; gearmand will coalesce identical in-flight background jobs with the same function name + unique key. Great for "recompute this report" storms.
Testing seam: type-hint GearmanDispatcher (an interface) in controllers and jobs, bind a fake in TestCase::setUp that records calls. Never let a unit test touch a socket.
--ignore-platform-reqs your way past this — you'll get a container that can't boot. Vendor the 60 lines instead.hautelook_gearman.* is guessable but the authoritative answer is Resources/config/services.xml. If you're on legacy Symfony, php app/console container:debug | grep gearman.doBackground arrive as garbage unless you encode them. Half of all "Gearman is broken" reports are this.doBackground gives you a handle, not an outcome. There's no built-in status callback. If you need "did it finish?", you're polling jobStatus() or writing results to your own DB — plan for the second one.image.resize and image.thumbnail with the same unique string are two jobs.gearmand is down, addServer succeeds silently and doBackground throws on submit. Wrap the dispatch and decide: log-and-fall-back-to-sync (fine for emails) or throw (correct for billing).gearadmin --status (queue depth per function), gearadmin --workers (are your workers even registered?), and start gearmand with -vvv to see submissions. A queue that never drains is almost always zero workers registered, not a broken client.servers config map (multiple gearmand hosts — addServer is called per entry, giving you failover), and the client wrapper class itself, which you can decorate with your own logging/metrics without touching the bundle.ext-gearman, or reach for the queue system you already have (database or Redis) — you'd need retries, backoff, and a dead-letter story anyway, and Laravel's queue worker gives you those for free.How can I help you explore Laravel packages today?