pwt777/pheanstalk-bundle
pwt777/pheanstalk-bundle integrates the Pheanstalk PHP client with Symfony, making it easy to configure and inject a beanstalkd connection and manage job queues from your application's service container.
Reality check first. pwt777/pheanstalk-bundle is a Symfony bundle, not a Laravel package. It is a fork of leezy/pheanstalk-bundle and ships zero Laravel service providers, facades, or queue connectors. Under Laravel you are really adopting two things: the pheanstalk/pheanstalk client library, and a thin Symfony DI shell that wraps it. Treat the bundle as optional scaffolding; use the client directly.
What 4.0.4 changed. The only change is declared Symfony 6 compatibility. There are no new features, no API changes, and no Laravel-side impact. The practical consequence is dependency resolution: the bundle can now coexist with the Symfony 6.x component set that Laravel 9 and 10 pin, so composer require is far less likely to dead-end than it was with the previous 1.x tagging. It does not make the bundle a Laravel integration, and it does not help Laravel 11+ apps, which pin Symfony 7.
Minimum viable install
composer require pheanstalk/pheanstalk
# optional, only if you want the Symfony wiring + CLI commands
composer require "pwt777/pheanstalk-bundle:^4.0" # 4.0.4 is the Symfony 6 line
Verify the fork pulls down cleanly against your PHP/Symfony constraint set:
composer why pwt777/pheanstalk-bundle
composer show pwt777/pheanstalk-bundle
composer why-not pwt777/pheanstalk-bundle 4.0.4 # run this on Laravel 11+ before wasting time
Where to look first
vendor/pwt777/pheanstalk-bundle/ — read LeezyPheanstalkBundle.php, DependencyInjection/Configuration.php (defines the leezy_pheanstalk tree), and Resources/config/services.yml. Those three files tell you every service ID and config key you might want to replicate in Laravel.composer.json of the bundle — confirm which pheanstalk/pheanstalk major and which symfony/* ranges 4.0.4 actually pulls. That single file decides whether this installs in your app at all.pheanstalk/pheanstalk source — that is the API you will actually call day-to-day.use/put/reserve/delete/release/bury/kick.First use case — a single producer
Beanstalkd is a job server; the pattern is a producer pushing JSON payloads to a named tube and a long-running worker consuming them.
use Pheanstalk\Pheanstalk;
$client = Pheanstalk::create('127.0.0.1', 11300, 60); // host, port, connect timeout
$client->useTube('emails')->put(
json_encode(['to' => $user->email, 'template' => 'welcome']),
Pheanstalk::DEFAULT_PRIORITY, // 1024
0, // delay seconds
60 // time to run (TTR)
);
If you have installed the Symfony bundle, the equivalent is resolving the leezy.pheanstalk service from your container instead of constructing the client by hand. In a Laravel app, do not try to boot the Symfony bundle — just mirror its config shape in config/pheanstalk.php and bind a singleton (see next section).
Pattern 1 — singleton client bound in a service provider. This is the correct way to use the package under Laravel. Ignore the bundle's DI entirely and lift its configuration contract.
// config/pheanstalk.php (mirrors leezy_pheanstalk.pheanstalks)
return [
'default' => env('BEANSTALKD_CONNECTION', 'primary'),
'connections' => [
'primary' => [
'host' => env('BEANSTALKD_HOST', '127.0.0.1'),
'port' => (int) env('BEANSTALKD_PORT', 11300),
'timeout' => (int) env('BEANSTALKD_TIMEOUT', 60),
],
],
];
// app/Providers/BeanstalkdServiceProvider.php
use Pheanstalk\Pheanstalk;
$this->app->singleton(Pheanstalk::class, function ($app) {
$c = config('pheanstalk.connections.' . config('pheanstalk.default'));
if (method_exists(Pheanstalk::class, 'create')) {
return Pheanstalk::create($c['host'], $c['port'], $c['timeout']);
}
return new Pheanstalk($c['host'], $c['port'], $c['timeout']); // v3 fallback
});
This pattern is unaffected by 4.0.4 — the bundle is not in the call path, which is exactly why it is the recommended one.
Pattern 2 — producer in a queued job or controller. Keep payloads small and version-tagged so consumers can evolve independently:
$client->useTube('reports')->put(json_encode([
'v' => 1,
'job' => 'generate-pdf',
'params' => ['report_id' => $report->id],
]), 1024, 0, 300);
Pattern 3 — artisan worker loop. Laravel has no native Beanstalkd queue driver, so write a dedicated command. This is where the bundle's Symfony commands would have lived; in Laravel you own them.
while (true) {
$job = $client->watch('reports')->ignore('default')
->reserveWithTimeout(10); // returns null on timeout in pheanstalk v4
if ($job === null) {
continue; // idle tick — also a good place to check a stop flag
}
try {
dispatch_sync(new HandleReportsPayload($job->getData()));
$client->delete($job);
} catch (Throwable $e) {
report($e);
$client->bury($job, 100); // park for inspection; do not silently release-loop
}
}
Notes on this pattern: always ignore('default') after watch(), otherwise you will consume unrelated tubes. Prefer a supervised process (supervisord, laravel-horizon-style wrapper, or systemd) so a segfaulted worker restarts. Handle SIGTERM and finish the in-flight job before exiting.
Pattern 4 — port the bundle's tube-management commands. The bundle's value-add is its console commands for operational tasks (list servers, stats per tube, peek at ready/buried jobs, kick buried jobs, pause a tube, delete a tube). You cannot run them under Artisan, but you can port the few you actually use as thin Artisan commands that call the same Pheanstalk methods: statsTube($tube), peekReady($tube), peekBuried($tube), kick($max), pauseTube($tube, $seconds).
Pattern 5 — multi-connection routing. The bundle supports several named pheanstalk connections and a "beanstalkd locator" for resolving them by name. Reproduce that with a ConnectionLocator-style class or Manager that lazily builds clients keyed by connection name; this lets you keep a primary for general work and an analytics server for heavier pipelines.
Integration tips
statsTube($tube)['current-jobs-ready']) from a scheduled command rather than from inside workers.Maintenance status moved from "abandoned" to "barely alive." The 4.0.4 release adds Symfony 6 compatibility and nothing else. That is the first sign of life in years for a fork with minimal adoption, so treat it as a one-off compatibility patch rather than a resumption of active maintenance. Prefer depending on pheanstalk/pheanstalk directly and copying the ~40 lines of configuration/services you need. Pin an exact constraint ("pwt777/pheanstalk-bundle": "^4.0") so a future 4.x patch cannot shift behavior under you.
Symfony 6 compatibility is a double-edged sword. On Laravel 9/10 (Symfony 6 components) this release is what makes the bundle installable at all — that is a real improvement over the 1.x line. On Laravel 11+ (Symfony 7 components) the ^6.0 constraint is now a harder blocker than before, not a softer one: Composer will refuse the install rather than resolve to something broken. Run composer why-not pwt777/pheanstalk-bundle 4.0.4 first; if it reports a Symfony 7 conflict, stop there and use the client directly.
Symfony/Laravel dependency collisions (still). Even on Laravel 9/10, the bundle pulls symfony/* components that overlap with Laravel's own pinned versions. Composer may resolve to versions that break Laravel, or refuse to install. This alone is a strong reason to vendor only the pheanstalk client.
Check which client major 4.0.4 targets. The API changed between client majors: v4 exposes Pheanstalk::create() and typed job objects; v3 used new Pheanstalk(...) and untyped returns. The bundle's services.yml may still target the older client. Guard with method_exists() (as shown above) or pin pheanstalk/pheanstalk to whichever major matches, then read the changelog. Do not assume the bundle's 4.x major tracks the client's 4.x major — they are versioned independently.
No auto-discovery. Laravel will not register the Symfony bundle. Any attempt to $this->app->register(LeezyPheanstalkBundle::class) will fail — it implements Symfony\Component\HttpKernel\Bundle\Bundle, not Illuminate\Support\ServiceProvider. Symfony 6 compatibility does not change this.
reserve() blocks and can be dangerous. In pheanstalk v4, reserveWithTimeout() returns null on timeout; in v3 behavior differs (throws or blocks indefinitely depending on version and the reserve-timeout argument). Never call a blocking reserve inside an HTTP request — it will hold the PHP-FPM worker. Always set a finite server-side reserve-timeout and pass a shorter client-side deadline if the client supports it.
Duplicate delivery is normal, not an error. If a worker dies, exceeds TTR, or the connection drops after reserve, Beanstalkd re-queues the job. Design handlers to be idempotent. Keep TTR comfortably above the p99 handler time.
Buried jobs accumulate silently. bury (or a handler that throws and buries) removes the job from the ready queue. Without an operator or a scheduled peekBuried/kick routine, buried jobs become invisible work loss. Add a scheduled command that reports buried counts per tube.
use/watch are per-connection state. The connection keeps a current tube for put and a watch list for reserve. Reusing a long-lived singleton across different jobs without re-issuing useTube()/watch() will silently send work to the wrong tube. Re-assert tube selection at the start of every producer call and every worker loop iteration.
Ignore the default tube. A worker that only calls watch('emails') watches default too. Always pair with ->ignore('default').
CLI commands are Symfony Console, not Artisan. You cannot invoke them via php artisan. If you truly need them, run the Symfony binary out-of-band (or write a tiny Symfony console entrypoint in the vendor context), but expect config to be read from a Symfony parameters.yaml/services.yaml that doesn't exist in a Laravel app. The 4.0.4 Symfony 6 bump actually makes the bundle's own console bootstrapping work again on modern Symfony — which is irrelevant in Laravel, and porting is still faster.
Extension points are Symfony-oriented. The bundle's useful customization surfaces are the connection factory and the command set — both registered through Symfony DI service tags. Under Laravel, the equivalent extension point is your own service provider and your own Artisan commands; treat the bundle as documentation of the Beanstalkd API rather than code to extend.
Debugging checklist. Connection refused → check Beanstalkd is actually listening on 11300 and not firewalled. Producer "works" but consumer idle → verify tube names match exactly (case-sensitive) and that the consumer isn't accidentally watching default. Jobs disappearing → check TTR re-delivery and whether other workers already deleted them. stats() / statsTube() output is the fastest way to distinguish "queue empty" from "queue stuck due to buried jobs."
How can I help you explore Laravel packages today?