zerkalica/semaphore-bundle
Symfony bundle that brings semaphore-based locking to your app: guard critical sections and shared resources with a simple, configurable service so concurrent requests and workers don't collide. Lightweight integration for safe, predictable concurrency.
Read this first: zerkalica/semaphore-bundle is a Symfony bundle, not a Laravel package. It has 2 stars, 0 dependents, and no recent activity — treat it as abandoned reference code, not a dependency you install into a Laravel app. There is no extra.laravel block, so Package Discovery won't register anything even if Composer pulls it in.
What it actually does: it wires the underlying zerkalica/semaphore library into Symfony's DI container as configurable services, so you can inject named semaphores. The library itself is a thin wrapper over PHP's SysV semaphore functions (sem_get / sem_acquire / sem_release, ext-sysvsem) — i.e. process-level mutual exclusion on a single machine.
Minimal path if you want to evaluate it:
composer require zerkalica/semaphore-bundle in a scratch Symfony app (not in Laravel) just to read the code, or clone the repo.DependencyInjection/ extension and Resources/config/ first — that's where the named-semaphore configuration schema lives. Then read the library's semaphore/factory classes.// app/Providers/AppServiceProvider.php
$this->app->singleton('semaphore.exports', fn () => new \Zerkalica\Semaphore\Semaphore('nightly-export'));
if (! $sem->acquire()) { // check the actual signature in the source
$this->warn('already running, exiting');
return self::SUCCESS;
}
try {
$this->doTheWork();
} finally {
$sem->release();
}
Before you go further, compare against what Laravel gives you for free — for most tasks the native options are strictly better and maintained:
Cache::lock('nightly-export', 600)->get(); // Redis/database/array driver
// or on a queued job:
public function middleware(): array { return [(new WithoutOverlapping('exports'))->expireAfter(600)]; }
Service-provider binding (the only sane integration). Skip the bundle, wrap the library once and inject it. Keep the semaphore key in config so environments can't collide:
// config/services.php
'semaphores' => ['exports' => 'app-'.env('APP_ENV').'-exports'],
$this->app->singleton(Semaphore::class, fn ($app) =>
new Semaphore(config('services.semaphores.exports'))
);
Try/finally is mandatory. Every acquire must be paired with a release in finally. Wrap the whole pattern in a small helper so nobody on the team hand-rolls it:
final class Exclusive
{
public function __construct(private Semaphore $sem) {}
public function run(callable $fn, mixed $fallback = null): mixed
{
if (! $this->sem->acquire()) {
Log::info('semaphore busy, skipping', ['name' => $this->sem->getName()]);
return $fallback;
}
try { return $fn(); }
finally { $this->sem->release(); }
}
}
Command-level guard. In handle(), acquire at the top; if it fails, return self::SUCCESS rather than an error — scheduled overlaps are normal, not failures. Pair with withoutOverlapping() in routes/console.php for the scheduler, which is the Laravel-native equivalent.
Job middleware. If you like the bundle's ergonomics, model a job middleware on it — but ship WithoutOverlapping in production and keep the semaphore version for jobs that must not share a process-level resource (local disk spool, a fixed temp path, a hardware device).
Named-semaphore registry. If you port the bundle's config approach, keep a single map of name → purpose so audit is trivial, and give each critical section its own name. Sharing one semaphore across unrelated tasks serializes them for no reason.
Where the pattern genuinely earns its keep vs. Redis locks: SysV semaphores don't need a network round-trip and don't expire. For very hot, very short critical sections on one box (a local file index rebuild), that's a real advantage. For anything else, use Cache::lock().
ext-sysvsem is required. Check function_exists('sem_get') at boot, not at 2 a.m. when the cron fails. Many containers, Alpine images, and shared hosts ship PHP without it.sem_get's fourth argument) are cleaned up when the process dies, so SIGKILL/OOM is usually survivable — but a thrown exception with no finally is not. Don't rely on auto-release as your error handling.ipcs -s and remove stale ones with ipcrm -s <id>. In Docker, leaked semaphores die with the container, which masks bugs locally and makes them reappear in long-lived VMs.sem_* in an afternoon.Resources/config/*.xml|yaml to see the exact constructor arguments, then copy that wiring into your own provider — that's the whole migration.How can I help you explore Laravel packages today?